React 开发中关于 this 的那些事儿

2,344 阅读7分钟

在React开发的这半年以来,关于this以及es6中方法的书写及其使用多多少少出现一些问题,期间也在Evernote上做过一些阶段性的总结,但陆续有遇到一些问题,导致我不得不写下这篇文章

  • 在es5中, 普通用function声明的函数是定义在window对象上的,显而易见其内部的this肯定是默认指向window对象。而直接用函数名来调用得到的函数内this也是window对象。可以这么理解,那个看不见的调用者就是window
  • 在es6中,为了规范function中this的所属问题,在使用‘use strict’模式下,不显示地用window对象调用函数则其内部的this为undefined,也就是强制你使用window对象严格得去调用。

关于bind

与this相关的 bind call apply 三兄弟

  1. bind通常用来重新绑定函数体中的this并返回一个具有指定this的函数。
  2. call 和 apply 则表示重新指定this并调用返回结果,区别在于call采用多个实参的方式传参,apply则是使用一个数组。

共同点: 在于第一个参数指定为this(这里用对象描述比较合理些),作用都是用来重定向this;从第二个参数起都算作是参数传递。
不同点: [1]是用来返回具有特定this的函数,[2]是用来调用函数的。而对于函数的获得,则通过其声明宿主(指所在对象,并非es6中的class类。在es6中,class和Java的类似,想要调用需要创建对象)

React中的bind

准确说,应该是es6中bind的使用,但因为在开发中常用到,就暂且以React开发为背景。
因为是返回指定this的函数,因此它的应用场景一定是函数的传递,而非函数的普通调用。在开发中,函数的传递赋值主要有以下几个场景(如有遗漏,欢迎补充):

  • 组件属性:组件内部函数的回调,通过把一个函数传递给组件的props,本质上和把函数作为实参传递给被调用函数是一致的。(至于是在Jsx中虚拟Dom中传递还是通过createElementcloneElement等React API就不多说了)
  • 高阶函数:本质上也是一种回调,就是在调用的地方提前写好即将被调用的函数并传进去,这里的‘即将’既可以是同步也可以是异步。
    在以上两种情况中,组件的属性对this的依赖还是比较大的

当我们给组件属性所传递的函数中,需要使用到当前类中非静态函数或字段(这两种成员都归属于this)时就不得不使用this来调用。

使用场景一

class Foo extends Component{
  render () {
    return 
  }
  readBook () {
    let bookName = this.getBookName()
    this.props.ooxx
    this.state.ooxx
  }
}
...

以上代码在运行时,由于this.readBook传递了一个纯函数,而该函数在CustomView类中执行时,this会被指向当前类CustomView的实例this,显而易见此时的foo、props、ooxx显然在另外一个类中是找不到的,undefined这个老朋友的如约而至也成为必然。

此时我们只需要改写传入方式为this.readBook.bind(this)即可将当前this绑定到readBook中,当然也可以绑定其他你所想要的,只要符合你的需求。

使用场景二

由于让函数调用bind会返回一个全新的函数,因此在高阶函数的某些场景下就会因为不是同一个函数(准确说是引用) 而导致出错,如监听器的注册与注销:

class PauseMenu extends Component {
  constructor (props) {
    super(props);
    this._onAppPaused = this.onAppPaused.bind(this); 
  } 
  componentWillMount () {
    AppStateIOS.addEventListener('change', this._onAppPaused); 
  }
  componentDidUnmount () {
    AppStateIOS.removeEventListener(
      'change', this._onAppPaused
    )
  }
  onAppPaused(event){ 
  }
}

可以看到我们在构造函数里为onAppPaused函数单独绑定了this并存到当前this当中,这样在注册和注销的时候都可以保证拿到的是同一个函数对象。(这里需要注意,_onAppPaused是直接定义在this上的,因此在针对一个class new出实例后,_onAppPaused是属于实例的,而onAppPaused是定义在类里的非静态函数,在类的prototype上,实例的_proto_原型上的。)

在bind时传参数

这个需求,就我个人来讲是一个非常不合理的需求。高阶函数本来就是传入一个方法签名,而你非要利用bind的附加参数功能有点‘乱来’的意思了。不过也不是不可以,只需要注意此时如果在高阶函数内(或组件内) 的方法回调中本身带有参数,则在bind时禁止附加参数,否则会覆盖原有的回调参数。

Lambda表达式与匿名函数

Lambda表达式是es6中函数的一种新的声明方式,如下:

() => {
  console.log(...)
}

以上是一个简单的Lambda表达式,在多数的Js教程中都被称作"箭头函数",它的书面写法上等价于匿名函数,如下:

function () {
  console.log(...)
}

他们都可以被用来在高阶函数以及组件属性上以及普通方法的定义,然而又有一些差异:

  • Lambda表达式中的this指向当前作用域的this,因此这种情况下就没有bind的事情了,因为Lambda已经有自己的this了。同bind一样,Lambda同样会返回一个全新的函数。

     this.props...} />

    此时呢,Lambda表达式是直接作为回调被赋值了。因此所有的回调参数都可以在此获取到,后续与this相关的逻辑代码都可以在Lambda表达式中书写。

  • 匿名函数会因为es5、6的情况而指向window或者undefined,此时如果方法体内需要用到当前的组件的this,则可以通过bind来完成,如:

    这样的代码等价于使用函数名bind this一样,如:

    
    ...
    readProps () {
      this.props...
    }

    如果上边的readProps的回调没有传出一些特别的参数,也可以用Lambda表达式改为:

     this.readProps()} />

    当然,这只是用箭头函数包裹了一下,我们仍然可以通过在箭头函数的参数列表中声明参数的方式来达到相同的目的。

使用Lambda表达式优化

之前提到了用bind在this中存留一份,可以在构造函数里用对象的属性来存储唯一的函数。

class PauseMenu extends Component {
  componentWillMount () {
    AppStateIOS.addEventListener('change', this._onAppPaused); 
  }
  componentDidUnmount () {
    AppStateIOS.removeEventListener(
      'change', this._onAppPaused
    )
  }
  _onAppPaused = (event) => { 
  };
}

这样做可以将_onAppPaused 作为类属性的一部分,就不用担心拿不到方法的唯一引用啦。其实这在React 的ES6中也是对this的一种提升方案,具体详情可以参考我翻译的这两篇文章:

Lambda表达式补充:

  1. 箭头后的部分即为表达式的返回值
  2. 如果出现多条语句操作运算的,需要使用{...} 以函数体的形式书写并以显示的方式return返回值
  3. 如果需要直接返回一个Object的话,语法上会和函数体冲突,此时就必须使用()保护一下,声明此处是一个对象而非函数体
  4. 参数方面,单参数可以不加()括弧, 但是一个以上就必须要加了。同时没有参数时需要使用()来完成语法上的补位

结语

在日常开发中,this还是一个让人比较头疼的东西。方法(对象中的函数) 内函数会导致this的重定向,其他的一些对象前套也会导致重定向问题,因此大家不要慌,不妨尝试手动获取一下外部this。

路漫漫其修远兮
吾将上下而求索
——《离骚》