3

This is the first article in a series of exploring the principles of JS native methods. This article will introduce how to implement call, apply and bind methods. Regarding the specific usage of these methods, MDN or articles on the site have been described very clearly, so I won't repeat them here.

Implement call by hand

ES3 version

Function.prototype.myCall = function(thisArg){
    if(typeof this != 'function'){
        throw new Error('The caller must be a function')
    }
     if(thisArg === undefined || thisArg === null){
        thisArg = globalThis
    } else {
        thisArg = Object(thisArg)
    }   
    var args = []
    for(var i = 1;i < arguments.length;i ++){
        args.push('arguments[' + i + ']')
    }
    thisArg.fn = this
    var res = eval('thisArg.fn(' + args + ')')
    delete thisArg.fn
    return res
}

ES6 version

Function.prototype.myCall = function(thisArg,...args){
    if(typeof this != 'function'){
        throw new Error('The caller must be a function')
    }
    if(thisArg === undefined || thisArg === null){
        thisArg = globalThis
    } else {
        thisArg = Object(thisArg)
    }
    thisArg.fn = this
    const res = thisArg.fn(...args)
    delete thisArg.fn
    return res
}

call calling a function through 060bec1234dd33, you can specify this in the function call And as long as the function is called through thisArg, this can be achieved, which is our main goal.

Implementation points

  1. The final is to call a function by myCall , so myCall and call as mounted on the function prototype. At the same time, it is precisely because myCall is called through a function, so in myCall we can get myCall through this, which is the function that is actually executed.
  2. It stands to reason that myCall is mounted on the function prototype. When we call myCall through a non-function, an error will definitely be thrown, so why check the caller's type myCall This is because when a caller obj = {} is an object, but inherits from Function ( obj.__proto__ = Function.prototype ), it can actually call the myCall method as a non-function. At this time, if type checking is not performed to ensure that it is a function, Then when it is directly used as a function call later, an error will be thrown
  3. If the thisArg passed to call is null or undefined, then thisArg will actually point to the global object; if thisArg is a basic type, then you can use Object() do a boxing operation and convert it into an object-mainly to ensure follow-up The function can be executed in the form of a method call. Can it be written as thisArg = thisArg ? Object(thisArg) : globalThis ? In fact, it is not possible. If thisArg is the boolean value false, then thisArg will eventually be equal to globalThis, but in fact it should be equal to Boolean {false} .
  4. As mentioned earlier, you can get the actually executed function through this myCall thisArg.fn = this equivalent to using this function as a method of thisArg, and we can call this function through the thisArg object later.
  5. thisArg.fn = this equivalent to adding a fn attribute to thisArg, so this attribute must be deleted before returning the execution result. In addition, in order to avoid overwriting the attribute fn with the same name that may exist on thisArg, you can also use const fn = Symbol('fn') construct a unique attribute, and then thisArg[fn] = this .
  6. The main difference between the ES3 version and the ES6 version lies in the transfer of parameters and the execution of functions:

    1. ES6 introduces the remaining parameters, so no matter how many parameters are passed in when the function is actually executed, these parameters can be obtained through the args array. At the same time, because the expansion operator is introduced, the args parameter array can be expanded, and the parameters can be expanded one by one. Pass to function execution
    2. But there is no remaining parameter in ES3, so when defining myCall , only one thisArg parameter is received, and then all the parameters are obtained through the arguments class array in the function body. What we need is all the elements except the first element (thisArg) in arguments, how to do it? If it is ES6, [...arguments].slice(1) is fine, but this is ES3, so we can only traverse the arguments from index 1, and then push to an args array. And it should be noted that here push enters the parameters in the form of strings, which is mainly for the convenience of passing the parameters to the function one by one when the function is executed through eval later.
    3. Why must eval be used to execute a function? Because we don't know how many parameters the function actually receives, and we don't need the expansion operator, we can only construct an executable string expression and explicitly pass in all the parameters of the function.

Implement apply by hand

The usage of apply is very similar to call, so the implementation is also very similar. The difference that needs to be noted is that call can also receive multiple parameters after receiving a thisArg parameter (that is, it accepts a parameter list), and apply after receiving a thisArg parameter, usually the second parameter is an array or an array-like object:

fn.call(thisArg,arg1,arg2,...)
fn.apply(thisArg,[arg1,arg2,...])        

If the second parameter is passed null or undefined, it is equivalent to passing only the thisArg parameter as a whole.

ES3 version

Function.prototype.myApply = function(thisArg,args){
    if(typeof this != 'function'){
        throw new Error('the caller must be a function')
    } 
    if(thisArg === null || thisArg === undefined){
        thisArg = globalThis
    } else {
        thisArg = Object(thisArg)
    }
    if(args === null || args === undefined){
        args = []
    } else if(!Array.isArray(args)){
        throw new Error('CreateListFromArrayLike called on non-object')
    }
    var _args = []
    for(var i = 0;i < args.length;i ++){
        _args.push('args[' + i + ']')
    }
    thisArg.fn = this
    var res = _args.length ? eval('thisArg.fn(' + _args + ')'):thisArg.fn()
    delete thisArg.fn
    return res
}

ES6 version

Function.prototype.myApply = function(thisArg,args){
    if(typeof thisArg != 'function'){
        throw new Error('the caller must be a function')
    } 
    if(thisArg === null || thisArg === undefined){
        thisArg = globalThis
    } else {
        thisArg = Object(thisArg)
    }
    if(args === null || args === undefined){
        args = []
    } 
    // 如果传入的不是数组,仿照 apply 抛出错误
    else if(!Array.isArray(args)){
        throw new Error('CreateListFromArrayLike called on non-object')
    }
    thisArg.fn = this
    const res = thisArg.fn(...args)
    delete thisArg.fn
    return res
}

