Home > Article > Web Front-end > This article will give you an in-depth understanding of the implementation of call, apply and bind methods
This article uses code examples to give you an in-depth analysis of how to implement call, apply and bind. As for the specific usage of these methods, MDN or the articles on the site have already described them clearly, so I will not go into details here.
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 }
When calling a function through call
, you can specify this in the function by passing thisArg to call
. And this can be achieved as long as the function is called through thisArg, which is our main goal.
Implementation points
In the end, myCall
is called through a function, so myCall
is mounted on the function prototype like call
. At the same time, it is precisely because myCall
is called through a function, so inside myCall
we can get the caller of myCall
through this, which is the actual execution that function.
It stands to reason that myCall
is mounted on the function prototype. When we call myCall
through a non-function, it will definitely Throws an error, so why check the caller's type in myCall
and customize an error? This is because when a caller obj = {}
is an object but inherits from Function
(obj.__proto__ = Function.prototype
), As a non-function, it can actually call the myCall
method. At this time, if there is no type check to ensure that it is a function, then when it is called directly as a function later, an error will be thrown.
If thisArg passed to call
is null or undefined, then thisArg will actually point to the global object; if thisArg is a basic type, you can use Object()
Perform a boxing operation and convert it into an object - mainly to ensure that the function can be executed later by method calling. So can it be written as thisArg = thisArg ? Object(thisArg) : globalThis
? In fact, it is not possible. If thisArg is a Boolean value false, it will cause thisArg to eventually be equal to globalThis, but in fact it should be equal to Boolean {false}
.
As mentioned before, you can get the actually executed function through this in myCall
, so thisArg.fn = this
is equivalent to Use this function as a method of thisArg, and then we can call this function through the thisArg object.
thisArg.fn = this
is equivalent to adding an fn attribute to thisArg, so this attribute must be deleted before returning the execution result. In addition, in order to avoid overwriting the property fn with the same name that may exist on thisArg, you can also use const fn = Symbol('fn')
to construct a unique property, and then thisArg[fn] = this
.
The main difference between the ES3 version and the ES6 version lies in the passing of parameters and the execution of functions:
ES6 introduces 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 of the introduction of the expansion operator, the args parameter array can be expanded and the parameters are passed to the function one by one for execution
But there is no such thing as remaining parameters in ES3, so when defining myCall
, it only receives one thisArg parameter, and then gets all the parameters through the arguments class array in the function body. What we need is all the elements in arguments except the first element (thisArg). How to do this? If it is ES6, just [...arguments].slice(1)
, but this is ES3, so we can only traverse the arguments starting from index 1, and then push them into an args array. . Also note that what is pushed here are parameters in the form of strings. This is mainly to facilitate passing the parameters to the function one by one when executing the function through eval later.
Why must eval be used to execute a function? Because we don't know how many parameters the function actually needs to receive, and we can't use the expansion operator, we can only construct an executable string expression and explicitly pass in all the parameters of the function.
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 after call accepts a thisArg parameter, it can also receive multiple parameters (that is, it accepts a parameter list), and after apply receives a thisArg parameter, usually the second parameter is an array or array-like object:
fn.call(thisArg,arg1,arg2,...) fn.apply(thisArg,[arg1,arg2,...])
如果第二个参数传的是 null 或者 undefined,那么相当于是整体只传了 thisArg 参数。
ES3 版本
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 版本
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 }
实现要点
基本上和 call 的实现是差不多的,只是我们需要检查第二个参数的类型。
bind
也可以像 call
和 apply
那样给函数绑定一个 this,但是有一些不同的要点需要注意:
bind
不是指定完 this 之后直接调用原函数,而是基于原函数返回一个内部完成了 this 绑定的新函数bind
的时候作为第二个参数传入,第二批可以在调用新函数的时候传入,这两批参数最终会合并在一起,一次传递给新函数去执行bind
的时候传入的 thisArg。换句话说,这种情况下的 bind
相当于是无效的ES3 版本
这个版本更接近 MDN 上的 polyfill 版本。
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 版本
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]) } } }
实现要点
1.bind
实现内部 this 绑定,需要借助于 apply
,这里假设我们可以直接使用 apply
方法
2.先看比较简单的 ES6 版本:
1). 参数获取:因为 ES6 可以使用剩余参数,所以很容易就可以获取执行原函数所需要的参数,而且也可以用展开运算符轻松合并数组。
2). 调用方式:前面说过,如果返回的新函数 fnBound 是通过 new 调用的,那么其内部的 this 会是 fnBound 构造函数的实例,而不是当初我们指定的 thisArg,因此 this instanceof fnBound
会返回 true,这种情况下,相当于我们指定的 thisArg 是无效的,new 返回的新函数等价于 new 原来的旧函数,即 new fnBound 等价于 new fnToBind,所以我们返回一个 new fnToBind 即可;反之,如果 fnBound 是普通调用,则通过 apply 完成 thisArg 的绑定,再返回最终结果。从这里可以看出,bind 的 this 绑定,本质上是通过 apply 完成的。
3.再来看比较麻烦一点的 ES3 版本:
1). 参数获取:现在我们用不了剩余参数了,所以只能在函数体内部通过 arguments 获取所有参数。对于 myBind
,我们实际上需要的是除开第一个传入的 thisArg 参数之外的剩余所有参数构成的数组,所以这里可以通过 Array.prototype.slice.call
借用数组的 slice 方法(arguments 是类数组,无法直接调用 slice),这里的借用有两个目的:一是除去 arguments 中的第一个参数,二是将除去第一个参数之后的 arguments 转化为数组(slice 本身的返回值就是一个数组,这也是类数组转化为数组的一种常用方法)。同样地,返回的新函数 fnBound 后面调用的时候也可能传入参数,再次借用 slice 将 arguments 转化为数组
2). 调用方式:同样,这里也要判断 fnBound 是 new 调用还是普通调用。在 ES6 版本的实现中,如果是 new 调用 fnBound,那么直接返回 new fnToBind()
,这实际上是最简单也最容易理解的方式,我们在访问实例属性的时候,天然就是按照 实例 => 实例.__proto__ = fnToBind.prototype
这样的原型链来寻找的,可以确保实例成功访问其构造函数 fnToBInd 的原型上面的属性;但在 ES3 的实现中(或者在网上部分 bind 方法的实现中),我们的做法是返回一个 fnToBind.apply(this)
,实际上相当于返回一个 undefined 的函数执行结果,根据 new 的原理,我们没有在构造函数中自定义一个返回对象,因此 new 的结果就是返回实例本身,这点是不受影响的。这个返回语句的问题在于,它的作用仅仅只是确保 fnToBind 中的 this 指向 new fnBound 之后返回的实例,而并没有确保这个实例可以访问 fnToBind 的原型上面的属性。实际上,它确实不能访问,因为它的构造函数是 fnBound 而不是 fnToBind,所以我们要想办法在 fnBound 和 fnToBind 之间建立一个原型链关系。这里有几种我们可能会使用的方法:
// 这里的 this 指的是 fnToBind fnBound.prototype = this.prototype
这样只是拷贝了原型引用,如果修改 fnBound.prototype
,则会影响到 fnToBind.prototype
,所以不能用这种方法
// this 指的是 fnToBind fnBound.prototype = Object.create(this.prototype)
通过 Object.create
可以创建一个 __proto__
指向 this.prototype
的实例对象,之后再让 fnBound.prototype
指向这个对象,则可以在 fnToBind 和 fnBound 之间建立原型关系。但由于 Object.create
是 ES6 的方法,所以无法在我们的 ES3 代码中使用。
// this 指的是 fnToBind const Fn = function(){} Fn.prototype = this.prototype fnBound.prototype = new Fn()
这是上面代码采用的方法:通过空构造函数 Fn 在 fnToBind 和 fnBound 之间建立了一个联系。如果要通过实例去访问 fnToBind 的原型上面的属性,可以沿着如下原型链查找:
实例 => 实例.__proto__ = fnBound.prototype = new Fn() => new Fn().__proto__ = Fn.prototype = fnToBind.prototype
更多编程相关知识,请访问:编程教学!!
The above is the detailed content of This article will give you an in-depth understanding of the implementation of call, apply and bind methods. For more information, please follow other related articles on the PHP Chinese website!