This is the first day of my participation in Gwen Challenge
This is the first article in a series exploring the principles of JS native methods. This article shows you how to implement the Call, apply, and bind methods. As for the specific usage of these methods, MDN or articles on the site have been described very clearly, and will not be repeated 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
}
Copy the code
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
constres = thisArg.fn(... args)delete thisArg.fn
return res
}
Copy the code
When calling a function from call, you can specify this in the function by passing thisArg to call. This can be done by making the function called through thisArg, which is our main goal.
Realize the point
-
MyCall is ultimately called through a function, so myCall is mounted on the function prototype just like Call. And because myCall is called through a function, inside myCall we can call this to get the caller of myCall, that is, the function that is actually executed.
-
MyCall is normally mounted on a function prototype. When we call myCall from a non-function, we are bound to throw an error, so why check the type of the caller in myCall and define an error? This is because when a caller obj = {} is an object, but inherits from Function (obj.__proto__ = function.prototype), it can actually call myCall as a non-function. If you don’t type check to make sure it’s a function, you’ll throw an error later when you call it as a function
-
If thisArg passed to call is null or undefined, thisArg will actually point to the global object; If thisArg is a primitive type, then you can use Object() to do a boxing operation to convert it to an Object — mainly to ensure that the function can be executed later as a method call. So can I write thisArg = thisArg? Object(thisArg) : How about globalThis? If thisArg was Boolean false, thisArg would end up equal to globalThis, but in fact it should be equal to Boolean {false}.
-
ThisArg. Fn = this = thisArg; thisArg = thisArg; thisArg = thisArg; thisArg = thisArg; thisArg = thisArg;
-
ThisArg. Fn = this adds a fn attribute to thisArg, so delete this attribute before returning the result. Also, to avoid overwriting the property fn of the same name that might exist on thisArg, here too const fn = Symbol(‘fn’) can be used to construct a unique property, then thisArg[fn] = this.
-
The main differences between ES3 and ES6 are the passing of arguments and the execution of functions:
- ES6 introduces residual parameters, so no matter how many parameters are passed in the actual execution of the function, they can be obtained through the ARgs array. Meanwhile, because of the introduction of the expansion operator, the args array can be expanded, passing the parameters to the function execution
- But in ES3 there is no such thing as residual parameters, so in the definition
myCall
ThisArg, and get all the arguments from the body of the function via the arguments class array. We need all elements of arguments except the first element (thisArg). How do we do that? If it’s ES6, go straight[...arguments].slice(1)
That’s fine, but this is ES3, so we have to loop arguments from index 1 and then push into an ARgs array. Also note that the arguments pushed in here are string arguments, which are passed to the function one by one when the function is executed through eval. - Why is it necessary to pass eval to execute a function? Since we don’t know how many arguments the function actually takes, and we don’t use the expansion operator, we have to construct an executable string expression that explicitly passes all the arguments to the function.
Apply by hand
The use of apply is very similar to call, and therefore the implementation is very similar. The difference to note is that call can take multiple arguments (i.e. a list of arguments) after it takes a thisArg argument, whereas apply takes a thisArg argument and usually takes an array or array-like object as its second argument:
fn.call(thisArg,arg1,arg2,...) fn.apply(thisArg,[arg1,arg2,...] )Copy the code
If the second argument is passed null or undefined, then the whole thing is passed only thisArg.
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
}
Copy the code
ES6 version
Function.prototype.myApply = function(thisArg,args){
if(typeofthisArg ! ='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 = []
}
// If it is not an array, throw an error as apply does
else if(!Array.isArray(args)){
throw new Error('CreateListFromArrayLike called on non-object')
}
thisArg.fn = this
constres = thisArg.fn(... args)delete thisArg.fn
return res
}
Copy the code
Realize the point
The call implementation is basically the same, except that we need to check the type of the second argument.
Bind by hand
Bind can also bind functions to a this as call and apply do, but there are a few different points to note:
bind
Instead of calling the original function directly after specifying this, the original function returns a new function that completes the this binding internally- Arguments to the original function can be passed in batches, the first batch of which can be called later
bind
The second batch of arguments can be passed in when the new function is called. The two batches of arguments are eventually merged and passed to the new function for execution - If a new function is called using a new method, this inside the function refers to the instance instead of the original call
bind
When passed thisArg. In other words, in this casebind
It’s pretty much ineffective
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(){
// if called by new
return fnToBind.apply(this instanceof fnBound ? this:thisArg,args1.concat(args2))
}
// Instance inheritance
var Fn = function(){}
Fn.prototype = this.prototype
fnBound.prototype = new Fn()
return fnBound
}
Copy the code
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){
// if called by new
if(this instanceof fnBound){
return newfnToBind(... args1,... args2) }else {
return fnToBind.apply(thisArg,[...args1,...args2])
}
}
}
Copy the code
Realize the point
-
Bind implements the internal this binding with the help of Apply, assuming we can use the Apply method directly
-
Let’s start with the simpler ES6 version:
- Parameter fetching: Because ES6 can use residual arguments, it is easy to get the parameters needed to execute the original function, and arrays can be easily merged using the expansion operator.
- Call wayThisArg = thisArg; thisArg = thisArg; thisArg = thisArg; thisArg = thisArg; thisArg = thisArg
this instanceof fnBound
ThisArg is invalid, new returns a new function equivalent to new old function, new fnBound is equivalent to new fnToBind, so we return a new fnToBind; Otherwise, if fnBound is a normal call, thisArg is bound by apply and the final result is returned. As you can see, the this binding of bind is essentially done by apply.
-
Let’s take a look at the slightly more troublesome ES3 version:
-
Parameter fetching: Now we can’t use the remaining arguments, so we can only get all arguments inside the function body using Arguments. For myBind, we actually need is among the first incoming thisArg parameters consisting of all the parameters for the remainder of the Array, so it can be through the Array. The prototype. Slice. Call borrow Array slice method (arguments is an Array, You can’t call slice directly), this borrowing serves two purposes: to remove the first argument from arguments, and to convert arguments to an array (the return value of slice itself is an array, which is a common way of converting an array to an array). Similarly, the new returned function fnBound may be called with arguments, again borrowing from slice to convert arguments to an array
-
How it’s called: Again, we need to determine whether fnBound is a new call or a normal call. In the ES6 version, if new calls fnBound, we return new fnToBind(), which is actually the simplest and most understandable way to access instance properties, Prototype => instance.__proto__ = fnToBind. Prototype => instance.__proto__ = fnToBind. Prototype But in the ES3 implementation (or in some of the bind implementations on the web), what we do is we return a fntobind.apply (this), which essentially returns the result of undefined. According to the principle of new, We do not have a custom 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 refers to the instance returned after new fnBound. It does not ensure that the instance has access to the properties on the prototype of fnToBind. In fact, it really isn’t accessible, because its constructor is fnBound and not fnToBind, so we need to find a way to create a prototype chain relationship between fnBound and fnToBind. Here are a few methods we might use:
// This is fnToBind fnBound.prototype = this.prototype Copy the code
If you change fnbound. prototype, it will affect fntobInd. prototype, so you can’t use this method
// This is fnToBind fnBound.prototype = Object.create(this.prototype) Copy the code
Create an instance of __proto__ pointing to this.prototype and then fnbound. prototype pointing to that Object. You can establish a prototypical relationship between fnToBind and fnBound. But because object.create is an ES6 method, it cannot be used in our ES3 code.
// This is fnToBind const Fn = function(){} Fn.prototype = this.prototype fnBound.prototype = new Fn() Copy the code
This is the approach taken by the code above: a connection is established between fnToBind and fnBound via the empty constructor Fn. If you want to use an instance to access the attributes on the fnToBind prototype, you can look up the following prototype chain:
Prototype = new Fn() => new Fn().__proto__ = Fn. Prototype = fntobInd.prototype
-