手写代码实现call和apply功能

call和apply的功能类似, 区别仅仅是传入的参数形式不同。
它们的作用都是使用指定的this值来调用某个函数,用来改变上下文环境。

举例说明:

1
2
3
4
5
6
7
8
9
var foo = {
value: 1
};
function bar() {
console.log(this.value);
}
bar.call(foo); //输出1

call把this的指向改成指向foo, 但bar函数依旧执行了。

Step1

调用call的时候, 想当于把foo对象进行了一次改造:

1
2
3
4
5
6
7
8
var foo = {
value: 1,
bar: function() {
console.log(this.value)
}
};
foo.bar(); // 1

等于把bar函数设为了foo对象的一个属性——>执行bar函数——>删除这个属性

按照这个思路,可以写出一个call2模拟函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Function.prototype.call2 = function(context){
//用this获取调用call的函数
context.fn = this;
context.fn();
delete context.fn;
}
//test
var foo = {
age: 12
}
function bar(){
console.log(this.age);
}
bar.call2(foo); //输出12

但是这个模拟没有实现传参功能,所以需要进行改造。

Step2

关于传参, 举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var foo = {
value: 1
};
function bar(name, age) {
console.log(name)
console.log(age)
console.log(this.value);
}
bar.call(foo, 'Snapline', 28);
//输出
//Snapline
//28
//1

call的传参是不定的, 用arguments来获取所有的参数。

改造如下:

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
Function.prototype.call2 = function(context){
//用this获取调用call的函数
context.fn = this;
//获取参数
const args = [...arguments].slice(1);
context.fn(...args);
delete context.fn;
}
//test
var foo = {
value: 1
};
function bar(name, age) {
console.log(name)
console.log(age)
console.log(this.value);
}
bar.call2(foo, 'Snapline', 28);
//输出
//Snapline
//28
//1

Step3

基本已经完成了模拟, 有几个小问题需要注意:

  • 1.this参数可以传null, 此时this指向window

    1
    2
    3
    4
    5
    6
    7
    var value = 1;
    function bar() {
    console.log(this.value);
    }
    bar.call(null); // 相当于window.value 输出1
  • 2.函数可以有返回值

所以有如下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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
Function.prototype.call2 = function(context){
//传入的必须是函数
if(typeof this !== 'function'){
throw new TypeError('Error');
}
//传入null的话,this指向window
context = context || window;
//用this获取调用call的函数
context.fn = this;
//获取参数
const args = [...arguments].slice(1);
//创建result用来做返回值
const result = context.fn(...args);
//恢复默认的fn
delete context.fn;
//返回值
return result;
}
//test
var value = 2;
var obj = {
value: 1
}
function bar(name, age) {
console.log(this.value);
return {
value: this.value,
name: name,
age: age
}
}
bar.call2(null); // 2
console.log(bar.call2(obj, 'Snapline', 28));
//输出
//1
//Object {
// value: 1,
// name: 'Snapline',
// age: 28
// }

实现apply

实现了call以后实现apply就非常简单了, 只是参数形式不同。

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
//实现apply
Function.prototype.apply2 = function(context){
//传入的必须是函数
if(typeof this !== 'function'){
throw new TypeError('Error');
}
context = context || window;
context.fn = this;
let result;
if(Array.isArray(arguments[1])) {
// 通过...运算符将数组转换为用逗号分隔的参数序列
result = context.fn(...arguments[1])
} else {
result = context.fn()
}
delete context.fn;
return result;
}
//test
function test(arg1, arg2) {
console.log(arg1, arg2)
console.log(this.a, this.b)
}
test.apply2({
a: 'a',
b: 'b'
}, [1, 2])
//(1,2)
//(a,b)
Snapline wechat
扫码关注我的公众号“约翰柠檬的唱片店”
Buy me a cup of Coffee