React事件处理函数中的this为什么需要绑定?(否则会undefined)

2,527 阅读5分钟

起因

一个前端小伙伴给我看React事件处理函数的两种写法,问了一些问题,于是我查阅了一些文档...

左边是写React最常用的写法,我们都知道用途是使用箭头函数,默认绑定外层this,右边的写法乍一看还真有些复杂

想搞清楚的问题

  1. 为什么React需要绑定this(为什么不绑定会报错thisundefined)
  2. 绑定方法

知识点

首先复习几个知识点

构造函数、原型、实例三者之间的关系

// 构造函数
function Point(x, y) {
  this.x = x;
  this.y = y;
}
// 原型方法
Point.prototype.toString = function () {
  return '(' + this.x + ', ' + this.y + ')';
};
// new实例
var p = new Point(1, 2);

ES6 Class关键字

组合继承(构造函数+原型链)

ES6 的Class可以看作一个语法糖,写法让对象原型的写法更加清晰、更像面向对象编程的语法,上面的代码用 ES6 的Class改写,就是下面这样。

//定义类
class Point {
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}

Point类除了构造方法 constructor,还定义了一个toString方法。事实上,类的所有方法都定义在类的prototype属性上面。

class Point {
  constructor() {
    // ...
  }

  toString() {
    // ...
  }

  toValue() {
    // ...
  }
}

// 等同于

Point.prototype = {
  constructor() {},
  toString() {},
  toValue() {},
};

严格模式

类和模块的内部,默认就是严格模式,不需要使用use strict,严格模式下不会指定全局作用域的this

this指向

  • fn(),这里可以看成 window.fn(),因此 this === window
  • obj.fn(),便是 obj 调用了函数,既函数中的this === obj
  • 构造函数形式调用时,this为所生成的实例对象
  • apply,call,bind调用,this为指定的对象

三种方式修改 this 的指向

  • call: fn.call(target, 1, 2)指定函数中的this,并调用函数
  • apply: fn.apply(target, [1, 2])call,区别:以数组的形式获取参数
  • bind: fn.bind(target)(1,2)指定函数中的this,并返回函数,给一个函数永久绑定this值

箭头函数

特性之一:箭头函数会默认帮我们绑定外层this的值,所以在箭头函数中this的值和外层的this是一样的

this指针丢失问题

这是一个JavaScript语言的问题,和是否React无关。看个例子

let obj = {
    tmp:'Yes!',
    testLog:function(){
        console.log(this.tmp);
    }
};
obj.testLog(); // Yes!
// this指向obj,能够正常输出tmp属性;

修改一下代码:

let obj = {
    tmp:'Yes!',
    testLog:function(){
        console.log(this.tmp);
    }
};
let tmpLog = obj.testLog; // 中间变量
tmpLog(); // undefined
// 没有直接调用obj对象中的testLog方法,而是使用了一个tmpLog过渡
// 当调用tmpLog()时,方法中的this丢失了指向,默认指向window
// window.tmp未定义就是undefined;

在JavaScript中,如果你传递一个函数名给一个变量,然后通过在变量后加括号()来调用这个方法,此时方法内部的this的指向就会丢失。

React代码分析

React组件类中,使用了ES6的Class定义了一个组件类,当在其他组件中调用或者使用ReactDOM.render()方法将其渲染到界面时会生成一个组件实例。根据this指向的基本规则,这里的this最终会指向组件的实例。

class Button extends React.Component {
  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this);
  }
 
  handleClick(){
    console.log('this is:', this);
  }
 
  render() {
    return (
      <button onClick={this.handleClick}>
        Click me
      </button>
    );
  }
}

组件实例生成的时候,构造器constructor会被执行,此处着重分析一下这行代码:

this.handleClick = this.handleClick.bind(this)

赋值语句右侧的表达式先查找this.handleClick方法,此时的this指向新生成的实例,此处会查找到原型方法handleClick,接着执行bind(this)生成了一个指定了this的新方法,将新生成的函数赋值给实例的handleClick属性,由对象的赋值机制可知,此处的handleClick会直接作为实例属性生成。

总结:把原型方法handleClick()添加为实例方法handleClick(),并且指定这个方法中的this永久指向当前的实例。

当用户的点击动作发生时,调用onClick方法,由于onClick只是中间变量,所以处理函数handleClick中的this指向丢失。

Class的内部是强制运行在严格模式下的,不会指定thiswindow,而是undefined。因此,如果不进行this.handleClick = this.handleClick.bind(this);的话,handleClick方法执行时,内部的thisundefined,如若访问this的属性就会报错。

React绑定this的4种方法

  1. 在构造函数中使用bind绑定this
class Button extends React.Component {

  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this);
  }
  
  handleClick(){
    console.log('this is:', this);
  }
  
  render() {
    return (
      <button onClick={this.handleClick}>
        Click me
      </button>
    );
  }
}
  1. 在调用的时候使用bind绑定this
class Button extends React.Component {

  handleClick(){
    console.log('this is:', this);
  }
 
  render() {
    return (
      <button onClick={this.handleClick.bind(this)}>
        Click me
      </button>
    );
  }
}
  1. 在调用的时候使用箭头函数绑定this
class Button extends React.Component {
 
  handleClick(){
    console.log('this is:', this);
  }
 
  render() {
    return (
      <button onClick={()=>this.handleClick()}>
        Click me
      </button>
    );
  }
}
  1. 使用属性初始化器语法绑定this
class Button extends React.Component {
 
  handleClick=()=>{
    console.log('this is:', this);
  }
 
  render() {
    return (
      <button onClick={this.handleClick}>
        Click me
      </button>
    );
  }
}

方式2和方式3都是在调用的时候再绑定,优点:写法比较简单,当组件中没有state的时候就不需要添加类构造函数来绑定this

缺点:每一次调用都会生成一个新的方法实例,因此对性能有影响,并且当这个函数作为属性值传入低阶组件的时候,这些组件可能会进行额外的重新渲染,因为每一次都是新的方法实例作为的新的属性传递。

方式1和方式4是一次绑定,多次调用

方式4:将方法初始化为箭头函数,因此在定义函数的时候就绑定了this,不需要在类构造函数中绑定,调用的时候不需要再作绑定。结合了方式1、方式2、方式3的优点,但是需要用babel转译

小结

方式1是官方推荐的绑定方式,也是性能最好的方式。方式2和方式3会有性能影响并且当方法作为属性传递给子组件的时候会引起重渲问题。方式4是最好的绑定方式,需要结合bable转译

只要是需要在调用的地方传参,就必须在事件调用的地方使用bind或者箭头函数,这个没有什么解决方案。