0%

javascript-apply-call-bind

在JavaScript中,apply, call, bind是三个非常重要的方法,它们的一个重要作用是在调用函数时改变this的值。注意:箭头函数的this值无法修改。

  1. Function.prototype.apply()
  2. Function.prototype.call()
  3. Function.prototype.bind()

apply

假设我们有一个函数用来计算两数之和:

1
2
3
function add(a, b) {
return a + b;
}

我们可以正常调用这个函数:

1
add(1, 2); // 3

也可以使用apply方法来调用这个函数,第一个参数是this的值,第二个参数是一个数组,数组中的元素是函数的参数。这里我们不需要this,所以第一个参数传入null

1
add.apply(null, [1, 2]); // 3

call

call方法和apply方法类似,只是参数的传递方式不同,call方法的参数是一个个传递的,而不是通过数组传递。还是以上面的求和函数为例,使用call方法调用,第一个参数是this的值,后面的参数是函数的参数。这里我们不需要this,所以第一个参数传入null

1
add.call(null, 1, 2); // 3

bind

bind的调用方式与call类似,参数也是一个一个传递的。但是与callapply直接调用函数不同,bind会创建一个新的函数并返回该函数,所以在使用bind时,我们通常会用一个变量接收bind返回的函数,再通过这个变量调用函数。

1
2
3
fn.call(thisValue, arg1, arg2, ...);
fn.apply(thisValue, [arg1, arg2, ...]);
fn.bind(thisValue)(arg1, arg2, ...);

还记得前面的例子吗?将对象方法赋值给普通函数时,this指向了全局对象,而全局对象上没有name属性,所以输出undefined

1
2
3
4
5
6
7
8
9
const person = {
name: 'Philip',
sayName() {
console.log(this.name);
},
};

const sayName = person.sayName;
sayName(); // undefined

我们只要用bind修改一下this,让它绑定到person对象,就可以输出Philip了。

1
2
3
4
5
6
7
8
9
10
const person = {
name: 'Philip',
sayName() {
console.log(this.name);
},
};

// 通过sayName接收bind返回的函数。
const sayName = person.sayName.bind(person);
sayName(); // Philip

call, apply, bind的使用场景

我们知道,函数中有一个隐含对象arguments,包含了函数调用时传入的所有参数。它是一个类数组对象,我们无法对它使用数组的方法,比如map, filter, reduce等。但是我们可以通过call, apply, bind方法将数组的方法应用到arguments上。

比如下面这个求和函数,在函数体内部,我们可以使用reduce方法来计算所有参数的和。

1
2
3
function sum() {
return Array.prototype.reduce.call(arguments, (acc, cur) => acc + cur, 0);
}

注意,上面的例子仅作演示只用,一个更加规范的写法是:

1
2
3
function sum(...args) {
return args.reduce((acc, cur) => acc + cur, 0);
}

再看一个class中使用bind的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class CountDown {
count = 0;
interval = null;

constructor(count) {
this.count = count;
}

decrement() {
console.log(this.count);
this.count -= 1;
if (this.count < 0) {
console.log("Done");
clearInterval(this.interval);
}
}

start() {
this.interval = setInterval(this.decrement, 1000);
}
}

const foo = new CountDown(10);
foo.start();

上面的代码中,this.interval是一个定时器,每隔1秒调用this.decrement方法,但是setInterval这个函数比较特殊,当我们将一个函数传递给setInterval时,这个函数的this值会指向全局对象,所以this.count会输出undefined。详情请参考这里

解决方法有两种,一种是在start方法中使用bind方法,将this绑定到CountDown类。

1
2
3
start() {
this.interval = setInterval(this.decrement.bind(this), 1000);
}

另一种方法是将setInterval的回调函数改为箭头函数,因为箭头函数没有自己的this binding, 所以this指向其parent scope,也就是CountDown。个人推荐使用这种方法。

1
2
3
start() {
this.interval = setInterval(() => this.decrement(), 1000);
}

当然,也可以将decrement方法改为箭头函数,因为箭头函数没有自己的this binding, 所以this指向其parent scope,也就是CountDown

1
2
3
4
5
6
7
8
decrement = () => {
console.log(this.count);
this.count -= 1;
if (this.count < 0) {
console.log("Done");
clearInterval(this.interval);
}
}

Conclusion

  • applycall的作用是改变函数的this值,apply的参数是一个数组,call的参数是一个个传递的。这里有一个小窍门,applya开头的,数组array也是a开头的,所以apply的参数是一个数组。
  • bind传递参数的方法和call类似(这个经常被忽略),但是bind会创建一个新的函数并返回该函数,所以在使用bind时,我们通常会用一个变量接收bind返回的函数,再通过这个变量调用函数。