Skip to main content

JS 02 - functions, closures, this, objects

Functions

function showMessage() {
alert("Hello everyone!");
}
  • Does not need return statement (returns undefined).
  • Variables declared inside function are local
  • Can access variables from outside
  • Outer variable is only used, if there is not a local one (shadowed)
  • Pass arbitrary data to functions using parameters
  • If value for parameter is not provided – it becomes undefined
  • Can use default values function showMessage(text = "greetings"){}
  • Default is evaluated when no parameter was presented function showMessage(text = getText() ){}

Function expressions

let sayHi = function () {
alert("Hello");
};
  • In JavaScript, a function is not a "magical language structure", but a special kind of value.
  • Can be passed around as any other value
  • typeof( Math.abs ) === "function" // true
  • A Function Expression is created when the execution reaches it and is usable only from that moment.
  • A Function Declaration can be called earlier than it is defined.
  • In strict mode, when a Function Declaration is within a code block, it’s visible everywhere inside that block. But not outside of it.

Arrow functions

Multiline arrow functions – you need return statement

let sum = (a, b) => {
// the curly brace opens a multiline function
let result = a + b;
return result; // if we use curly braces, then we need explicit "return"
};

alert(sum(1, 2)); // 3

For future discussion:

  • Arrow functions have no this. If this is accessed, it’s taken from outside.
  • Arrow functions have no arguments variable
  • No super also

Closures

A closure is like a backpack. Imagine a function carrying a backpack that contains all the variables it needs from the environment where it was created. No matter where the function goes, it always has access to its backpack.
More formally, a closure is created when a function retains access to variables from its outer scope, even after that scope has finished executing.

function createCounter() {
let count = 0; // This is our backpack
return function() {
count++; // Accessing count from the outer scope
return count;
};
}

const counter = createCounter();

console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

Can be used to create private variables

function playerScore() {
let score = 0;
return {
increase: function() {
score += 10;
return score;
},
getScore: function() {
return score;
}
};
}

const player = playerScore();

console.log(player.getScore()); // 0
console.log(player.increase()); // 10

Function Currying in JavaScript (Closure Example)

Function currying is a technique that transforms a function with multiple arguments into a sequence of functions that each take one argument at a time, using closures to remember previously passed values.

  • Breaks a multi-argument function into unary functions.
  • Uses closures to retain earlier arguments.
  • Enables partial application of functions.
  • Helps create reusable and specialized functions.
// Normal Function
// function add(a, b) {
// return a + b;
// }
// console.log(add(2, 3));

// Function Currying
function add(a) {
return function(b) {
return a + b;
};
}

const addWith2 = add(2);
console.log(addWith2(3)); // 5

Lexical Scoping

Closures rely on lexical scoping, which means a function’s scope is determined by where it is defined, not where it is executed, allowing inner functions to access variables from their outer function.

  • Scope is fixed at function definition time.
  • Inner functions can access outer function variables.
  • Enables closures to "remember" their environment.

Closures with this keyword

Closures can be confusing with the this keyword because this is determined by how a function is called, not where it is defined, so inside a closure it may not refer to the expected object.

  • this is not lexically scoped (except in arrow functions).
  • Its value depends on the calling context.
  • Closures don’t change how this works.
  • Arrow functions inherit this from their surrounding scope.
function Person(name) {
this.name = name;

this.sayName = function () {
console.log(this.name);
};

setTimeout(function () {
console.log(this.name);
// Undefined because 'this' refers to global object
}.bind(this), 1000);
// Fix with bind
}

const G = new Person("GFG");
G.sayName();

bind()

The bind() method creates a new function that, when called, has its this keyword set to the provided value.

call()

The main differences between bind() and call() is that the call() method:

  • Accepts additional parameters as well
  • Executes the function it was called upon right away.
  • The call() method does not make a copy of the function it is being called on.

The call() method calls a function with a given this value and arguments provided individually

apply()

call() and apply() serve the exact same purpose. The only difference between how they work is that call() expects all parameters to be passed in individually, whereas apply() expects an array of all of our parameters.

function greet(greeting, punctuation) {
console.log(greeting + ', ' + this.name + punctuation);
}

const person = { name: 'Alice' };

greet.call(person, 'Hello', '!'); // Output: Hello, Alice!
greet.apply(person, ['Hi', '?']); // Output: Hi, Alice?

Objects - Basics 1

  • Objects are used to store keyed collections of various data and more complex entities

  • Can be created with { } or with new Object()

  • Properties can be accessed and added two ways: dot notation and brackets.

    let user = {
    name: "FooBar",
    };
    user.name = "akaver";
    user["name"] = "akaver";
    user["fist and lastname"] = "Andres Käver"; //can use multiword properties
  • Properties can be removed with delete operator. delete user.name;

  • Properties names can be computed in case of [].

    let fruit = "apple";
    let bag = {
    [fruit + "Computers"]: 5,
    };

Objects - Basics 2

  • If property name in object will be the same as variable – shorthand syntax

    let name = "fooBar";
    let person = {
    name, // same as name:name
    };
  • Property name limitations

  • Strings or symbols (will be covered later)

  • Other types are converted to strings.

    let x = {
    1: "foo",
    };
    x["1"] === "foo";
  • Can use keywords as names.

    let x = { return: "foobar" };
  • Special property proto (later)

Objects - Basics 3

  • Property existance test

    user.noSuchProperty === undefined; // true when not found
  • Special operator in – key in object

    "age" in user;
    let key = "age";
    key in user;
  • Edge case

let x = { name: undefined }; // property exists

console.log(x.name === undefined); // true – but property is there?
console.log("name" in x); // true
console.log(x.foo === undefined); // true
console.log("foo" in x); // false
  • Note: in checks the prototype chain too. To check only own properties, use Object.hasOwn(obj, key) (ES2022) or obj.hasOwnProperty(key)

Objects - Basics 4

  • To walk over all keys of an object, there exists a special form of the loop: for..in
  • Or use Object.keys(someObject) – returns array of property names.
  • Or use Object.values(someObject)– returns array of property values.
let user = {
name: "John",
age: 30,
isAdmin: true,
};

for (let key in user) {
// keys
alert(key); // name, age, isAdmin

// values of the keys
alert(user[key]); // John, 30, true
}

Objects - Basics 5

  • Object properties – order
  • Integer properties are sorted, others appear in creation order.
  • The integer property - a string that can be converted to-and-from an integer without a change.

Objects - Basics 6

Comparison by reference

let a = {};
let b = {};
alert(a == b); // false

Objects - Basics 7

  • Object copying – BY REFERENCE
  • Shallow copy
Object.assign(dest, [src1, src2, src3...]);
let clone = Object.assign({}, user);
  • What to do when properties are other objects – deep copy
  • There's a standard algorithm for deep cloning for more complex cases, called the Structured cloning algorithm. Use the built-in structuredClone(obj) (available in all modern browsers and Node.js 17+). For older environments, use lodash _.cloneDeep(obj).
let user = {
name: "John",
age: 30,
isAdmin: true,
};

let clone = {}; // the new empty object

// let's copy all user properties into it

for (let key in user) {
clone[key] = user[key];
}

// now clone is a fully independent clone
clone.name = "Pete";

alert(user.name); // still John in the original object

Symbols 1

Somewhat advanced

Symbol is a primitive type for unique identifiers.

Symbols are created with Symbol() call with an optional description (name).

Symbols are always different values, even if they have the same name.

For same-named symbols to be equal, use the global registry: Symbol.for(key) returns (creates if needed) a global symbol with key as the name. Multiple calls of Symbol.for with the same key return exactly the same symbol.

Symbols 2

Symbols don’t auto-convert to strings.

let id = Symbol("id");
alert(id); // error
alert(id.toString()); // ok "Symbol(id)"

Symbols allow to create "hidden" properties of an object, that no other part of code can accidentally access or overwrite.

let id = Symbol("id");
user[id] = 1;
let x = { [id]: "foo" };

Symbols 3

  • Symbols are skipped by for…in
  • Object.keys(user) also ignores them
  • Built-in method Object.getOwnPropertySymbols(obj) - get all symbols.
  • Also there is a method named Reflect.ownKeys(obj) - returns all keys of an object including symbolic ones.
  • Object.assign copies symbols over to new object.

