深入理解React中的上下文this

来源:互联网 发布:淘宝店铺怎么注册 编辑:程序博客网 时间:2024/06/06 12:30

写在前面

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()

但是,在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);

箭头函数使用注意点

(1)函数体内的 this 对象,就是定义时所在的对象,而不是使用时所在的对象。

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

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

(4)不可以使用 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的代码如下:

// ES6function foo() {  setTimeout(() => {    console.log('id:', this.id);  }, 100);}// ES5function 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: 1var t2 = f().call({id: 3})(); // 输出 id: 1var 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对象的写法( call 、 apply 、 bind )。但是,箭头函数并不适用于所有场合,所以ES7提出了“函数绑定”( function bind )运算符,用来取代 call 、 apply 、 bind 调用。虽然该语法还是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 && _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

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

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

从这个例子我们可以发现:只要不采用 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>        );    }

显示效果如下:

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

(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 入门》

0 0
原创粉丝点击