我们经常会需要在 constructor 里面对事件处理函数使用 bind() 来对 this 进行绑定。那么,为什么呢?本文将对此进行探究。
参考文章:This is why we need to bind event handlers in Class Components in React
我们先来看个例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22class Foo extends React.Component{
constructor(props) {
super(props);
}
handleClick(event) {
console.log(this); // 'this' is undefined
}
render() {
return (
<button type="button" onClick={this.handleClick}>
Click Me
</button>
);
}
}
ReactDOM.render(
<Foo />,
document.getElementById("app")
);
当我们点击按钮触发 handleClick 事件的时候,输出的 this 是 undefined。看上去似乎是这个函数丢失了自己的上下文。
在 JS 中,this 的绑定是如何工作的
我们都知道,函数内部的 this 的值取决于该函数如何被调用。
默认绑定
1 | function display() { |
在这个例子中,我们直接调用了这个函数 display()。在非严格模式下,this 指向的是全局对象:window 或者 global。而在严格模式下,就是 undefined。
隐式绑定
1 | var obj = { |
当我们像这样用一个对象上下文来调用方法的时候,display() 里面的 this 就指向了 obj。
但假如我们把这个函数赋值给了另一个变量,然后调用它,我们就会得到不同的结果:1
2
3var name = "uh oh! global";
var outerDisplay = obj.display;
outerDisplay(); // uh oh! global
在上面的例子中,当我们调用 outerDisplay() 的时候,并没有给他指定上下文。它只是一个简单的函数调用,并没有所有者对象。在这种情况下,this 的值就回退到了默认绑定。它指向了全局对象或者是 undefined。
这种情况尤其会发生在把这种函数作为回调传给其他自定义函数、第三方库的函数或者是像 secTimeout 之类的原生函数。
举个例子:1
2
3
4
5
6
7
8
9// A dummy implementation of setTimeout
function setTimeout(callback, delay){
//wait for 'delay' milliseconds
callback();
}
setTimeout( obj.display, 1000 );
我们会发现,当调用 setTimeout 的时候,JS 把 obj.display 赋值给了 callback 这个参数。这个赋值操作,如同我们在前面看到的那样,会导致 display() 失去它的上下文。当这个回调最终在 setTimeout 里执行的时候,它里面的 this 就回退到了默认绑定。
显示绑定
为了避免这些情况,我们可以使用 bind 方法来给函数显示绑定一个 this:1
2
3
4
5
6var name = "uh oh! global";
obj.display = obj.display.bind(obj);
var outerDisplay = obj.display;
outerDisplay();
// Saurabh
现在,当我们调用 outerDisplay() 的时候,它里面的 this 就指向了 obj。
即使现在把 obj.display 作为回调,它里面的 this 依然指向的是 obj。
在文章的开头,我们看到在组件 Foo 里,如果我们不绑定 this,那么调用的时候在函数里拿到的 this 就是 undefined。
正如前面提到并解释的,这是因为在 JS 中 this 的绑定方式造成的,而非 React 的问题。让我们把 React 相关的代码移掉,用原生 JS 来模拟这个情况:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class Foo {
constructor(name){
this.name = name
}
display(){
console.log(this.name);
}
}
var foo = new Foo('Saurabh');
foo.display(); // Saurabh
// The assignment operation below simulates loss of context
// similar to passing the handler as a callback in the actual
// React Component
var display = foo.display;
display(); // TypeError: this is undefined
我们没有去模拟真实的事件和处理,而是使用了同义代码。正如我们在 React Component 示例中观察到的那样,由于将处理函数作为回调导致 this 失去了它的上下文,这和赋值操作是一个意思。这也是我们在非 React JS 代码中观察到的现象。
”等一下!这个值不应该是指向全局对象的吗?我们并没有指定严格模式啊?“你可能会这么问。
不,原因是:
类声明和类表达式的主体是以严格模式执行的,即 constructor、static 和 prototype methods。Getter 和 setter 函数以严格模式执行。参考文章:(Classes - JavaScript | MDN)
所以,为了避免这个问题,我们需要像这样对 this 进行绑定:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class Foo {
constructor(name){
this.name = name
this.display = this.display.bind(this);
}
display(){
console.log(this.name);
}
}
var foo = new Foo('Saurabh');
foo.display(); // Saurabh
var display = foo.display;
display(); // Saurabh
当然,还有其他方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class Foo {
constructor(name){
this.name = name;
}
display(){
console.log(this.name);
}
}
var foo = new Foo('Saurabh');
foo.display = foo.display.bind(foo);
foo.display(); // Saurabh
var display = foo.display;
display(); // Saurabh
但考虑到 constructor 里面是做一些初始化操作的地方,在那里进行绑定是最理想最有效的地方。
为什么不用为箭头函数进行绑定呢?
我们可以使用 2 种使用箭头函数的写法来进行绑定:
公共类字段语法(实验功能)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class Foo extends React.Component{
handleClick = () => {
console.log(this);
}
render(){
return (
<button type="button" onClick={this.handleClick}>
Click Me
</button>
);
}
}
ReactDOM.render(
<Foo />,
document.getElementById("app")
);在回调中使用箭头函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class Foo extends React.Component{
handleClick(event){
console.log(this);
}
render(){
return (
<button type="button" onClick={(e) => this.handleClick(e)}>
Click Me
</button>
);
}
}
ReactDOM.render(
<Foo />,
document.getElementById("app")
);
两者都使用了 ES6 中引入的箭头函数。当时用这些方法的时候,事件处理器已经自动绑定到组件实例上了,我们就不用再在 constructor 里进行绑定。
原因是,在箭头函数中,this 是绑定在词法环境上的。这意味着,它使用封闭函数或全局范围的上下文作为它的值。
在公共类字段语法示例中,箭头函数包含在 Foo 类(或构造函数)中,因此上下文就是组件实例,这就是我们想要的结果。
长话短说,当我们在 React 的类组件中,像这样把事件处理函数作为回调传递的时候:1
<button type="button" onClick={this.handleClick}>Click Me</button>
这个函数丢失了自己隐式绑定的上下文。被触发的时候,this 就回退到了默认绑定,并被设置为 undefined,因为类声明和原型方法运行在严格模式下。
当我们在 constructor 里绑定了 this 后,就不用担心丢失上下文的问题了。
箭头函数则不受此行为的影响,因为他们使用词法环境绑定了 this。