call的原生实现

在平时的 coding 过程中,很多时候都用到了 call 方法对当前调用方法的 this 指向做了改变,那么这个 call 究竟是怎么达到这个更改当前执行环境的 this 执行的呢?

自己实现一个 call 加深对方法的认识

首先看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
var test = {
name: 'test name'
};
var obj = {
name: 'obj name',
fn: function () {
console.log(this.name);
}
}

obj.fn(); // obj name
obj.fn.call(test); // test name

这里我们调用 obj.fn() 方法将当前 obj 下面的 name 值打印在了控制台,那么通过 call 方法改变 obj 的执行环境后就能打印出 test name 到控制台是怎么处理的呢?实现请看下面。
自己实现一个 call 方法改变当前方法调用的执行环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
var test = {
name: 'test name'
};
var obj = {
name: 'obj name',
fn: function () {
console.log(this.name);
}
};

obj.fn(); // obj name
obj.fn.customCall(test); // ?

// customCall 实现
Function.prototype.customCall = function(o) {
// 将当前调用函数的 this 绑定到需要更改到的对象 test 上的 _fn_ 属性上
// 即这里的 this 是 obj.fn 这个方法,将这个方法存储到 test 对象的属性 _fn_ 上
o._fn_ = this;

// 执行需要将 this 指向更改到的对象 test 上的新属性 _fn_,因为它在前一步赋值了函数 fn:function(){console.log(this.name);}
// 执行时,是由 test 调用的函数,即 this 指向是当前 test,所以在控制台打印出 test name
o._fn_();

// 执行完之后删除 _fn_ 属性,去除后续影响
// 这里的 _fn_ 只是一个属性名,但是在真正在需要着这样做的时候一定要注意可能会覆盖 o 上的原有属性,取名也很重要
delete o._fn_;
}

在这个方法 customCall 成功在 Function 的原型上实现之后, `obj.fn.customCall(test)` 就能够成功打印出 test name 了。

obj.fn.customCall(test); // test name

到这里,自己实现的 customCall 基本完成了一大半,那还有什么没处理完呢?
在我们调用原生的 call 方法时是可以传入参数的,那么 customCall 方法也应该改一下,最后,自己动手实现的 customCall 完整版如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
Function.prototype.customCall = function(o, ...arg) {
let result;
// 如果调用 customCall 没有传入参数时则绑定到 全局
// 如果是像 string、number 这些值时转换成 object
let context = (o === null || o === undefined) ? window || global : Object(o);

context._fn_ = this;
result = context._fn_(...arg);
delete context._fn_;
// 如果有返回值则返回具体的值,否则返回 undefined
return result;
}
/* 如果这里不用 es6 的语法处理参数,实现如下
Function.prototype.customCall = function(o) {
// 如果调用 customCall 没有传入参数时则绑定到 全局
// 如果是像 string、number 这些值时转换成 object
let context = (o === null || o === undefined) ? window || global : Object(o);

let argArr = [];
for (let i = 0; i < arguments.length; i++) {
argArr.push('arguments[' + i + ']');
// 这里要push 这行字符串 而不是直接push 值
// 因为直接push值会导致一些问题
// 例如: push一个数组 [1,2,3]
// 在下面👇 eval调用时,进行字符串拼接,JS为了将数组转换为字符串 ,
// 会去调用数组的 toString() 方法,变为 '1,2,3' 就不是一个数组了,相当于是3个参数.
// 而push这行字符串,eval 方法,运行代码会自动去 arguments 里获取值
}

context._fn_ = this;
// 字符串拼接,JS会调用 argArr 数组的 toString() 方法,这样就传入了所有参数
eval('context._fn_(' + argArr.toString() +')');
delete context._fn_;
}
*/

var test = {
name: 'test name'
};
var obj = {
name: 'obj name',
fn: function () {
console.log(this.name, ...arguments);
}
};

obj.fn(1, 2, 3); // obj name 1 2 3
obj.fn.customCall(test, 1, 2, 3); // test name 1 2 3

总结

这里对 call 方法进行了再次的学习,从不同角度进行了更加深入的理解