Deep understanding of call, apply, bind

Posted May 27, 202012 min read

call, apply, bind are all related to this, these three methods are the prototype methods of JavaScript built-in object Function.

basic introduction

1 . Syntax

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.

Parameters:

thisArg(optional):

  1. this of func points to thisArg object;
  2. In non-strict mode:If thisArg is specified as null, undefined, then this of func points to the window object;
  3. In strict mode:this of func is undefined;
  4. The value of the original value(number, string, Boolean value) this will point to the automatic packaging object of the original value, such as String, Number, Boolean.

param1, param2(optional):parameters passed to func.

  1. If param is not passed or is null/undefined, it means that no parameters need to be passed in.
  2. The second parameter of apply is an array-like object. The value of each item in the array is the parameter passed to func.

2 . Must be a function to call call/apply/bind

call, apply, and bind are the three methods attached to the Function object, and only functions have these methods. You can call them as long as they are functions, for example:Object.prototype.toString is a function, we often see this usage:Object.prototype.toString.call(data).

3 . Function

Changing the this point when the function is executed, all the current use of them is based on this point.

4 . How not to confuse call and apply

There are not many people who confuse these two methods. Don't underestimate this problem. Just remember the following method. Double a memory method:apply starts with a, and the parameter it passes to func is an Array-like object(array-like object), which also starts with a.

The only difference between call and apply

The parameters passed to func are written differently:

  • apply is the second parameter. This parameter is an array-like object:the parameters passed to func are written in the array.
  • Call parameters from 2nd to n are all passed to func.

The difference between call/apply and bind

carried out:

  • call/apply changed the function's this and immediately execute the function;
  • bind returns the function after changing the this pointer, do not execute the function.

return value:

  • call/apply returns the execution result of func;
  • bind returns a copy of func and specifies this of func to save the func parameter.

The return value section has a detailed example analysis in the bind application below.

What is an array-like?

Let's talk about arrays first, which we are all familiar with. Its characteristics are:it can be called by index, such as array \ [0 ]; it has length attribute length; it can be traversed by for loop or forEach method.

So, what is an array-like? As the name suggests, it is have objects similar to the characteristics of the array. For example, the following object is an array-like array.

let arrayLike = {
    0:1,
    1:2,
    twenty three,
    length:3
};

The array-like arrayLike can be called through the corner label, has a length property, and can also be traversed through the for loop.

Arrays are still commonly used, but we may not notice them. For example, our method for obtaining DOM nodes returns an array-like array; for another example, all parameters obtained by using arguments in a function body are also an array-like array.

However, it should be noted that ** array-like methods cannot use the methods on the array prototype chain such as forEach, splice, push, etc., after all, it is not a real array.

The core idea of call/apply/bind:borrowing method

What is the borrowing method, let us make an analogy.

in life:

I usually don't have time to cook, and I want to cook a beef hot pot for my family on the weekend. But there is no suitable pot, and I do n t want to go out to buy it, so I borrowed a pot from my neighbor to use it, which not only achieves the purpose, but also saves money, and serves two purposes.

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 A objects is more convenient, which not only achieves the purpose, but also saves memory.

This is the core idea of call/apply/bind:borrowing method. With the help of the implemented method, change the this point of the data in the method to reduce the repetitive code and save memory.

Remember the class array just now? If it wants to use the slice method on the Array prototype chain, it can do this:

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

By analogy, domNodes can apply other methods under Array.

Application scenario

These application scenarios, you can find that their philosophy is:borrowing methods.

1 . Determine the data type

Object.prototype.toString is suitable for judging the type, but with it we can 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"; //Match 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

);

2 . Array-like objects to borrow arrays

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, borrow the push method of the array:

//Array-like objects
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}

3 . Apply to get the maximum and minimum value of the array

apply directly passes the array as the parameter of the method to be called, and saves one step to expand the array, such as using Math.max and Math.min to obtain 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

4 . Inheritance

ES5 inheritance is also to inherit the parent class methods/properties by borrowing the parent class constructor:

//father
function supFather(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green']; //complex types

}

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 to assign the method and attribute in the parent class constructor to the subclass
    supFather.call(this, name);
    this.age = age;
}

//Rewrite the prototype of the subclass and fix the constructor pointing
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 attributes 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 will not list them one by one. The key lies in their idea of borrowing methods, if you don't understand it, you can read it several times.

5 . Which call or apply should I use?

