深刻理解 React 中的上下文 this

寫在前面

JavaScript中的做用域scope 和上下文 context 是這門語言的獨到之處,每一個函數有不一樣的變量上下文和做用域。這些概念是JavaScript中一些強大的設計模式的後盾。在ES5規範裏,咱們能夠遵循一個原則——每一個function內的上下文this指向該function的調用方。好比:html

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 入門》 中,對箭頭函數的作了以下介紹:react

箭頭函數的基本介紹

ES6 容許使用「箭頭」=> 定義函數。git

var f = v => v;
//上面的箭頭函數等同於:
var f = function(v) {
  return v;
};
  • 若是箭頭函數不須要參數或須要多個參數,就使用一個圓括號表明參數部分es6

    var f = () => 5;
    // 等同於
    var f = function () { return 5 };
    var sum = (num1, num2) => num1 + num2;
    // 等同於
    var sum = function(num1, num2) {
      return num1 + num2;
    };
  • 若是箭頭函數的代碼塊部分多於一條語句,就要使用大括號將它們括起來,而且使用return語句返回(重要)github

    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;

    上面代碼只用了兩行,就定義了兩個簡單的工具函數。若是不用箭頭函數,可能就要佔用多行,並且還不如如今這樣寫醒目。app

  • 箭頭函數的一個用處是簡化回調函數

    // 正常函數寫法
    [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的代碼以下:

// 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,因此t一、t二、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 && _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>
    );
}

顯示效果以下:

這裏寫圖片描述

分析上面的代碼,有幾處容易出錯的地方:

  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

相關文章
相關標籤/搜索