前言
本文 1630 字, 阅读大约需要 8 分钟.
总括: 本文从零开始通过提出问题然后解决问题的方式模拟实现了比较完善的 call 和 apply 方法
参考文档: Function.prototype.call(),Function.prototype.apply()
公众号:「前端进阶学习」, 回复「666」, 获取一揽子前端技术书籍
每一个不曾起舞的日子, 都是对生命的辜负.
正文
call,apply 简介
首先介绍下 call 和 apply 两个方法, 这两个方法都是挂载在函数的原型上的, 所以所有的函数都可以调用这两个方法.
注意: call() 方法的作用和 apply() 方法类似, 区别就是 call() 方法接受的是参数列表, 而 apply() 方法接受的是一个参数数组.
例子:
- 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
对于 this 不熟悉的同学可以先异步: 理解 JavaScript 的 this. 总结起来一句话: JavaScript 函数的 this 指向调用方, 谁调用 this 就指向谁, 如果没人谁调用这个函数, 严格模式下指向 undefined, 非严格模式指向 Windows.
所以本质上 call 和 apply 就是用来更改被调用函数的 this 值的. 如上, call 和 apply 只有参数的不同, 模拟实现了 call, 那么 apply 就只是参数处理上的区别. 也就是说, call 和 apply 干了两件事:
改变被调用函数的 this 值;
传参调用;
### 更改 this
现在模拟实现 call 和 apply 的问题转移到另一个问题上, 即如何去更改一个函数的 this 值, 很简单:
- 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);
也就是说我们把这个方法赋值给对象, 然后对象调用这个函数就可以了. 改变一个函数的 this 步骤很简单, 首先将这个函数赋值给 this 要指向的对象, 然后对象调用这个函数, 执行完从对象上删除掉这个函数就好了. 步骤如下:
- obj.foo = foo;
- obj.foo();
- delete obj.foo;
有了思路我们实现第一版 call 方法:
- Function.prototype.call2 = function(context) {
- context = context || {};
- context[this.name] = this;
- context[this.name]();
- delete context[this.name];
- }
this.name 是函数声明的名称, 但其实是没必要一定对应函数名称的, 我们随便用一个 key 都可以:
- Function.prototype.call2 = function(context) {
- context = context || {};
- context.func = this;
- context.func();
- delete context.func;
- }
使用新的 call 调用上面的函数:
- foo.call2(obj1); // 1
- foo.call2(obj2); // 2
OK,this 的问题解决了, 接下来就是传参的问题:
传参
函数中的参数保存在一个类数组对象 arguments 中. 因此我们可以从 arguments 里面去拿从传到 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;
- }
此时问题来了, 如何把参数 params 传递到 func 中呢? 比较容易想到的办法是利用 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;
- }
看下我们的例子:
- foo.call2(obj1, 1); // 2
- foo.call2(obj2, 2); // 4
还有一个实现, 是利用不常用的 eval 函数, 即我们把参数拼接成一个字符串, 传给 eval 函数去执行,
eval() 函数可计算某个字符串, 并执行其中的的 JavaScript 代码.
看下我们的第二版实现:
- Function.prototype.call2 = function(context) {
- context = context || {};
- var params = [];
- for (var i = 1; i < arguments.length; i++) {
- params[i - 1] = arguments[i];
- }
- // 注意, 此处的 this 是指的被调用的函数
- context.func = this;
- eval('context.func(' + params.join(",") + ')');
- delete context.func;
- }
其它
call 和 apply 还有另外两个重要的特性, 可以正常返回函数执行结果, 接受 null 或 undefined 为参数的时候将 this 指向 Windows, 然后我们来实现下这两个特性, 然后加上必要的判断提示, 这是我们的第三版实现:
- Function.prototype.call2 = function(context) {
- context = context || Windows;
- var params = [];
- // 此处将 i 初始化为 1, 是为了跳过 context 参数
- for (var i = 1; i < arguments.length; i++) {
- params[i - 1] = arguments[i];
- }
- // 注意, 此处的 this 是指的被调用的函数
- context.func = this;
- var res = eval('context.func(' + params.join(",") + ')');
- delete context.func;
- return res;
- }
然后我们调用测试下:
- foo.call2(obj1, 1); // 2
- foo.call(2, 1); // NaN
- foo.call2(2, 1); // context.func is not a function
如上我们发现将对象改成数字 2 后原始 call 返回了 NaN, 我们的 call2 却报错了, 说明一个问题, 我们直接 context = context || Windows 是有问题的. 内部还有一个类型判断, 解决这个问题后, 我们的第四版实现如下:
- Function.prototype.call2 = function(context) {
- if (context === null || context === undefined) {
- context = Windows;
- } else {
- context = Object(context) || context;
- }
- var params = [];
- // 此处将 i 初始化为 1, 是为了跳过 context 参数
- for (var i = 1; i < arguments.length; i++) {
- params[i - 1] = arguments[i];
- }
- // 注意, 此处的 this 是指的被调用的函数
- context.func = this;
- var res = eval('context.func(' + params.join(",") + ')');
- delete context.func;
- return res;
- }
这就是我们的最终代码, 这个代码可以从 ES3 一直兼容到 ES6, 此时:
- foo.call(2, 1); // NaN
- foo.call2(2, 1); // NaN
模拟实现 apply
apply 和 call 只是参数上的区别, 将 call2 改写就好了:
- Function.prototype.apply2 = function(context, arr) {
- if (context === null || context === undefined) {
- context = Windows;
- } else {
- context = Object(context) || context;
- }
- // 注意, 此处的 this 是指的被调用的函数
- context.func = this;
- arr = arr || [];
- var res = eval('context.func(' + arr.join(",") + ')');
- delete context.func;
- return res;
- }
以上就是我们最终的实现, 目前还有一个问题就是 context.func 的问题, 这样一来我们传进来的 context 就不能使用 func 字符串作为方法名了.
结论
我们实现过程都解决了以下问题:
更改被调用函数的 this;
将参数传递给被调用函数;
将被调用函数结果返回, 第一个参数为 null 或 undefined 的时候被调用函数的 this 指向 Windows;
解决类型判断的问题;
以上.
能力有限, 水平一般, 欢迎勘误, 不胜感激.
来源: https://www.qcloud.com/developer/article/1588806