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
是怎樣一種狀況呢?前端
在阮一峯老師的《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);
函數體內的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
,因此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對象的寫法(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");
this
目前React
的編寫風格已經全面地啓用了ES6和部分ES7規範,因此不少ES6的坑在React
裏一個個浮現了。本篇重點介紹 this
,也是近期跌得最疼的一個。
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
對象始終指向該類自身。
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> ); }
顯示效果以下:
分析上面的代碼,有幾處容易出錯的地方:
_renderRow
做爲component
的方法來定義,而後在對應的render
函數內經過this
來調用。很重要的一點,這裏this._renderRow
做爲的是函數名方式傳遞。
_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
,從而報錯。
父組件如要調用子組件的方法,有兩種方式:
第一種 經過匿名函數的方式
callback = {()=>this.modalShow()}
第二種 使用 bind
callback = {this.modalShow.bind(this)}
注意:若是要綁定的函數須要傳參數,能夠這麼寫:
xxx.bind(this,arg1,arg2...)
@歡迎關注個人 github
和 我的博客 -Jafeney