# 定义
MDN对 call 的定义是:
call()
方法使用一个特定的this
值和单独给出一个参数或多个参数来调用一个函数。
举个例子:
var foo = {
value: 1
}
function bar() {
console.log(this.value)
}
bar.call(foo); // 1
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
注意到两点:
- call 改变了 this 的指向,指向到了foo
- bar 函数执行了
# 第一版
那么怎么来实现这两个效果呢? 假设调用 call 的时候,把 foo 对象改造成如下:
var foo = {
value: 1,
bar: function() {
console.log(this.value)
}
}
foo.bar(); // 1
1
2
3
4
5
6
7
2
3
4
5
6
7
这样就可以把 this 指向 foo 对象了。
但是这样就会给 foo 对象添加多了一个属性,可以用 delete 删除掉就好了
思路就是:
- 将函数设为对象的属性
- 执行该函数
- 删除该函数
foo.fn = bar
foo.fn()
delete foo.fn
1
2
3
2
3
于是可以尝试实现第一版:
Function.prototype.myCall = function(context) {
// 用 this 可以获取调用 myCall 的函数
// context 是传入的参数,this 要指向的对象
context.fn = this
context.fn();
delete context.fn;
}
1
2
3
4
5
6
7
2
3
4
5
6
7
尝试一下看看能不能实现效果:
var foo = {
value: 1
}
function bar() {
console.log(this.value)
}
bar.myCall(foo) // 1
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
是可以实现的。
# 第二版
第一版实现了改变 this 的值和执行调用 call 的函数。 MDN定义中说到,call 还能给定参数执行函数。举例来说:
var foo = {
value: 1
}
function bar(name, age) {
console.log(name)
console.log(age)
console.log(this.value)
}
bar.call(foo, 'revan', '18'); // revan, 18, 1
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
我们知道,函数调用是存在 Arguments 类数组对象
arguments = {
0: foo,
1: 'revan',
2: '18',
length: 3
}
1
2
3
4
5
6
2
3
4
5
6
可以取从第二个到最后一个的参数,放到一个数组里。
var args = [];
for (var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}
// args 为 ["arguments[1]", "arguments[2]"]
1
2
3
4
5
2
3
4
5
这样就解决了不定参数的问题,接着还需要把这个参数组放到要执行的函数的参数里面。 我们可以用 eval 方法去执行:
eval('context.fn(' + args + ')')
// => eval('context.fn(arguments[1], arguments[2])')
1
2
2
这里 args 会自动调用 Array.toString() 这个方法,把数组转成字符串 arguments[1], arguments[2]
尝试写第二版:
Function.prototype.myCall = function (context) {
context.fn = this
var args = []
for (var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}
eval('context.fn(' + args + ')')
delete context.fn
}
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
测试一下:
var foo = {
value: 1
}
function bar(name, age) {
console.log(name)
console.log(age)
console.log(this.value)
}
bar.myCall(foo, 'revan', '18'); // revan, 18, 1
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
It works!
# 第三版
到现在为止已经完成了80%的功能,但是有2点需要注意的是:
1.call 可以不指定第一个参数,此时 this 为 null,当为 null 的时候,视为指向 window。
var value = 1;
function bar() {
console.log(this.value)
}
bar.call(null); // 1
1
2
3
4
5
6
2
3
4
5
6
2.函数有返回值的情况下,需要把返回值返回。
修改下我们最后一版的代码:
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
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
测试一下看看能否实现我们想要的功能:
var value = 2;
var obj = {
value: 1
}
function bar(name, age) {
console.log(this.value)
return {
value: this.value,
name: name,
age: age
}
}
bar.myCall(null); // 2 (window.value)
console.log(bar.myCall(obj, 'revan', 18))
// {
// age: 18,
// name: "revan",
// value: 1
// }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
到此就完成了 call 的模拟实现。
这里顺便给出用 ES6 的方式的实现。
ES6 方式:
Function.prototype.myCall = function(thisArg) {
// this指向调用call的对象
if (typeof this !== 'function') {
// 调用call的若不是函数则报错
throw new TypeError('Error');
}
// 声明一个 Symbol 属性,防止 fn 被占用
const fn = Symbol('fn')
const args = [...arguments].slice(1);
thisArg = thisArg || window;
// 将调用call函数的对象添加到thisArg的属性中
thisArg[fn] = this;
// 执行该属性
const result = thisArg[fn](...args);
// 删除该属性
delete thisArg[fn];
// 返回函数执行结果
return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# apply 的实现
apply 的实现和 call 的类似,只不过 apply 传入的参数是一个数组。输入参数如果为 null 或者 undefined,则表示不需要传入任何参数。
Function.prototype.myApply = 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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ES6 方式:
Function.prototype.myApply = function(thisArg) {
if (typeof this !== 'function') {
throw this + ' is not a function';
}
const args = arguments[1];
const fn = Symbol('fn')
thisArg[fn] = this;
const result = thisArg[fn](...arg);
delete thisArg[fn];
return result;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15