Implementation points

Basically the implementation of call is similar, but we need to check the type of the second parameter.

Implement bind by hand

bind can also call and apply , but there are some different points to note:

  • bind does not call the original function directly after specifying this, but returns a new function that is bound to this internally based on the original function
  • The parameters of the original function can be passed in batches. The first batch can be passed in bind , and the second batch can be passed in when calling the new function. The two batches of parameters will eventually be merged together. Pass to the new function once to execute
  • If the new function is called through the new method, the this inside the function will point to the instance instead of the bind that was passed in when 060bec1234e4b4 was originally called. In other words, bind this case is equivalent to invalid

ES3 version

This version is closer to the polyfill version on MDN.

Function.prototype.myBind = function(thisArg){
    if(typeof this != 'function'){
        throw new Error('the caller must be a function')
    }
    var fnToBind = this
    var args1 = Array.prototype.slice.call(arguments,1)
    var fnBound = function(){
        // 如果是通过 new 调用
        return fnToBind.apply(this instanceof fnBound ? this:thisArg,args1.concat(args2))     
    }
    // 实例继承
    var Fn = function(){}
    Fn.prototype = this.prototype
    fnBound.prototype = new Fn()
    return fnBound
}

ES6 version

Function.prototype.myBind = function(thisArg,...args1){
    if(typeof this != 'function'){
        throw new Error('the caller must be a function')
    }
    const fnToBind = this
    return function fnBound(...args2){
        // 如果是通过 new 调用的
        if(this instanceof fnBound){
            return new fnToBind(...args1,...args2)
        } else {
            return fnToBind.apply(thisArg,[...args1,...args2])
        }
    }
}

Implementation points

1. bind realizes the internal this binding, which requires the help of apply . Here we assume that we can directly use the apply method

2.Look at the simpler ES6 version first:

1). parameter acquisition : Because ES6 can use the remaining parameters, it is easy to obtain the parameters needed to execute the original function, and the expansion operator can also be used to easily merge the arrays.

2). calling method : As mentioned earlier, if the returned new function fnBound is called by new, then its internal this will be an instance of the fnBound constructor, instead of the thisArg we specified at the beginning, so this instanceof fnBound will return true In this case, it is equivalent to the thisArg that we specify is invalid, and the new function returned by new is equivalent to the old function of new, that is, new fnBound is equivalent to new fnToBind, so we can return a new fnToBind; otherwise, If fnBound is a normal call, the binding of thisArg is completed by apply, and then the final result is returned. It can be seen from this that the this binding of bind is essentially done through apply.

3. Let's look at the more troublesome ES3 version:

1). parameter acquisition : Now we can't use the remaining parameters, so we can only get all the parameters through arguments inside the function body. For myBind , what we actually need is an array of all the remaining parameters except the first thisArg parameter passed in, so here we can Array.prototype.slice.call (arguments is an array-like, and slice cannot be called directly), The borrowing here has two purposes: one is to remove the first parameter in arguments, and the other is to convert the arguments after removing the first parameter into an array (the return value of slice itself is an array, which is also the conversion of a class array to an array A commonly used method). Similarly, the returned new function fnBound may also pass in parameters when it is called later, and again use slice to convert the arguments into an array

2). calling method : Similarly, here we have to judge whether fnBound is a new call or a normal call. In the implementation of the ES6 version, if new calls fnBound, it will directly return new fnToBind() . This is actually the simplest and easiest way to understand. When we access the instance properties, we naturally follow the instance => instance.__proto__ = The prototype chain like fnToBind.prototype can ensure that the instance successfully accesses the properties on the prototype of its constructor fnToBInd; but in the implementation of ES3 (or in the implementation of some bind methods on the Internet), our approach is to return a fnToBind.apply(this) actually equivalent to returning an undefined function execution result. According to the principle of new, we did not customize a return object in the constructor, so the result of new is to return the instance itself, which is not affected. The problem with this return statement is that it only ensures that this in fnToBind points to the instance returned after new fnBound, but does not ensure that this instance can access the properties on the prototype of fnToBind. In fact, it cannot be accessed because its constructor is fnBound instead of fnToBind, so we have to find a way to establish a prototype chain relationship between fnBound and fnToBind. There are several methods we might use:

 // 这里的 this 指的是 fnToBind
 fnBound.prototype = this.prototype

This is just a copy of the prototype reference, if you modify fnBound.prototype , it will affect fnToBind.prototype , so this method cannot be used

// this 指的是 fnToBind
fnBound.prototype = Object.create(this.prototype)

Through Object.create you can create an __proto__ pointing to this.prototype , and then let fnBound.prototype point to this object, you can establish a prototype relationship between fnToBind and fnBound. But because Object.create is an ES6 method, it cannot be used in our ES3 code.

// this 指的是 fnToBind
const Fn = function(){}
Fn.prototype = this.prototype
fnBound.prototype = new Fn()

This is the method used by the above code: a connection is established between fnToBind and fnBound through the empty constructor Fn. If you want to access the properties on the prototype of fnToBind through an example, you can search along the prototype chain as follows:

instance => instance.__proto__ = fnBound.prototype = new Fn() => new Fn().__proto__ = Fn.prototype = fnToBind.prototype


Chor
2k 声望5.9k 粉丝

引用和评论

0 条评论