深入理解 React 中的上下文 this

发布时间:2019-08-10 发布网站:脚本宝典
脚本宝典收集整理的这篇文章主要介绍了深入理解 React 中的上下文 this脚本宝典觉得挺不错的,现在分享给大家,也给大家做个参考。
@H_777_1@写在前面

JavaScript中的作用域scoPE 和上下文 context 是这门语言的独到之处,每个函数有不同的变量上下文和作用域。这些概念是JavaScript中一些强大的设计模式的后盾。在ES5规范里,我们可以遵循一个原则——每个function内的上下文this指向该function的调用方。比如:

VAR Module = {
    name: 'Jafeney',
    First: function() {
        console.LOG(this);   // this对象指向调用该方法的Module对象
        var second = (function() {
            console.log(this)  // 由于变量提升,this对象指向Window对象
        })()
    },
    inIT: function() {
        this.first()
    }
}

Module.init()

深入理解 React 中的上下文 this

但是,在ES6规范中,出现了一个逆天的箭头操作符 => ,它可以替代原先ES5里function的作用,快速声明函数。那么,在没有了function关键字,箭头函数内部的上下文this怎样一种情况呢?

ES6 中的箭头函数

在阮一峰老师的《ECMAScript 6 入门》 中,对箭头函数的做了如下介绍:

箭头函数的基本介绍

ES6 允许使用“箭头”=> 定义函数。

