深入理解JavaScript系列(8):call/apply

call bind apply 的区别

首先,我们先来了解一下 call,apply,bind 的区别,方便我们去手写实现自己的 call、apply、bind

区别 bind call apply
是否立即调用
参数一 指定的对象(该函数的执行上下文 指定的对象(该函数的执行上下文 指定的对象(该函数的执行上下文
其他参数 后面的参数都是传入函数的值 后面的参数都是传入函数的值 只有两个参数,第二个参数是数组

call 实现

我们可以分为七个步骤:

  1. 判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况
  2. 判断传入上下文对象是否存在,如果不存在,则设置为 window
  3. 处理传入的参数,截取第一个参数后的所有参数
  4. 将函数作为上下文对象的一个属性
  5. 使用上下文对象来调用这个方法,并保存返回结果
  6. 删除刚才新增的属性
  7. 返回结果

第一版

call 在使用一个指定的 this 值和若干个指定的参数值的前提下调用某个参数或方法。

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

我们来看看步骤:

  • 第一步就不用多说了,你可以直接使用一个 Object 对象去调用试一下,是抛出了一个错误的,这里我们用打印来表示。
  • 第二步就是获取该方法的上下文对象,并调用这个方法了。
  • 第三步就是删除新增的属性。
1
2
3
4
5
6
7
8
9
Function.prototype.myCall = function (context) {
if (typeof this !== 'function') {
console.log('只有函数可以调用 myCall');
return;
}
context.fn = this;
context.fn();
delete context.fn;
};

这时候我们会发现打印结果对上了,也是 2

第二版

接下来就是处理参数了,从区别可以知道,call 是可以有无数参数的,我们在第二步之前添加一个步骤。

先来看看真实 call 的测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
let value = 1;
const foo = {
value: 2,
};
function fn(name, age) {
console.log(this.value);
console.log(name);
console.log(age);
}
fn.call(foo, 'hzzzzzzzq', 18);
// 2
// hzzzzzzzq
// 18
  • 获取第二个参数开始的全部参数
1
2
3
4
5
6
7
8
9
10
Function.prototype.myCall = function (context) {
if (typeof this !== 'function') {
console.log('只有函数可以调用 myCall');
return;
}
let args = [...arguments].slice(1); // 截取参数
context.fn = this;
context.fn(...args); // 给函数传入参数
delete context.fn;
};

使用我们的 myCall 执行,发现结果是相同的。

最后版

在这里我们需要注意两个问题

  • 传入的对象是 null

注意,我将外层的定义 let 改成了 var,因为 var 设置了全局变量,如果设置 let 则会打印 undefined,并没有在 window 对象中定义。

1
2
3
4
5
6
7
8
var value = 1;
const foo = {
value: 2,
};
function fn(name, age) {
console.log(this.value);
}
fn.call(null); // 1

执行代码,可以知道,传入 null 时,指向了我们的 window 对象。

  • 对象可以有返回值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var value = 1;
const foo = {
value: 2,
};
function fn(name, age) {
return {
value: this.value,
name,
age,
};
}
const result = fn.call(foo, 'hzzzzzzzq', 18);
console.log(result);
// {
// value: 2,
// name: 'hzzzzzzzq',
// age: 18
// }
  • 最后代码全代码

首先处理传入 null 的时候,判断传入上下文对象是否存在,如果不存在,则设置为 window
处理第二个问题则是将方法执行结果进行保留,返回结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
Function.prototype.myCall = function (context) {
if (typeof this !== 'function') {
// 判断,在手写过程中 可以不需要
console.log('只有函数可以调用 myCall');
return;
}
context = context || window; // 判断上下文对象是否存在
context.fn = this; // 将函数作为上下文的一个属性
let args = [...arguments].slice(1); // 获取传入的参数,从第二个参数起的所有参数
const result = context.fn(...args); // 执行该函数,并使用上面的参数
delete context.fn; // 删除新增对象
return result; // 返回结果
};

测试一下我们自己写的 call 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var value = 2;
let obj = {
value: 1,
};
function test(name, age) {
console.log(this.value);
return {
value: this.value,
name: name,
age: age,
};
}
console.log(test.myCall(obj, 'hzq', 18));
// 1
// {
// "value": 1,
// "name": "hzq",
// "age": 18
// }

apply 实现

我们首先了解了三者的区别,我们发现 applycall 的区别,就在于传入的参数,针对这一区别,我们开始实现 apply,就只需要对 call 的代码进行部分修改即可。

  1. 判断调用对象是否为函数,即使我们是定义在函数的原型上的,但是可能出现使用 call 等方式调用的情况
  2. 判断传入上下文对象是否存在,如果不存在,则设置为 window
  3. 将函数作为上下文对象的一个属性
  4. 判断参数值是否传入
  5. 使用上下文对象来调用这个方法,并保存返回结果
  6. 删除刚才新增的属性
  7. 返回结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Function.prototype.myApply = function (context, arr) {
if (typeof this !== 'function') {
console.log('只有函数可以调用 myApply');
return;
}
context = context || window;
context.fn = this;
let result = null;
if (!arr) {
// 数组没有值时,直接调用
result = context.fn();
} else {
// 数组有值,传入参数
result = context.fn(...arr);
}
delete context.fn;
return result;
};

测试与 call 类似,只是修改了传入参数为数组, 打印结果相同。

1
console.log(test.myApply(obj, ['hzq', 18]));

文章参考

JavaScript 深入之 call 和 apply 的模拟实现

-------------------- 本文结束 感谢阅读 --------------------