call,apply的模拟实现

call的实现

先看call的用法

1
2
3
4
5
6
7
8
var obj = {
value: 'hello'
}

function show() {
console.log(this.value)
}
show.call(obj) // hello

我们知道,这里show方法执行了,同时将this指向了obj这个对象。现在要模拟这个实现。

主要过程是转变为类似以下的形式

1
2
3
4
5
6
var obj = {
value: 'hello',
show: function() {
console.log(this.value)
}
}

以上面的例子为例,就是要在obj对象里添加一个方法,这个方法就是我们外层要执行的方法,然后调用这个方法。我们自己添加了一个方法,最后执行完肯定要删掉它,保持原来的对象不变。

1
2
3
4
5
Function.prorotype.myCall = (context) => {
context.fn = this; // this可以拿到要执行的方法
context.fn();
delete context.fn;
}

我们知道call是可以接收其他参数的。对于接收不定数量的参数,我们自然想到用arguments

call接收的第一个参数是指定的对象,剩下的参数我们就从arguments[1]开始取啦。

1
2
3
4
var args = [];
for (var i = 1, length = arguments.length; i < length; i++) {
args.push('arguments[' + i + ']'); // 这里存为字符串因为后面还会转回来
}

剩下的步骤就是把args展开放大obj.fn里了

1
eval('context.fn(' + args + ')')

这里需要了解一下eval。eval是将接收的字符串以js代码的方式执行,同时,这个例子里的args会调用Array.toString()方法。看一下Array.toString方法

1
2
var arr = [1, 2, 3];
arr.toString() // "1,2,3"

所以 eval('context.fn(' + args +')') 这段代码,最后实际是变为类似下面这种

1
context.fn(arguments[1], arguments[2], ........)

还有需要注意的点

  • 指定对象的参数可能为null
  • 函数可能会有返回值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Function.prototype.myCall = function(context) {
var context = context || window;
context.fn = this;

var args = [];
for (var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}

var result = eval('context.fn(' + args + ')');

delete context.fn
return result;
}

apply的实现

跟call几乎一样,apply因为接受的第二个参数是固定的数组类型的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Function.prototype.apply = function(context, arr) {
var context = Object(context) || window;
context.fn = this;

var result;
if (!arr) {
result = context.fn();
} else {
var args = [];
for (var i = 0, len = arr.length; i < len; i++) {
args.push('arr[' + i + ']');
}
result = eval('context.fn(' + args + ')')
}

delete context.fn
return result;
}