JavaScrip 中的 this, bind, call & apply 简述
this
是一个比较特殊的东西,基本上可以理解成 this
的指向是就近调用的指向,因此 this
在 JS 中也是一个比较令人困惑的知识点。
之前绕过 this
的方法基本上采用 arrow function,因为 arrow function 不包含对 this
, arguments
, 或者 super
的绑定,因此使用 arrow function 时,this
的指向更容易判断一些,不过最近感觉也是时候细究一下 this
, call
, apply
和 bind
了。
this 的指向问题
上面说了,this
的指向采用的是就近调用原则,以下面的代码为例:
const person = {
name: 'John',
greet() {
console.log(this);
console.log('Hello ' + this.name);
},
};
正常情况下,greet
中的 this
指向应该指代的是 person
,在下面这种调用的情况下是正确的:
person.greet();
这个情况下,greet
是通过 person
这个对象去调用的,因此 greet
绑定的环境是 person
对象本身。
不过换一种方式调用,就会有不同的结果:
const { greet } = person;
greet();
这时候的 greet
指向就变成了 global,或者在严格模式下变为 undefined:
这是因为 greet
被解构出来了,在这个时候调用它的是 global 对象下,换言之,这个时候的 this
就会自动绑定成 global
对象。
再换一种调用的方法:
const user = {
name: 'user',
};
user.greet = person.greet;
user.greet();
这个时候的 greet
因为是被 user
调用的,因此绑定的对象就是 user
:
大多数情况下 this
的问题不是很大,不过有的时候用到 callback 时就会是一个比较麻烦的事情,如下面模拟一个挂载 callback,并且实现延后执行的情况:
const example = {
addEventListener(cb) {
this.cb = cb;
},
click() {
this.cb();
},
};
example.addEventListener(person.greet);
example.click();
这时候调用 greet
中的指向就变成了 example
:
这种方法也是很多时候 behind the scene 的实现,因此 this
的指向是相当不清晰的。
在一些情况下,这也就是 bind
, call
和 apply
可以帮忙的地方。
bind
bind
会创建一个新的函数,同时 bind
会将 this
绑定成第一个传过去的参数。
遥想当初使用 React 的 class based component 时,在没有使用 arrow function 的时候,都是使用 bind
去执行的:
class ExplainBindingsComponent extends Component {
constructor() {
super();
this.onClickMe = this.onClickMe.bind(this);
}
onClickMe() {
console.log(this);
}
}
这就是为了将方法中的 this
绑定到当前类中,使其不随从就近原则。这里的第一个参数就是想要绑定的对象,在无所谓第绑定的对象时可以用 null
,使用 this
指代的是当前对象,也可以明确的使用一个已经实例化的对象。
上面的几个案例使用 bind
绑定的改写方法为:
// 'use strict';
const person = {
name: 'John',
greet() {
console.log(this);
console.log('Hello ' + this.name);
},
};
const { greet } = person;
greet();
greet.bind(person)();
const user = {
name: 'user',
};
user.greet = person.greet.bind(person);
user.greet();
const example = {
addEventListener(cb) {
this.cb = cb;
},
click() {
this.cb();
},
};
example.addEventListener(person.greet.bind(person));
example.click();
这样都可以通过 bind
将 this
绑定到固定的对象上,使其不受就近原则的影响。另外,bind
也可以接受其他的参数,并将其传到调用的函数中,如:
const person = {
name: 'John',
greet(from) {
console.log(this);
console.log('Hello ' + this.name + ' from ' + from);
},
};
const { greet } = person;
// greet();
greet.bind(person, 'Sam')();
在一些情况下,使用 bind
也可以简化代码,如下面的代码可以实现一段基础的加减乘除的操作:
const math = {
accumulated: 0,
add(num) {
const oldNum = this.accumulated;
this.accumulated += num;
console.log(`${oldNum} + ${num} = ${this.accumulated}`);
},
substract(num) {
const oldNum = this.accumulated;
this.accumulated -= num;
console.log(`${oldNum} - ${num} = ${this.accumulated}`);
},
multiply(num) {
const oldNum = this.accumulated;
this.accumulated *= num;
console.log(`${oldNum} * ${num} = ${this.accumulated}`);
},
divide(num) {
const oldNum = this.accumulated;
this.accumulated /= num;
console.log(`${oldNum} / ${num} = ${this.accumulated}`);
},
};
math.add(10);
math.substract(5);
math.multiply(2);
math.divide(5);
这种情况下,可以使用 bind
去简化操作:
const math = {
accumulated: 0,
calculation(operation, num) {
let oldNum = this.accumulated;
if (operation === '+') {
this.accumulated += num;
} else if (operation === '-') {
this.accumulated -= num;
} else if (operation === '*') {
this.accumulated *= num;
} else if (operation === '/') {
this.accumulated /= num;
}
console.log(`${oldNum} ${operation} ${num} = ${this.accumulated}`);
},
};
const { calculation } = math;
calculation.bind(math, '+', 10)();
calculation.bind(math, '-', 5)();
calculation.bind(math, '*', 2)();
calculation.bind(math, '/', 5)();
最后的运行结果也是一致的:
不是说一定要用 bind
去执行,也可以用其他的方式使得代码更加清晰可读,并提高复用性
call & apply
call
和 apply
两个不会创建一个新的函数,但是它们的用法和 bind 相似,第一个参数都是 this
的指向,只不过 call
可以传递无限多的参数,而 apply
会接受一个 array like 的结构作为第二个参数:
call(thisArg, arg1, /* …, */ argN);
apply(thisArg, argsArray);
根据 mdn 所说,通常情况下,在不涉及到 constructed 的情况下,bind
和 call
是可以被视作效果相同的:
You can generally see
const boundFn = fn.bind(thisArg, arg1, arg2)
as being equivalent toconst boundFn = (...restArgs) => fn.call(thisArg, arg1, arg2, ...restArgs)
for the effect when it’s called (but not whenboundFn
is constructed).
reference
- Function.prototype.bind()