The effect of call and apply is exactly the same, the difference is that

  • Call is used to determine the number/order of parameters, and apply if the number/order of parameters is uncertain.
  • Consider readability:use call if the number of parameters is small. 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, for example to obtain the maximum/minimum value of the array.

Use apply if the number/order of parameters is uncertain, such as the following example:

const obj = {
    age:24,
    name:'OBKoro1',
}

const obj2 = {
    age:777
}

callObj(obj, handle);
callObj(obj2, handle);

//Decide 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
}

6 . Bind application scenarios

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? Can be cleverly achieved through bind.

for(var i = 1; i <= 5; i ++) {
    //Cache parameters
    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, this function is also a closure

It saves the function's this pointer, initial parameters, and every change of i will be saved by the bind closure, so output 1-5. For specific details, there is a handwritten bind method below, which can be understood by reading in detail.

Missing callback function this

This is a common problem. After debugging it, I found that this is the missing problem.

class Page {
    constructor(callBack) {
        this.className = 'Page';
        this.MessageCallBack = callBack; //Callback function
        this.MessageCallBack('message sent to the registration page'); //execute the callback function of PageA
    }
}

class PageA {
    constructor() {
        this.className = 'PageA';
        //The problem is in the following sentence
        this.pageClass = new Page(this.handleMessage); //Registration page transfer callback function
    }

    //Callback to communicate with the page
    handleMessage(msg) {
        console.log('Handle communication', this.className, msg); //'Page' this points to an error
    }
}

new PageA();

Why is the callback function this lost? Obviously, there will be no problem when it is declared, and there will be no problem when the callback function is executed. The problem is when passing the callback function:

this.pageClass = new Page(this.handleMessage);

Because the passed this.handleMessage is the function memory address, there is no context object attached, which means that the function this.handleMessage is not bound to its this point.

Now that we know the problem, we can solve the problem as long as the this of the bound callback function points to PageA.

The missing solution for the callback function this

(1) This of the bind binding callback function points to:

This is a typical bind application scenario, bind this to use as a callback function.

this.pageClass = new Page(this.handleMessage.bind(this));
//This point of the callback function is bound

(2) Use the arrow function to bind this to point to:

The this of the arrow function points to the this of the first ordinary function of the outer layer when it is defined, in this case, the class class:PageA

this.pageClass = new Page(() => this.handleMessage());
//The arrow function binding this points

Implement call/apply and bind with native JavaScript

Handwriting to implement call, apply, bind(especially bind) has always been a relatively high-frequency interview question, here we also implement these functions together.

1 . Implement call

Realization Ideas

  1. Refer to the syntax rules of call, you need to set a parameter thisArg, which is the pointing of this

  2. Encapsulate thisArg as an Object

  3. By creating a temporary method for thisArg, so that thisArg is the object that called the temporary method, it will implicitly point this of the temporary method to thisArg

  4. Execute the temporary method of thisArg and pass parameters

  5. Delete the temporary method and return the execution result of the method

    /**

    • Implement call with native JavaScript

    • /
      Function.prototype.myCall = function(thisArg, ... arr) {

      //1. Judge the legality of parameters //////////////////////////
      if(thisArg === null || thisArg === undefined) {
      //The value of this specified as null and undefined will automatically point to the global object(window in browser)
      thisArg = window;
      } else {
      thisArg = Object(thisArg); //Create an object that can contain numbers/strings/booleans,

                             //thisArg will point to an object containing the original value.

      }

      //2. Get this point ///////////////////////////
      const specialMethod = Symbol("anything"); //Create a non-repeating constant
      //If the name of the function calling myCall is func, it is called in the form of func.myCall();
      //According to the introduction in the last article, this in the myCall function points to func
      thisArg [specialMethod]= this; //Create a temporary property for thisArg object to store this(that is, func function)
      //Further, according to the previous article, func is called as a method of thisArg object, then this in func
      //Point to thisArg object. Thus, it is cleverly done to implicitly point this to thisArg!
      let result = thisArg [specialMethod](... arr);

      //3. Closed
      delete thisArg [specialMethod]; //Delete temporary method
      return result; //Return the execution result of the temporary method
      };

      let obj = {
      name:"coffe1891"
      };

      function func() {
      console.log(this.name);
      }

      func.myCall(obj); //>> coffe1891

If you read the notes carefully, you can basically understand it. The main rule is to judge the rules pointed by this.

Talk more about how to judge thisArg correctly:

The above code judges that the writing of thisArg is an adjusted rigorous writing, because many front-end engineers have found that the parameter thisArg is simply judged by whether it is false, such as:

//Not rigorous enough
thisArg = thisArg? Object(thisArg):window;
thisArg = thisArg || window;

After testing, the following three cases are false, thisArg will be accidentally bound to the window:

//The online solution:thisArg = thisArg || window;
//This scheme is not rigorous, code example:
//The parameter clearly passed the value, the intention was not to make thisArg point to the window, but the result was accidentally pointed to the window
handle.elseCall(''); //window
handle.elseCall(0); //window
handle.elseCall(false); //window

Therefore, you should judge carefully, as follows:

//strict judgment
if(thisArg === null || thisArg === undefined) {
  //The value of this specified as null and undefined will automatically point to the global object(window in browser)
  thisArg = window;
} else {
  thisArg = Object(thisArg);
  //Create an object containing the original value(number, string, boolean),
  //thisArg will point to an object containing the original value.
}

2 . Implement Apply

Implementation ideas

  1. The parameter processing passed to the function is not the same, the other parts are the same as the call;

  2. apply accepts the second parameter as an array-like object

    /**

    • Apply with native JavaScript

    • /
      Function.prototype.myApply = function(thisArg) {
      if(thisArg === null || thisArg === undefined) {
      thisArg = window;
      } else {
      thisArg = Object(thisArg);
      }

      //Determine whether it is [array-like object]
      function isArrayLike(o) {
      if(
      o && //o is not null, undefined, etc.
      typeof o === "object" && //o is an object
      isFinite(o.length) && //o.length is a finite value
      o.length> = 0 && //o.length is a non-negative value
      o.length === Math.floor(o.length) && //o.length is an integer
      o.length <4294967296
      )
      //o.length <2 ^ 32
      return true;
      else return false;
      }

      const specialMethod = Symbol("anything");
      thisArg [specialMethod]= this;

      let args = arguments [1]; //Get the parameter array
      let result;

      //Handle the second parameter passed in
      if(args) {
      //Whether to pass the second parameter
      if(! Array.isArray(args) &&! isArrayLike(args)) {
      throw new TypeError(

       "The second parameter is neither an array nor an array-like object. Error thrown"

      );
      } else {
      args = Array.from(args); //convert to array
      result = thisArg [specialMethod](... args); //execute the function and expand the array, passing the function parameters
      }
      } else {
      result = thisArg specialMethod ;
      }

      delete thisArg [specialMethod];
      return result; //return function execution result
      };

3 . Implement bind

Implementation ideas

(1) Copy calling function:

  • Call the function, that is, call the function of myBind, temporarily store it with a variable;
  • Use Object.create to copy the prototype of the calling function to funcForBind;

(2) Return the copied function funcForBind;

(3) Call the copied function funcForBind:

  • new call judgment:use instanceof to determine whether the function is called via new to determine the bound context;
  • Bind this and pass parameters through call;
  • Return the execution result of the calling function.

Here is an implementation based on ES6(there are many implementations based on the old ES, but there is no simple implementation of the spread and rest operators of ES6):

/**
* Implement bind with native JavaScript
* /
Function.prototype.myBind = function(objThis, ... params) {
const thisFn = this; //Store the calling function, and the above params(function parameters)
//Parameters are passed twice for the returned function secondParams
let funcForBind = function(... secondParams) {
//Check if this is an instance of funcForBind? That is to check whether funcForBind is called through new
const isNew = this instanceof funcForBind;

    //The new call is bound to this, otherwise it is bound to the incoming objThis
    const thisArg = isNew? this:Object(objThis);

    //Use call to execute the calling function, bind the pointer of this, and pass the parameters. Return the execution result
    return thisFn.call(thisArg, ... params, ... secondParams);
  };

  //Copy the prototype of the calling function to funcForBind
  funcForBind.prototype = Object.create(thisFn.prototype);
  return funcForBind; //Return copy function
};

Line 7 of the above code may be difficult to understand. What does second params mean? for example:

let func = function(p, secondParams) {//In fact, the func used in the test can have any number of parameters
    console.log(p.name);
    console.log(this.name);
    console.log(secondParams);
}
let obj = {
    name:"1891"
}
func.myBind(obj, {name:"coffe"})("Secondary parameter");
//>> coffe
//>> 1891
//>> Secondary parameter transmission

After calling myBind on line 8 above, a new function is returned, and then the new function is passed parameters, which is the second pass.