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 |
- 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
- The only difference between apply and call is the parameter form
- 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:
- 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)
- This property will always exist on the target object f
Solution:
- 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.
- 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
-
Note that the parameter form of apply is array, so we passed in args instead of... Args
-
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.
-
If we execute f directly, this in the binding function points to the global object.
-
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
- 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).
- 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]