Deeply understand call, apply, bind

In the previous article, we know that call, apply, and bind are all related to the this point. These three methods are the prototype methods of the JavaScript built-in object Function. A considerable number of front-end engineers still have a relatively simple understanding of them. The so-called solid JavaScript foundation cannot bypass these basic and commonly used knowledge points. This time let us understand and master them in depth.

1.1 Basic introduction

  1. Grammar
func.call(thisArg, param1, param2, ...)//func is a function
func.apply(thisArg, [param1,param2,...])
func.bind(thisArg, param1, param2, ...)

return value: call/apply: return the result of func execution; bind: Returns a copy of func, and has the specified this value and initial parameters.

parameter: thisArg (optional): This of func points to thisArg object; In non-strict mode: if thisArg is specified as null or undefined, then this of func points to the window object;

In strict mode: this of func is undefined; This value of the original value (number, string, Boolean value) will point to the automatic packaging object of the original value, such as String, Number, Boolean.

param1, param2 (optional): The parameters passed to func. If param is not passed or is null/undefined, it means that no parameters need to be passed in. The second parameter of apply is an array-like object, and the value of each item in the array is the parameter passed to func.

Must be a function to call call/apply/bind

Call, apply, and bind are three methods attached to the Function object, and only functions have these methods. As long as it is a function, you can call them. For example: Object.prototype.toString is a function. We often see this usage: Object.prototype.toString.call(data).

What is a borrowing method, let us make an analogy. in life: I usually don't have time to cook. I want to cook a beef hot pot for my family on weekends. But there was no suitable pot, and I didn't want to go out to buy it, so I borrowed a pot from my neighbor to use it. This not only achieved the goal but also saved money. In the program: The A object has a method, and the B object needs to use the same method for some reason. So at this time, do we extend a method for the B object alone, or borrow the method of the A object? Of course, the method of borrowing the A object is more convenient, which not only achieves the purpose but also saves memory. This is the core idea of ​​the call/apply/bind: borrow method. With the aid of the implemented method, this point of the data in the method is changed to reduce duplication of code and save memory. Remember the array-like just now? If it wants to borrow the slice method on the Array prototype chain, it can do this:

let domNodes = Array.prototype.slice.call(document.getElementsByTagName("*"))

By analogy, domNodes can use other methods under Array.

1.2 Application scenarios

In these application scenarios, if you experience more, you will find that their philosophy is: borrowing methods.

  1. Determine the data type Object.prototype.toString is suitable for judging the type, and we can use it to judge almost all types of data:

Object.prototype.toString is suitable for judging the type, and we can use it to judge almost all types of data:

function isType(data, type) {
  const typeObj = {
    "[object String]": "string",
    "[object Number]": "number",
    "[object Boolean]": "boolean",
    "[object Null]": "null",
    "[object Undefined]": "undefined",
    "[object Object]": "object",
    "[object Array]": "array",
    "[object Function]": "function",
    "[object Date]": "date", // Object.prototype.toString.call(new Date())
    "[object RegExp]": "regExp",
    "[object Map]": "map",
    "[object Set]": "set",
    "[object HTMLDivElement]": "dom", // document.querySelector('#app')
    "[object WeakMap]": "weakMap",
    "[object Window]": "window", // Object.prototype.toString.call(window)
    "[object Error]": "error", // new Error('1')
    "[object Arguments]": "arguments"
  };

  let name = Object.prototype.toString.call(data); //Borrow Object.prototype.toString() to get the data type
  let typeName = typeObj[name] || "Unknown type"; // matching data type
  return typeName === type; // Determine whether the data type is the incoming type
}

console.log(
  isType({}, "object"), //>> true
  isType([], "array"), //>> true
  isType(new Date(), "object"), //>> false
  isType(new Date(), "date") //>> true
);

Array-like object borrowing array method Because array-like objects are not real arrays, there are no methods that come with array types, so we need to borrow array methods. For example, the push method of borrowing an array:

//Array-like object
var arrayLike = {
0: "OB",
1: "Koro1",
length: 2
};
Array.prototype.push.call(arrayLike, "Add Array Item 1", "Add Array Item 2");
console.log(arrayLike);
//>> {"0":"OB","1":"Koro1","2":"Add array item 1","3":"Add array item 2","length":4}

1.3 apply to get the maximum and minimum values ​​of the array apply directly passes the array as the parameter of the method to be called, and also eliminates the step of expanding the array, such as using Math.max, Math.min to get the maximum/minimum value of the array.