Object methods, this

  • Objects properties can contain anything – including functions.

  • Can be defined in several ways.

  • To access current instance of the object – use this

let user = {
name: "John",
age: 30,
};

user.sayHi = function () {
alert("Hello!");
};

user = {
sayHi: function () {
alert("Hello");
},
};

user = {
sayHi() {
alert("Hello");
},
};

Arrow functions and this

Arrow functions have no this

Arrow functions are special: they don’t have their "own" this. When referenced this from such a function, it’s taken from the outer "normal" function.

let user = {
firstName: "Toomas",
sayHi() {
let arrow = () => alert(this.firstName);
arrow();
},
};

user.sayHi(); // Toomas
warning

Do not use arrow functions as object methods when you need this to refer to the object — arrow functions inherit this from the enclosing scope, not from the object they are called on.

let user = {
name: "Toomas",
sayHi: () => alert(this.name), // `this` is NOT user, it's the outer scope!
};
user.sayHi(); // undefined

Constructor functions, operator new

Constructor functions – just regular functions, few conventions

  • Named with capital letters

  • Should only be executed with “new" operator

  • A new empty object is created and assigned to this.

  • The function body executes. Usually it modifies this, adds new properties to it.

  • The value of this is returned.

function User(name) {
// this = {}; (implicitly)

// add properties to this
this.name = name;
this.isAdmin = false;

// return this; (implicitly)
}

let user = new User("Jack");

Constructor functions 2

Inside a function, we can check whether it was called with new or without it, using a special new.target property.

function User(name) {
if (!new.target) {
// if you run me without new
return new User(name); // ...I will add new for you
}
this.name = name;
}

let john = User("John"); // redirects call to new User
alert(john.name); // John

Constructor functions, return

Usually, constructors do not have a return statement. Their task is to write all necessary stuff into this, and it automatically becomes the result.

But if there is a return statement:

  • If return is called with an object, then the object is returned instead of this.
  • If return is called with a primitive, it's ignored.
tip

In modern JavaScript, ES6 classes (covered in lecture 03) are the preferred way to create objects with shared behavior. Constructor functions are still important to understand — classes are syntactic sugar over them.

Destructuring assignment

  • Two most used data structures in JS – Array and Object
  • Objects allow us to create a single entity that stores data items by key, and arrays allow us to gather data items into an ordered collection.
  • You can ignore elements with extra commas
  • Works with any iterable on right hand side
  • Assign to anything in left hand side
  • Gather remaining values into new array with “…putRestHere"
  • Absent values – undefined, can use defaults
let arr = ["Foo", "Bar"];

// destructuring assignment
// sets firstName = arr[0]
// and surname = arr[1]
let [firstName, surname] = arr;

Destructuring objects

  • Also works with objects:

    let {var1, var2} = {var1:, var2:}
  • Ordering of variables does not matter, names have to match.

  • Names on the left can be changed { sourceProperty: targetVariable }

  • In case of pre-existing variables use parenthesis

    let title, width, height;
    ({ title, width, height } = { title: "Menu", width: 200, height: 100 });
    function showMenu({ title = "Menu", width = 100, height = 200 } = {}) {
    alert(`${title} ${width} ${height}`);
    }

    showMenu(); // Menu 100 200

Rest parameters

  • Any function can be called with any number of parameters

  • Use rest parameter to collect arguments into array

    function showName(firstName, lastName, ...titles) {}
  • Rest parameter must be the last one

  • There is older built-in array like arguments variable (but its not array)

  • Arrow functions do not have arguments variable

Spread syntax

  • Reverse of rest parameters

    let arr = [3, 5, 1];
    alert(Math.max(...arr)); // 5 (spread turns array into a list of arguments)
  • Can be used several times and mixed with normal parameters

  • Can be used to concatenate arrays

    let arr = [3, 5, 1];
    let arr2 = [8, 9, 15];
    let merged = [0, ...arr, 2, ...arr2];
  • Can be used on objects the same way

    const o1 = { a: "va", b: "vb" };
    const o2 = { c: "vc", ...o1 };

Object getters and setters

From outside, an accessor property looks like a regular one.

let obj = {
get propName() {
// getter, the code executed on getting obj.propName
},

set propName(value) {
// setter, the code executed on setting obj.propName = value
},
};