var f = v => v;
//上面的箭头函数等同于:
var f = function(v) {
  return v;
};
  • 如果箭头函数不需要参数或需要多个参数,就使用一个括号代表参数部分

    var f = () => 5;
    // 等同于
    var f = function () { return 5 };
    var sum = (num1, num2) => num1 + num2;
    // 等同于
    var sum = function(num1, num2) {
      return num1 + num2;
    };
  • 如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回(重要

    var sum = (num1, num2) => { return num1 + num2; }
  • 由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号(重要)

    var getTempItem = id => ({ id: id, name: "Temp" });
  • 箭头函数可以与变量解构结合使用

    const full = ({ first, last }) => first + ' ' + last;
    // 等同于
    function full(person) {
      return person.first + ' ' + person.last;
    }
  • 箭头函数使得表达更加简洁

    const isEven = n => n % 2 == 0;
    const square = n => n * n;

    上面代码只用了两行,就定义了两个简单的工具函数。如果不用箭头函数,可能就要占用多行,而且还不如现在这样写醒目。

  • 箭头函数的一个用处是简化回调函数

    // 正常函数写法
    [1,2,3].map(function (x) {
      return x * x;
    });
    
    // 箭头函数写法
    [1,2,3].map(x => x * x);

箭头函数使用注意点

@H_923_360@
  • 函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。

  • 不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。

  • 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用Rest参数代替。

  • 不可以使用yield命令,因此箭头函数不能用作Generator函数。

  • this 指向固定化

    ES5规范中,this对象的指向是可变的,但是在ES6的箭头函数中,它却是固定的。

    function foo() {
      setTimeout(() => {
        console.log('id:', this.id);
      }, 100);
    }
    
    var id = 21;
    
    foo.call({ id: 42 });   // 输出 id: 42

    注意:上面代码中,setTimeout的参数是一个箭头函数,这个箭头函数的定义生效是在foo函数生成时,而它的真正执行要等到100毫秒后。如果是普通函数,执行时this应该指向全局对象window,这时应该输出21。但是,箭头函数导致this总是指向函数定义生效时所在的对象(本例是{id: 42}),所以输出的是42。

    箭头函数的原理

    this指向的固定化,并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数。所以,箭头函数转成ES5的代码如下:

    // ES6
    function foo() {
      setTimeout(() => {
        console.log('id:', this.id);
      }, 100);
    }
    
    // ES5
    function foo() {
      var _this = this;
    
      setTimeout(function () {
        console.log('id:', _this.id);
      }, 100);
    }

    上面代码中,转换后的ES5版本清楚地说明了,箭头函数里面根本没有自己的this,而是引用外层的this

    两道经典的面试题

    // 请问下面有几个this 
    
    function foo() {
      return () => {
        return () => {
          return () => {
            console.log('id:', this.id);
          };
        };
      };
    }
    
    var f = foo.call({id: 1});
    
    var t1 = f.call({id: 2})()(); // 输出 id: 1
    var t2 = f().call({id: 3})(); // 输出 id: 1
    var t3 = f()().call({id: 4}); // 输出 id: 1

    上面代码之中,其实只有一个this,就是函数foo的this,所以t1、t2、t3都输出同样的结果。因为所有的内层函数都是箭头函数,都没有自己的this,它们的this其实都是最外层foo函数的this。另外,由于箭头函数没有自己的this,所以也不能用call()apply()bind()这些方法去改变this的指向

    // 请问下面代码执行输出什么
    
    (function() {
      return [
        (() => this.x).bind({ x: 'inner' })()
      ];
    }).call({ x: 'outer' });

    上面代码中,箭头函数没有自己的this,所以bind方法无效,内部的this指向外部的this。所以上面的代码最终输出 ['outer']

    函数绑定 ::

    箭头函数可以绑定this对象,大大减少了显式绑定this对象的写法(callapplybind)。但是,箭头函数并不适用于所有场合,所以es7提出了“函数绑定”(function bind运算符,用来取代callapplybind调用。虽然该语法还是ES7的一个提案,但是Babel转码器已经支持。

    函数绑定运算符是并排的两个双冒号(::),双冒号左边是一个对象,右边是一个函数。该运算符会自动将左边的对象,作为上下文环境(即this对象),绑定到右边的函数上面。

    foo::bar;
    // 等同于
    bar.bind(foo);
    
    foo::bar(...arguments);
    // 等同于
    bar.apply(foo, arguments);
    
    const hasOwnPRoperty = Object.prototype.hasOwnProperty;
    function hasOwn(obj, key) {
      return obj::hasOwnProperty(key);
    }

    如果双冒号左边为空,右边是一个对象的方法,则等于将该方法绑定在该对象上面。

    var method = obj::obj.foo;
    // 等同于
    var method = ::obj.foo;
    
    let log = ::console.log;
    // 等同于
    var log = console.log.bind(console);

    由于双冒号运算符返回的还是原对象,因此可以采用链式写法。

    // 例一
    import { map, takeWhile, forEach } From "iterlib";
    
    getplayers()
    ::map(x => x.character())
    ::takeWhile(x => x.strength > 100)
    ::forEach(x => console.log(x));
    
    // 例二
    let { find, html } = jake;
    
    document.querySelectorAll("div.myClass")
    ::find("p")
    ::html("hahaha");

    React 中的各种 this

    目前React的编写风格已经全面地启用了ES6和部分ES7规范,所以很多ES6的坑在React里一个个浮现了。本篇重点介绍 this,也是近期跌得最疼的一个。

    component 方法内部的 this

    还是用具体的例子来解释吧,下面是我 Royal 项目里一个Table组件(Royal正在开发中,欢迎fork贡献代码 ^_^)

    import React, { Component } from 'react'
    import Checkbox from '../../FormControls/Checkbox/' 
    import './style.less'
    
    class Table extends Component {
        constructor(props) {
            super(props)
            this.state = {
                dataSource: props.dataSource || [],
                columns: props.columns || [],
                wrapClass: props.wrapClass || null,
                wrapStyle: props.wrapStyle || null,
                style: props.style || null,
                classname: props.className || null,
            }
            this.renderRow = props.renderRow || null
        }
    
        onSelectAll() {
            for (let ref in this.refs) {
                if (ref!=='selectAll') {
                    this.refs[ref].setState({checked:true})
                }
            }
        }
    
        offSelectAll() {
            for (let ref in this.refs) {
                if (ref!=='selectAll') {
                    this.refs[ref].setState({checked:false})
                }
            }
        }
    
        _renderHead() {
            return this.state.columns.map((item,i) => {
                return [<th>{i===0?<Checkbox ref="selectAll" onConfirm={()=>this.onSelectAll()} onCancel={()=>this.offSelectAll()} />:''}{item.title}</th>]
            })
        }
    
        _renderBody() {
            let _renderRow = this.renderRow;
            return this.state.dataSource.map((item) => {
                return _renderRow &amp;& _renderRow(item)
            })
        }
    
        render() {
            let state = this.state;
            return (
                <div className={state.wrapClass} style={state.wrapStyle}>
                    <table
                        border="0"
                        style={state.style}
                        className={"ry-table " + (state.className && state.className : "")}>
                        <thead>
                            <tr>{this._renderHead()}</tr>
                        </thead>
                        <tbody>
                            {this._renderBody()}
                        </tbody>
                    </table>
                </div>
            )
        }
    }
    
    export default Table

    ComponentReact内的一个基类,用于继承和创建React自定义组件。ES6规范下的面向对象实现起来非常精简,class关键字 可以快速创建一个类,而Component类内的所有属性和方法均可以通过this访问。换而言之,在Component内的任意方法内,可以通过this.xxx的方式调用该Component的其他属性和方法。

    接着分析上面的代码,寥寥几行实现的是对一个Table组件的封装,借鉴了ReactNative组件的设计思路,通过外部传递dataSource(数据)、columns(表格的表头项)、renderRow(当行渲染的模板函数)来完成一个Table的构建,支持全选和取消全选的功能、允许外部传递classNamestyle对象来修改样式。

    从这个例子我们可以发现:只要不采用function定义函数,Component所有方法内部的this对象始终指向该类自身。

    container 调用 component 时传递的 this

    还是继续上面的例子,下面在一个做为Demo的container里调用之前 的Table

    import Table from '../../components/Views/Table/' 

    接着编写renderRow函数并传递给Table组件

    _renderRow(row) {
        // ------------ 注意:这里对callback函数的写法 -----------
        let onEdit = (x)=> {
            console.log(x+x)
        }, onDelete = (x)=> {
            console.log(x*x)
        }
        // ---------------------------------------------------
        return (
            <tr>
                <td><Checkbox ref={"item_" + row.key} />{row.key}</td>
                <td>{row.name}</td>
                <td>{row.age}</td>
                <td>{row.birthday}</td>
                <td>{row.job}</td>
                <td>{row.address}</td>
                <td>
                    <Button type="Primary" callback={()=>onEdit(row.key)} text="编辑" />
                    <Button type="secondary" callback={()=>onDelete(row.key)} text="删除" />
                </td>
            </tr>
        )
    }
    
    //... 省略一大堆代码
    
    render() {
        let dataSource = [{
            key: '1',
            name: '胡彦斌',
            age: 32,
            birthday: '2016-12-29',
            job: '前端工程师',
            address: '西湖区湖底公园1号'
            }, {
            key: '2',
            name: '胡彦祖',
            age: 42,
            birthday: '2016-12-29',
            job: '前端工程师',
            address: '西湖区湖底公园1号'
        }],columns = [{
            title: '编号',
            dataIndex: 'key',
            key: 'key',
            },{
            title: '姓名',
            dataIndex: 'name',
            key: 'name',
            }, {
            title: '年龄',
            dataIndex: 'age',
            key: 'age',
            }, {
            title: '生日',
            dataIndex: 'birthday',
            key: 'birthday',
            }, {
            title: '职务',
            dataIndex: 'job',
            key: 'job',
            },{
            title: '住址',
            dataIndex: 'address',
            key: 'address',
            }, {
            title: '操作',
            dataIndex: 'operate',
            key: 'operate',
        }];
        return (
            <div>
                <Table dataSource={dataSource} columns={columns} renderRow={this._renderRow}/>
            </div>
        );
    }

    显示效果如下:

    深入理解 React 中的上下文 this

    分析上面的代码,有几处容易出错的地方:

    1. _renderRow 作为component的方法来定义,然后在对应的render函数内通过this来调用。很重要的一点,这里this._renderRow作为的是函数名方式传递。

    2. _renderRow 内部Button组件的callback按钮点击后触发的回调,也是一个函数,但是这个函数没有像上面一样放在component的方法里定义,而是作为一个变量定义并通过匿名函数的方式传递给子组件:

      let onEdit = (x)=> {
          console.log(x+x)
      }
      
      // .....
      callback={()=>onEdit(row.key)}

      这样就避开了使用this时上下文变化的问题。这一点是很讲究的,如果沿用上面的写法很容易这样写:

      onEdit(x) {
         console.log(x+x)
      }
      
      // ... 
      callback={()=>this.onEdit(row.key)}

      但是很遗憾,这样写this传递到子组件后会变成undefined,从而报错。

    3. 父组件如要调用子组件的方法,有两种方式:

      • 第一种 通过匿名函数的方式

        callback = {()=>this.modalShow()}
      • 第二种 使用 bind

        callback = {this.modalShow.bind(this)}

    注意:如果要绑定的函数需要传参数,可以这么写: xxx.bind(this,arg1,arg2...)

    参考

    《ECMAScript 6 入门》


    @欢迎关注我的 github个人博客 -Jafeney

    脚本宝典总结

    以上是脚本宝典为你收集整理的深入理解 React 中的上下文 this全部内容,希望文章能够帮你解决深入理解 React 中的上下文 this所遇到的问题。

    如果觉得脚本宝典网站内容还不错,欢迎将脚本宝典推荐好友。

    本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
    如您有任何意见或建议可联系处理。小编QQ:384754419,请注明来意。