const arr = [15, 6, 12, 13, 16];
const max = Math.max.apply(Math, arr); // 16
const min = Math.min.apply(Math, arr); // 6

inherit ES5 inheritance is also achieved by borrowing the construction method of the parent class to achieve the inheritance of the parent class method/attribute:

// father
function supFather(name) {
this.name = name;
this.colors = ['red','blue','green']; // complex type
}
supFather.prototype.sayName = function (age) {
console.log(this.name,'age');
};
// Subclass
function sub(name, age) {
// Borrow the method of the parent class: modify its this point, assign the methods and attributes in the parent class's constructor to the subclass
supFather.call(this, name);
this.age = age;
}
// Rewrite the prototype of the subclass, correct the constructor point
function inheritPrototype(sonFn, fatherFn) {
sonFn.prototype = Object.create(fatherFn.prototype); // inherit the properties and methods of the parent class
sonFn.prototype.constructor = sonFn; // Fix the constructor to point to the inherited function
}
inheritPrototype(sub, supFather);
sub.prototype.sayAge = function () {
console.log(this.age,'foo');
};
// Instantiate the subclass, you can find the properties and methods on the instance
const instance1 = new sub("OBKoro1", 24);
const instance2 = new sub("小明", 18);
instance1.colors.push('black')
console.log(instance1);
//>> {"name":"OBKoro1","colors":["red","blue","green","black"],"age":24}
console.log(instance2);
//>> {"name":"小明","colors":["red","blue","green"],"age":18}

There are many similar application scenarios, so I won't list them one by one. The key lies in the idea of ​​their borrowing method. If you don't understand it, you can read it several times.

Which one should be used for call and apply? The effect of call and apply is exactly the same, and the difference between them is also If the number of parameters/order is determined, use call, and if the number/order of parameters are uncertain, use apply. Consider readability: use call if the number of parameters is small, and if the number of parameters is large, integrate the parameters into an array and use apply. If the parameter set is already an array, use apply, such as the above to get the maximum/minimum value of the array. If the number/order of the parameters is uncertain, use apply, such as the following example:

const obj = {
age: 24,
name:'OBKoro1',
}
const obj2 = {
age: 777
}
callObj(obj, handle);
callObj(obj2, handle);
// Determine the number and order of parameters to be passed according to certain conditions
function callObj(thisAge, fn) {
let params = [];
if (thisAge.name) {
params.push(thisAge.name);
}
if (thisAge.age) {
params.push(thisAge.age);
}
fn.apply(thisAge, params); // The number and order are uncertain, call cannot be used
}
function handle(...params) {
console.log('params', params); // do some thing
}

Application scenarios of bind Save function parameters First look at the next classic interview question:

for (var i = 1; i <= 5; i++) {
   setTimeout(function test() {
        console.log(i) //>> 6 6 6 6 6
    }, i * 1000);
}

The reason for this phenomenon is that when setTimeout is executed asynchronously, i has become 6. So how to make him output: 1,2,3,4,5? It can be achieved cleverly through bind.

for (var i = 1; i <= 5; i++) {
    setTimeout(function (i) {
        console.log('bind', i) //>> 1 2 3 4 5
    }.bind(null, i), i * 1000);
}

In fact, closures are also used here. We know that bind will return a function, which is also a closure (the next article will introduce "closures" in depth). It saves the this point and the initial parameters of the function, and every change of i will be saved by the closure of bind, so it outputs 1-5. For specific details, there is a handwritten bind method below, which can be understood by reading it in detail. The callback function this is missing This is a common problem. The following is the real problem I encountered when developing the VSCode plug-in to handle webview communication. At first, I thought there was something wrong with the VSCode API. After debugging, I found that this point was missing.

class Page {
constructor(callBack) {
this.className ='Page';
this.MessageCallBack = callBack; //Callback function
this.MessageCallBack('Message sent to the registration page'); // Execute PageA's callback function
}
}
class PageA {
constructor() {
this.className ='PageA';
//The problem is the following sentence
this.pageClass = new Page(this.handleMessage);//Register page transfer callback function
}
// Callback for communication with the page
handleMessage(msg) {
console.log('Processing communication', this.className, msg); //'Page' this points to the error
}
}
new PageA();

Why is the callback function this lost? Obviously, there will be no problems when declaring, and there is no possibility of problems when executing the callback function. The problem is when passing the callback function: