Preface
This article is 1630 words, it takes about 8 minutes to read.
All in all: in this paper, we simulate and implement a relatively perfect call and apply method from scratch by raising problems and then solving them
- Reference documents: Function.prototype.call(),Function.prototype.apply()
- The official account: front-end learning, reply to "666", get a package of front-end technology books. Every day without dancing is a disappointment to life.
text
Introduction to call and apply
First, I will introduce two methods, call and apply, which are both mounted on the function prototype, so all functions can call these two methods.
Note: the function of call() method is similar to that of apply(). The difference is that call() method accepts parameter list, while apply() method accepts an array of parameters.
Example:
function foo(b = 0) { console.log(this.a + b); } const obj1 = { a: 1 }; const obj2 = { a: 2 }; foo.call(obj1, 1); // 2 foo.call(obj2, 2); // 4 foo.apply(obj1, [1]); // 2 foo.apply(obj2, [2]); // 4
For students unfamiliar with this, you can asynchronize first: Understand this in Javascript . To sum up, this of Javascript functions points to the caller, and whoever calls this points to the caller. If no one calls this function, it points to undefined in strict mode and window s in non strict mode.
So call and apply are essentially used to change this value of the called function. As mentioned above, call and apply are only different in parameters. If call is implemented by simulation, then apply is only the difference in parameter processing. In other words, call and apply do two things:
- Change this value of the called function;
- Transfer parameter call;
###Change this
Now the problem of analog implementation of call and apply is transferred to another problem, that is, how to change this value of a function. It is very simple:
function foo(b = 0) { console.log(this.a + b); } const obj1 = { a: 1, foo: foo }; const obj2 = { a: 2, foo: foo }; obj1.foo(1); obj2.foo(2);
That is to say, we assign this method to the object, and then the object calls this function. It's very simple to change this step of a function. First, assign this function to the object this points to, then the object calls this function, and delete the function from the object after execution. The steps are as follows:
obj.foo = foo; obj.foo(); delete obj.foo;
With ideas, we can implement the first version of call method:
Function.prototype.call2 = function(context) { context = context || {}; context[this.name] = this; context[this.name](); delete context[this.name]; }
this.name is the name of the function declaration, but it is not necessary to correspond to the function name. We can use any key:
Function.prototype.call2 = function(context) { context = context || {}; context.func = this; context.func(); delete context.func; }
Use the new call to call the above function:
foo.call2(obj1); // 1 foo.call2(obj2); // 2
OK, the problem of this is solved. Next is the problem of parameter transfer:
Biography
The arguments in the function are stored in a class array object, arguments. So we can get the parameters from arguments to call2:
Function.prototype.call2 = function(context) { context = context || {}; var params = []; for (var i = 1; i < arguments.length; i++) { params[i - 1] = arguments[i]; } context.func = this; context.func(); delete context.func; }
At this point, the question arises. How can params be passed to func? The easy way to think of is to use the extended operator of ES6:
Function.prototype.call2 = function(context) { context = context || {}; var params = []; for (var i = 1; i < arguments.length; i++) { params[i - 1] = arguments[i]; } context.func = this; context.func(...params); delete context.func; }
Look at our example:
foo.call2(obj1, 1); // 2 foo.call2(obj2, 2); // 4
Another implementation is to use the less commonly used Eval function, that is, we splice the parameters into a string and pass it to the eval function for execution,
The eval() function evaluates a string and executes the JavaScript code in it.
Take a look at our second implementation:
Function.prototype.call2 = function(context) { context = context || {}; var params = []; for (var i = 1; i < arguments.length; i++) { params[i - 1] = arguments[i]; } // Note that this here refers to the called function context.func = this; eval('context.func(' + params.join(",") + ')'); delete context.func; }
Other
call and apply also have two other important features. They can return function execution results normally. When accepting null or undefined as parameters, point this to window. Then we can implement these two features and add necessary judgment prompts. This is our third version implementation:
Function.prototype.call2 = function(context) { context = context || window; var params = []; // i is initialized to 1 here to skip the context parameter for (var i = 1; i < arguments.length; i++) { params[i - 1] = arguments[i]; } // Note that this here refers to the called function context.func = this; var res = eval('context.func(' + params.join(",") + ')'); delete context.func; return res; }
Then we call the following test:
foo.call2(obj1, 1); // 2 foo.call(2, 1); // NaN foo.call2(2, 1); // context.func is not a function
As we found above, after changing the object to the number 2, the original call returned NaN, but our call 2 reported an error, indicating that there is a problem with our direct context = context || window. There is also an internal type judgment. After solving this problem, our fourth version is implemented as follows:
Function.prototype.call2 = function(context) { if (context === null || context === undefined) { context = window; } else { context = Object(context) || context; } var params = []; // i is initialized to 1 here to skip the context parameter for (var i = 1; i < arguments.length; i++) { params[i - 1] = arguments[i]; } // Note that this here refers to the called function context.func = this; var res = eval('context.func(' + params.join(",") + ')'); delete context.func; return res; }
This is our final code, which can be compatible from ES3 to ES6. At this time:
foo.call(2, 1); // NaN foo.call2(2, 1); // NaN
Simulate the implementation of apply
apply and call are only parameter differences. Rewrite call2:
Function.prototype.apply2 = function(context, arr) { if (context === null || context === undefined) { context = window; } else { context = Object(context) || context; } // Note that this here refers to the called function context.func = this; arr = arr || []; var res = eval('context.func(' + arr.join(",") + ')'); delete context.func; return res; }
The above is our final implementation. At present, another problem is the problem of context.func, so that the context we passed in can't use func string as method name.
conclusion
Our implementation process has solved the following problems:
- Change this of the called function;
- Pass the parameter to the called function;
- Return the result of the called function. When the first parameter is null or undefined, this of the called function points to window;
- Solve the problem of type judgment;
Above.
Limited ability, general level, welcome to errata, thank you very much.
Subscribe to more articles, official account, advanced learning, reply to "666", and get a package of front-end technology books.