如果你尝试使用过 React 进行前端开发, 一定见过下面这样的代码:
- // 假想定义一个 ToggleButton 开关组件
- class ToggleButton extends React.Component{ constructor(props){
- super(props);
- this.state = {isToggleOn: true};
- this.handleClick = this.handleClick.bind(this);
- this.handleChange = this.handleChange.bind(this);
- }
- handleClick(){
- this.setState(prevState => ({
- isToggleOn: !preveState.isToggleOn
- }));
- }
- handleChange(){
- console.log(this.state.isToggleOn);
- }
- render(){
- return(
- <button onClick={this.handleClick} onChange={this.handleChange}>
- {this.state.isToggleOn ? 'ON':'OFF'}
- </button>
- )
- }
- }
构造方法中为什么要给所有的实例方法绑定 this 呢?
1. 代码执行的细节
上例仅仅是一个组件类的定义, 当在其他组件中调用或是使用 ReactDOM.render( )方法将其渲染到界面上时会生成一个组件的实例, 因为组件是可以复用的, 面向对象的编程方式非常适合它的定位. 根据 this 指向的基本规则就可以知道, 这里的 this 最终会指向组件的实例.
组件实例生成的时候, 构造器 constructor 会被执行, 此处着重分析一下下面这行代码:
this.handleClick = this.handleClick.bind(this);
此时的 this 指向新生成的实例, 那么赋值语句右侧的表达式先查找
this.handleClick( )
这个方法, 由对象的属性查找机制 (沿原型链由近及远查找) 可知此处会查找到原型方法
this.handleClick( )
, 接着执行 bind(this), 此处的 this 指向新生成的实例, 所以赋值语句右侧的表达式计算完成后, 会生成一个指定了 this 的新方法, 接着执行赋值操作, 将新生成的函数赋值给实例的 handleClick 属性, 由对象的赋值机制可知, 此处的 handleClick 会直接作为实例属性生成. 总结一下, 上面的语句做了一件这样的事情:
把原型方法 handleClick( )改变为实例方法 handleClick( ), 并且强制指定这个方法中的 this 指向当前的实例.
2. ES5 的写法中为什么不用 bind(this)?
ES5 的写法是指使用
React.createClass( )
方法来定义组件, React 在 V16 以上的新版本中已经移除了这个 API, 你可以通过阅读更早版本的源代码看到这个方法的细节.
- // 旧版本 `react` 中 `createClass` 方法片段
- if (this.__reactAutoBindMap) {
- this._bindAutoBindMethods();
- }
在老版本的 React 中, createClass()的定义中可以看到上面的代码, 抛开其他复杂的逻辑, 从方法名就可以看出这是一个自动绑定的方法, 实际上在这个方法中所完成的, 就是对组件中自定义方法的 this 强制绑定, 感兴趣的读者可以自行翻看源码了解细节.
3. 绑定 this 的必要性
在组件上绑定事件监听器, 是为了响应用户的交互动作, 特定的交互动作触发事件时, 监听函数中往往都需要操作组件某个状态的值, 进而对用户的点击行为提供响应反馈, 对开发者来说, 这个函数触发的时候, 就需要能够拿到这个组件专属的状态合集(例如在上面的开关组件 ToggleButton 例子中, 它的内部状态属性 state.isToggleOn 的值就标记了这个按钮应该显示 ON 或者 OFF), 所以此处强制绑定监听器函数的 this 指向当前实例的也很容易理解.
React 构造方法中的 bind 会将响应函数与这个组件 Component 进行绑定以确保在这个处理函数中使用 this 时可以时刻指向这一组件的实例.
4. 如果不绑定 this
如果类定义中没有绑定 this 的指向, 当用户的点击动作触发
this.handleClick( )
这个方法时, 实际上执行的是原型方法, 可这样看起来并没有什么影响, 如果当前组件的构造器中初始化了 state 这个属性, 那么原型方法执行时, this.state 会直接获取实例的 state 属性, 如果构造其中没有初始化 state 这个属性(比如 React 中的 UI 组件), 说明组件没有自身状态, 此时即使调用原型方法似乎也没什么影响.
事实上的确是这样, 这里的 bind(this)所希望提前规避的, 就是著名的 this 指针丢失的问题.
例如使用解构赋值的方式获取某个属性方法时, 就会造成引用转换丢失 this 的问题:
- const toggleButton = new ToggleButton();
- import {handleClick} = toggleButton;
上例中解构赋值获取到的 handleClick 这个方法在执行时就会报错, Class 的内部是强制运行在严格模式下的, 此处的 this 在赋值中丢失了原有的指向, 在运行时指向了 undefined, 而 undefined 是没有属性的.
另一个存在的限制, 是没有绑定 this 的响应函数在异步运行时可能会出问题, 当它作为回调函数被传入一个异步执行的方法时, 同样会因为丢失了 this 的指向而引发错误.
如果没有强制指定组件实例方法的 this, 在将来的使用中就无法安心使用引用转换或作为回调函数传递这样的方式, 对于后续使用和协作开发而言都是不方便的.
5. 小结
this 的使用非常灵活, 但这种灵活性也带来了很多混乱. 这里的 bind(this)是为了改进 javascript 语言级的缺陷, 并不是只有 React 中才需要这样做, 这个问题是伴随着面向对象编程而产生的, 在使用 javascript 进行插件和框架的开发时, 这个问题的影响会更加明显. 之所以说它是一个语言级的缺陷, 是因为 Java 中对于 this 在同样场景下的指向更符合正常思维逻辑, 而 javascript 中如果不显示绑定, 就会出现语言运行结果和方法的命名表意不一致的情况.
关于 this 更详尽的分析, 可以参考笔者的博文javascript 基础修炼系列 - What's this上下篇(链接地址 http://3ms.huawei.com/km/blogs/details/5580055 ).
参考
[1]ES6-Class 基本语法https://www.cnblogs.com/ChenChunChang/p/8296350.html
来源: https://www.cnblogs.com/dashnowords/p/9343383.html