Interviewer: can you write call, apply and bind by hand?

Keywords: Javascript Front-end

1. Usage and comparison of call, apply and bind

1.1 Function.prototype

All three are methods on the Function prototype, and all functions can call them

Function.prototype.call
Function.prototype.apply
Function.prototype.bind

1.2 syntax

fn stands for a function

fn.call(thisArg, arg1, arg2, ...) // Receive parameter list
fn.apply(thisArg, argsArray) // apply receive array parameters
fn.bind(thisArg, arg1, arg2, ...) // Receive parameter list

1.3 parameter description

thisArg: this value used at fn runtime

arg1,arg2,...: parameter list, passed to fn for use

argsArray: array or array like object (such as Arguments object) passed to fn for use

1.4 return value

call, apply: the same as the return value after fn execution

bind: returns a copy of the original function with the specified this value and initial parameters. And the returned function can pass parameters.

const f = fn.bind(obj, arg1, arg2, ...)
f(a, b, c, ...)
// Calling f is equivalent to calling fn.call (obj,... Args)
// args is the parameter passed in by calling bind plus the parameter list passed in by calling f
// arg1,arg2...a,b,c

1.5 function

The functions of the three methods are the same: changing the value of this when the function is running can realize the reuse of the function

1.6 usage examples

function fn(a, b) {
    console.log(this.myName);
}

const obj = {
    myName: 'Melon'
}

fn(1, 2) 
// Output: undefined 
// Because this points to the global object, there is no myName attribute on the global object

fn.call(obj, 1, 2) 
fn.apply(obj, [1, 2])
// Output: melon
// At this point, this points to obj, so you can read the myName attribute

const fn1 = fn.bind(obj, 1, 2)
fn1()
// Output: melon
// At this point, this points to obj, so you can read the myName attribute

1.7 comparison of three methods

method function parameter Execute now
apply Change the value of this when the function runs array yes
call Change the value of this when the function runs parameter list yes
bind Change the value of this when the function runs parameter list No. Returns a function
  1. apply and call will get the execution result immediately, while bind will return a function with this and parameters specified. You need to call this function manually to get the execution result
  2. The only difference between apply and call is the parameter form
  3. Only the parameter of apply is array, and the memory method: both apply and array start with a

2. Implement call, apply and bind

2.1 call implementation

2.1.1 easily confused variable direction

Now let's implement the call method, named myCall

We mount it on the Function prototype so that all functions can call this method

// We use the remaining parameters to receive the parameter list
Function.prototype.myCall = function (thisArg, ...args) {
  console.log(this)
  console.log(thisArg)
}

The first thing to understand is what this and thisArg in this function point to respectively

See how we call it:

fn.myCall(obj, arg1, arg2, ...)

Therefore, this in myCall points to fn and thisArg points to obj (target object)

Our goal is to make this in fn runtime (note that this is in fn) point to thisArg, the target object

In other words, let fn become the method of obj to run (the core idea)

2.1.2 simple call

We can write a simple version of myCall according to the above core ideas

Function.prototype.myCall = function (thisArg, ...args) {
  // Add a new method to thisArg
  thisArg.f = this; // this is fn
  // Run this method and pass in the remaining parameters
  let result = thisArg.f(...args);
  // Because the return value of the call method is the same as fn
  return result;
};

The basic functions of the call method are completed, but there are obvious problems:

  1. If multiple functions call this method at the same time and the target object is the same, the f attribute of the target object may be overwritten
fn1.myCall(obj)
fn2.myCall(obj)
  1. This property will always exist on the target object f

Solution:

  1. ES6 introduces a new primitive data type Symbol, which represents a unique value. The biggest usage is to define the unique attribute name of the object.
  2. The delete operator is used to delete a property of an object

2.1.3 call after optimizing obvious problems

Optimized myCall:

