Concepts
Closure
A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives you access to an outer function's scope from an inner function
-- mdn_web_docs
Using closure has below benefits:
- Data encapsulation: It can be used to create private variables and functions that can't be accessed from outside the closure. This is useful for hiding implementation details and maintaining state in an encapsulated way.
- Functional programming: Closures enable creating functions that can contain context and be passed around for invoke later.
- Event handlers/callbacks: Closures are often used in event handlers and callbacks to maintain state or access variables that were in scope when the handler or callback was defined.
- Module patterns: Closures enable the creation of modules with private and public parts.
Readings
call, apply and bind
Usage
Both call and apply are used to invoke functions with a specific this context and arguments. The difference lies in how they accept arguments, where:
call(thisArg, arg1, arg2, ...): Takes arguments individually.apply(thisArg, [argsArray]): Takes arguments as an array.
If the function is not in strict mode, thisArg will be replaced with the global object if null and undefined is provided, and primitive values will be converted to objects; otherwise, thisArg will remian whatever is passed in.
A common use case to use call and apply is to invoke functions on different objects by explicitly assign the this context. For instance,
const person = {
name: 'John',
greet() {
console.log(`Hello, this is ${this.name}`);
},
};
const anotherPerson = { name: 'Alice' };
const greetFunc = person.greet;
person.greet.call(anotherPerson); // Hello, my name is Alice
person.greet.apply(anotherPerson); // Hello, my name is Alice
greetFunc.call(anotherPerson); // Hello, my name is Alice
greetFunc.call(anotherPerson); // Hello, my name is Alice
function greet() {
console.log(`Hello, my name is ${this.name}`);
}
const person1 = { name: 'John' };
const person2 = { name: 'Alice' };
greet.call(person1); // Hello, my name is John
greet.call(person2); // Hello, my name is Alice
Whereas bind creates a new function with a specific this context and, optionally, preset arguments. bind is most useful for preserving the value of this in methods of classes that you want to pass into other functions
const person = {
name: 'John',
greet() {
console.log(`Hello, this is ${this.name}`);
},
};
const anotherPerson = { name: 'Alice' };
const greetFunc = person.greet;
greetFunc.bind(anotherPerson)(); // // Hello, my name is Alice
bind is often used to preserve the this context when a function is passed as callback.
class Person {
constructor(name) {
this.name = name;
}
// this context lost when execute context is lost
greet() {
console.log(`Hello, my name is ${this.name}`);
}
// Arrow functions have the this value bound to its lexical context.
hi = () => {
console.log(`Hi, this is ${this.name}`);
}
};
const john = new Person('John Doe');
setTimeout(john.greet, 1000); // Hello, my name is undefined
setTimeout(john.greet.bind(john), 2000); // Hello, my name is John Doe
setTimeout(john.hi, 2000); // Hello, my name is John Doe
bind can also be used to create a new function with some arguments pre-set. This is known as partial application or currying.
function getName(firstName='', lastName='') {
return firstName + ' ' + lastName;
}
const printName = getName.bind(null, 'John');
console.log(printName()); // John
console.log(printName('Doe')); // John Doe
Interchangeability
This three functions can be treated as sibling functions and can be implemented using one another.
/**
* Custom call
*/
Function.prototype.customCall = function(thisArgs, ...args){
return this.bind(thisArg)(...argArray);
}
// or
Function.prototype.customCall = function(thisArgs, ...args){
return this.bind(thisArg, ...argArray)();
}
/**
* Custom apply
*/
Function.prototype.customApply = function (thisArg, args = []) {
return this.bind(thisArg)(...args);
};
// or
Function.prototype.customApply = function (thisArg, args = []) {
return this.bind(thisArg)(...args);
};
// or
Function.prototype.customApply = function (thisArg, args = []) {
return this.call(thisArg, ...argArray);
};
/**
* Custom Bind
*/
Function.prototype.customBind = function (thisArg, ...argArray) {
const originalMethod = this;
return function (...args) {
return originalMethod.apply(thisArg, [...argArray, ...args]);
};
};
// or
Function.prototype.customBind = function (thisArg, ...argArray) {
const originalMethod = this;
return function (...args) {
return originalMethod.call(thisArg, ...argArray, ...args);
};
};
new
The new operator lets developers create an instance of a user-defined object type or of one of the built-in object types that has a constructor function.
When creating a new object with new, four steps are conducted:
- Creates a new object
- Sets the prototype
- Bind
thisto the new object - Returns the new object