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
- The final is to call a function by
myCall
, somyCall
andcall
as mounted on the function prototype. At the same time, it is precisely becausemyCall
is called through a function, so inmyCall
we can getmyCall
through this, which is the function that is actually executed. - It stands to reason that
myCall
is mounted on the function prototype. When we callmyCall
through a non-function, an error will definitely be thrown, so why check the caller's typemyCall
This is because when a callerobj = {}
is an object, but inherits fromFunction
(obj.__proto__ = Function.prototype
), it can actually call themyCall
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 - 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 useObject()
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 asthisArg = 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 toBoolean {false}
. - 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. 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 useconst fn = Symbol('fn')
construct a unique attribute, and thenthisArg[fn] = this
.The main difference between the ES3 version and the ES6 version lies in the transfer of parameters and the execution of functions:
- 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
- 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. - 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
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。