Function.prototype.myCall = function (thisArg, ...args) {
  // Generate unique attribute names to solve the problem of overwriting
  const prop = Symbol()
  // Be careful not to use it here
  thisArg[prop] = this; 
  // Run this method, pass in the remaining parameters, and you can't use it either
  let result = thisArg[prop](...args);
  // Delete attribute after running
  delete thisArg[prop]
  // Because the return value of the call method is the same as fn
  return result;
};

So far, the function of myCall method is relatively complete, but there are still some details to be added

2.1.4 call after supplementary details

If the thisArg (target object) we passed in is undefined or null, we will replace it with pointing to the global object (as described in the MDN document)

// Complete code
Function.prototype.myCall = function (thisArg, ...args) {
  // Replace with a global object: global or window
  thisArg = thisArg || global
  const prop = Symbol();
  thisArg[prop] = this;
  let result = thisArg[prop](...args);
  delete thisArg[prop];
  return result;
};

2.2 implementation of apply

The implementation ideas of apply and call are the same, but the parameter transfer forms are different

// Change the remaining parameters to receive an array
Function.prototype.myApply = function (thisArg, args) {
  thisArg = thisArg || global
  // Judge whether the parameter is received. If the parameter is not received, replace it with []
  args = args || []
  const prop = Symbol();
  thisArg[prop] = this;
  // Expand the incoming with the... Operator
  let result = thisArg[prop](...args);
  delete thisArg[prop];
  return result;
};

2.3 implementation of bind

2.3.1 simple bind

Implementation idea: bind will create a new binding function, which wraps the original function object. Calling the binding function will execute the wrapped function

call and apply have been implemented earlier. We can choose one of them to bind this, and then encapsulate a layer of functions to get a simple version of the method:

Function.prototype.myBind = function(thisArg, ...args) {
  // this points to fn
  const self = this
  // Return binding function
  return function() {
    // Wrapped the original function object
    return self.apply(thisArg, args)
  }
}

2.3.2 precautions

  1. Note that the parameter form of apply is array, so we passed in args instead of... Args

  2. Why define self before return to save this?

    Because we need to use closures to save this (that is, fn), so that the function returned by the myBind method can correctly point to fn at runtime

    The specific explanations are as follows:

// If self is not defined
Function.prototype.myBind = function(thisArg, ...args) {
  return function() {
    return this.apply(thisArg, args)
  }
}
const f = fn.myBind(obj) // Returns a function
// In order to see clearly, write it in the form below
// thisArg and args are stored in memory because closures are formed
const f = function() {
  return this.apply(thisArg, args)
}
// Now we call f
// You will find that its this points to the global object (window/global)
// Not what we expected fn
f()

2.3.3 let the function (binding function) returned by bind pass parameters

As mentioned earlier, the parameters returned by bind can be transferred (see 1.4). Now let's improve myBind:

Function.prototype.myBind = function(thisArg, ...args) {
  const self = this
  // Return the binding function and receive the parameters with the remaining parameters
  return function(...innerArgs) {
    // Merge two passed in parameters
    const finalArgs = [...args, ...innerArgs]
    return self.apply(thisArg, finalArgs)
  }
}

2.3.4 what are the problems with "new + binding function"

MDN: binding functions can also be used new Operator construction, which will appear as if the objective function has been constructed. The supplied value of this will be ignored, but the pre parameter will still be supplied to the analog function.

This is the description in the MDN document, which means that the binding function can be used as a constructor to create an instance, and the target object thisArg previously passed in as the first parameter of the bind method is invalid, but the previously provided parameters are still valid.

Let's start with myBind

Internal of binding function:

// Binding function f
function(...innerArgs) {
  ...
  // In order to see clearly, self is directly written as fn here
  return fn.apply(thisArg, finalArgs)
}

Create an instance of f with new:

const o = new f()

We all know (if not, read this: Front end interview handwritten code -- simulating the implementation of new operator ), the constructor code will be executed in the process of new, that is, the code in the binding function f will be executed here.

The code FN. Apply (thisArg, final args) is included, and the thisArg is still valid, which is inconsistent with the description of the native bind method

2.3.5 how to distinguish whether new is used in the binding function

How to solve this problem: when creating an instance of a binding function with new, invalidate the previously passed in thisArg

In fact, for binding function f, the value of this at execution time is uncertain.

  1. If we execute f directly, this in the binding function points to the global object.

  2. If we use new to create an instance of f, then this in f points to the newly created instance. (if this point is not clear, read this article: Front end interview handwritten code -- simulating the implementation of new operator)

Function.prototype.myBind = function(thisArg, ...args) {
  const self = this
  return function(...innerArgs) {
    console.log(this) // Note that this here is not certain
    const finalArgs = [...args, ...innerArgs]
    return self.apply(thisArg, finalArgs)
  }
}
// Binding function
const f = fn.myBind(obj)
// If we execute f directly, this in the binding function points to the global object
f()
// If we use new to create an instance of f, then this in f points to the newly created instance
const o = new f()

Based on the above two cases, we can modify the binding function returned by myBind to judge the value of this in the function, so as to distinguish whether the new operator is used

Make the following changes to myBind:

Function.prototype.myBind = function(thisArg, ...args) {
  const self = this
  const bound = function(...innerArgs) {
    const finalArgs = [...args, ...innerArgs]
    const isNew = this instanceof bound // To determine whether new is used
    if (isNew) {
      
    } 
    // If new is not used, it will be returned as before
    return self.apply(thisArg, finalArgs)
  }
  return bound
}

2.3.6 supplement internal operation of binding function

Now we need to know what operations should be performed if the instance is constructed by new.

See what happens when you use the native bind method:

const fn = function(a, b) {
  this.a = a
  this.b = b
}
const targetObj = {
  name: 'Melon'
}
// Binding function
const bound = fn.bind(targetObj, 1)
const o = new bound(2)
console.log(o); // fn { a: 1, b: 2 }
console.log(o.constructor); // [Function: fn]
console.log(o.__proto__ === fn.prototype); // true

As you can see, new bound() returns an instance created with fn as the constructor.

According to this point, the code in if (new) {} can be supplemented:

Function.prototype.myBind = function(thisArg, ...args) {
  const self = this
  const bound = function(...innerArgs) {
    const finalArgs = [...args, ...innerArgs]
    const isNew = this instanceof bound // To determine whether new is used
    if (isNew) {
      // Create an instance of fn directly
      return new self(...finalArgs)
    } 
    // If new is not used, it will be returned as before
    return self.apply(thisArg, finalArgs)
  }
  return bound
}
const bound = fn.myBind(targetObj, 1)
const o = new bound(2)

In this way, const o = new bound(2) is equivalent to const o = new self(...finalArgs), because if the constructor explicitly returns an object, it will directly overwrite the object created in the new process (if you don't know, you can see this article: Front end interview handwritten code -- simulating the implementation of new operator)

2.3.7 complete code

Function.prototype.myBind = function(thisArg, ...args) {
  const self = this
  const bound = function(...innerArgs) {
    const finalArgs = [...args, ...innerArgs]
    const isNew = this instanceof bound
    if (isNew) {
      return new self(...finalArgs)
    } 
    return self.apply(thisArg, finalArgs)
  }
  return bound
}

In fact, there are still some differences between this code and the native bind, but this is only an overall idea of implementing bind, and there is no need to be strict and consistent

3 supplement

  1. There are still some details about the apply and call methods that we haven't implemented: if this function (fn) is in Non strict mode If it is specified as null or undefined, it will be automatically replaced by pointing to the global object, and the original value will be wrapped (for example, 1 will be wrapped into an object by the wrapping class Number).
  2. The bind method is also an application of function coritization. Those who are not familiar with coritization can take a look at this article : handwritten code of front-end interview -- JS function

Official account [front end]

Posted by John Cartwright on Tue, 02 Nov 2021 21:20:45 -0700