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.
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);
};
};