在[上一篇文章][]中,咱們提到 ES6 的 class
語法糖是個近乎完美的方案,而且講解了實現繼承的許多內部機制,如 prototype
/__proto__
/constructor
等等。這篇,咱們就以實際的 babel 代碼爲例子,來驗證上節所言不虛。此外,本文還解釋了 React 組件中你須要 bind
一下類方法的原理所在。javascript
class
+ 字段聲明class
+ 方法聲明class
+ 字段聲明先來看個最簡單的例子,咱們僅僅使用了 class
關鍵字並定義了一個變量:java
class Animal { constructor(name) { this.name = name || 'Kat' } }
最後 babel 編譯出來的代碼以下。這裏筆者用的是 Babel 6 的穩定版 6.26,不一樣版本編譯出來可能有差別,但不至於有大的結構變更。react
'use strict' function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function') } } var Animal = function Animal(name) { _classCallCheck(this, Animal) this.name = name || 'Kat' }
確實十分簡單,對吧。這段代碼值得留意的點有兩個:express
一個是,使用 class
聲明的 Animal
最後實際上是被編譯爲一個函數。證實 class
跟類不要緊,只是個語法糖。數組
另外一個地方是,編譯器幫咱們插入了一個 _classCallCheck
函數調用,它會檢查你有沒有用 new Animal()
操做符來初始化這個函數。如有,則 this
會是被實例化的 Animal
對象,天然能經過 animal instanceof Animal
檢查;如果直接調用函數,this
會被初始化爲全局對象,天然不會是 Animal
實例,從而拋出運行時錯誤。這個檢查,正解決了[上一篇文章][]提到的問題:若是忘記使用 new
去調用一個被設計構造函數的函數,沒有任何運行時錯誤的毛病。babel
class
+ 方法聲明讓咱們再擴展一下例子,給它加兩個方法。閉包
class Animal { constructor(name) { this.name = name || 'Kat' } move() {} getName() { return this.name } }
'use strict' var _createClass = (function() { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i] descriptor.enumerable = descriptor.enumerable || false descriptor.configurable = true if ('value' in descriptor) descriptor.writable = true Object.defineProperty(target, descriptor.key, descriptor) } } return function(Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps) if (staticProps) defineProperties(Constructor, staticProps) return Constructor } })() function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function') } } var Animal = (function() { function Animal(name) { _classCallCheck(this, Animal) this.name = name || 'Kat' } _createClass(Animal, [ { key: 'move', value: function move() {}, }, { key: 'getName', value: function getName() { return this.name }, }, ]) return Animal })()
例子長了很多,但其實主要的變化只有兩個:一是 Animal
被包了一層而不是直接返回;二是新增的方法 move
和 getName
是經過一個 _createClass()
方法來實現的。它將兩個方法以 key
/value
的形式做爲數組傳入,看起來,是要把它們設置到 Animal
的原型鏈上面,以便後續繼承之用。函數
爲啥 Animal
被包了一層呢,這是個好問題,但答案咱們將留到後文揭曉。如今,咱們先看一下這個長長的 _createClass
實現是什麼:post
var _createClass = (function() { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i] descriptor.enumerable = descriptor.enumerable || false descriptor.configurable = true if ('value' in descriptor) descriptor.writable = true Object.defineProperty(target, descriptor.key, descriptor) } } return function(Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps) if (staticProps) defineProperties(Constructor, staticProps) return Constructor } })()
它是個當即執行函數,執行又返回了另外一個函數。說明啥,必定用了閉包,說明裏面要封裝些「私有」變量,那就是 defineProperties
這個函數。這很好,一是這個函數只會生成一次,二是明確了這個函數只與 _createClass
這個事情相關。ui
再細看這個返回的函數,接受 Constructor
、protoProps
和 staticProps
三個參數。staticProps
咱們暫時不會用到,回頭再講;咱們傳入的數組是經過 protoProps
接受的。接下來,看一下 defineProperties
作了啥事。
它將每個傳進來的 props 作了以下處理:分別設置了他們的 enumerable
、configurable
、writable
屬性。而傳進來的 target
是 Animal.prototype
,至關於,這個函數最後的執行效果會是這樣:
function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { // 前面處理其實獲得這樣這個 descriptor 對象: var descriptor = { ...props[i], enumerable: false, configurable: true, writable: true, } Object.defineProperty(target, descriptor.key, descriptor) } }
看到這裏就很明白了,它就是把你定義的 move
、getName
方法經過 Object.defineProperty
方法設置到 Animal.prototype
上去。前面咱們說過,prototype
是用來存儲公共屬性的。也就是說,這兩個方法在你使用繼承的時候,能夠被子對象經過原型鏈上溯訪問到。也就是說,咱們這個小小的例子裏,聲明的兩個方法已經具有了繼承能力了。
至於 enumerable
、configurable
、writable
屬性是什麼東西呢,查一下語言規範就知道了。簡單來講,writable
爲 false
時,其值不能經過 setter
改變;enumerable
爲 false
時,不能出如今 for-in
循環中。固然,這裏是粗淺的理解,暫時不是這篇文章的重點。
class Animal { constructor(name) { this.name = name || 'Kat' } } class Tiger extends Animal { constructor(name, type) { super(name) this.type = type || 'Paper' } }
加一層繼承和字段覆蓋能看到啥東西呢?能看到繼承底下的實現機制是怎麼樣的,以及它的 constructor
和 __proto__
屬性將如何被正確設置。帶着這兩個問題,咱們一塊兒來看下編譯後的源碼:
'use strict' function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError( "this hasn't been initialised - super() hasn't been called" ) } return call && (typeof call === 'object' || typeof call === 'function') ? call : self } function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError( 'Super expression must either be null or a function, not ' + typeof superClass ) } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true, }, }) if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : (subClass.__proto__ = superClass) } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function') } } var Animal = function Animal(name) { _classCallCheck(this, Animal) this.name = name || 'Kat' } var Tiger = (function(_Animal) { _inherits(Tiger, _Animal) function Tiger(name, type) { _classCallCheck(this, Tiger) var _this = _possibleConstructorReturn( this, (Tiger.__proto__ || Object.getPrototypeOf(Tiger)).call(this, name) ) _this.type = type || 'Paper' return _this } return Tiger })(Animal)
相比無繼承的代碼,這裏主要增長了幾個函數。_possibleConstructorReturn
顧名思義,可能不是很重要,回頭再讀。精華在 _inherits(Tiger, Animal)
這個函數,咱們按順序來讀一下。
function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError( 'Super expression must either be null or a function, not ' + typeof superClass ) } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true, }, }) if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : (subClass.__proto__ = superClass) }
首先是一段異常處理,簡單地檢查了 superClass
要麼是個函數,要麼得是個 null。也就是說,若是你這樣寫那是不行的:
const Something = 'not-a-function' class Animal extends Something {} // Error: Super expression must either be null or a function, not string
接下來這句代碼將 prototype
和 constructor
一併設置到位,是精華。注意,這個地方留個問題:爲何要用 Object.create(superClass.prototype)
,而不是直接這麼寫:
function _inherits(subClass, superClass) { subClass.prototype = superClass && superClass.prototype subClass.prototype.constructor = { ... } }
很明顯,是爲了不任何對 subClass.prototype
的修改影響到 superClass.prototype
。使用 Object.create(asPrototype)
出來的對象,其實上是將 subClass.prototype.__proto__ = superClass.prototype
,這樣 subClass
也就繼承了 superClass
,能夠達到這樣兩個目的:
superClass.prototype
原型上發生的修改都能實時反映到 subClass
的實例上subClass.prototype
上的任何修改不會影響到 superClass.prototype
最後,若是 superClass
不爲空,那麼將 subClass.__proto__
設置爲 superClass
。這是爲了繼承 superClass
的靜態方法和屬性。如如下的例子中,Cat.TYPE
能獲取到 Animal.TYPE
:
class Animal { static TYPE = 'PAPER' static createTyping() { return Animal.TYPE } } class Cat extends Animal {} console.log(Cat.TYPE) // PAPER console.log(Cat.createTyping()) // PAPER
至此,一個簡單的繼承就完成了。在使用了 extends
關鍵字後,實際上背後發生的事情是:
prototype
上的 __proto__
被正確設置,指向父「類」的 prototype
: subClass.prototype = { __proto__: superClass.prototype }
prototype
上的 constructor
被正確初始化,這樣 instanceof
關係能獲得正確結果__proto__
被指向父「類」,這樣父「類」上的靜態字段和方法能被子「類」繼承好,要點看完了。後面內容跟繼承關係不大,但既然源碼扒都扒了,咱們不妨繼續深刻探索一些場景:
看一個簡單的代碼:
class Animal { static create() { return new Animal() } }
首先要知道,這個「靜態」一樣不是強類型類繼承語言裏有的「靜態」的概念。所謂靜態,就是說它跟實例是不要緊的,而跟「類」自己有關係。好比,你能夠這樣調用:Animal.create()
,但不能這樣用:new Animal().create
。什麼場景下會用到這種模式呢?好比說:
Object.create
、Object.keys
等經常使用方法既然只有經過構造函數自己去調用,而不能經過實例來調用,指望它們被綁定到函數自己上彷佛很天然。咱們來看看上面這段代碼將被如何編譯:
'use strict' var _createClass = (function() { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i] descriptor.enumerable = descriptor.enumerable || false descriptor.configurable = true if ('value' in descriptor) descriptor.writable = true Object.defineProperty(target, descriptor.key, descriptor) } } return function(Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps) if (staticProps) defineProperties(Constructor, staticProps) return Constructor } })() function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function') } } var Animal = (function() { function Animal() { _classCallCheck(this, Animal) } _createClass(Animal, null, [ { key: 'create', value: function create() {}, }, ]) return Animal })()
熟悉的函數,熟悉的配方。與本文的第二個例子相比,僅有一個地方的不一樣:create
方法是做爲 _createClass
方法的第三個參數被傳入的,這正是咱們上文提到的 staticProps
參數:
var _createClass = (function() { function defineProperties(target, props) { ... } return function(Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps) if (staticProps) defineProperties(Constructor, staticProps) return Constructor } })() _createClass(Animal, null, [ { key: 'create', value: function create() {}, }, ])
能夠看見,create
方法是直接被建立到 Animal
上的:defineProperties(Animal, [{ key: 'create', value: function() {} }])
,最終會將函數賦給 Animal.create
。咱們的猜想並無錯誤。
class Tiger { static TYPE = 'REAL' }
還有個小例子。若是是靜態變量的話,一樣由於不但願在實例對象上所使用,咱們會看到編譯出來的代碼中它是直接被設置到函數上。代碼已經很熟悉,沒必要再講。
'use strict' function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function') } } var Tiger = function Tiger() { _classCallCheck(this, Tiger) } Tiger.TYPE = 'REAL'
有趣的是,靜態變量會不會被「子類」繼承呢?這個可請讀者本身作個實驗,驗證驗證。
寫 React 的東西,必定碰見過這個問題:
class Button extends React.Component { constructor() { super() this.state = { isToggleOn: true, } // 畫重點 👇👇👇👇👇👇👇👇👇👇👇👇 // this.toggleButton = this.toggleButton.bind(this) } static propTypes = { text: PropTypes.string, } // ❌❌❌ Uncaught TypeError: this.setState is not a function toggleButton() { this.setState({ isToggleOn: !this.state.isToggleOn, }) } render() { return <button onClick={this.toggleButton}>Toggle Me</button> } }
爲何會有這個問題呢?由於你扔進去的 this.toggleButton
函數,在 button
內部必定是經過 onClick()
這樣的方式來調用的,這樣的話,this
引用就會丟失爲 undefined
,那麼 React.Component
上的 setState
就調用不到。
能夠直接去 React 官方示例看看:https://codepen.io/gaearon/pe...
class Button extends React.Component { ... // ✅✅✅ This will work! toggleButton = () => { this.setState({ ... }) } ... }
解決方案呢,天然也有不少種,好比引用 @autobind
、使用 ES7 的 ::this.toggleButton
、使用箭頭函數等。好比上面 👆 這種最經常使用的解決方案。那麼同窗們有沒有想過這個問題,爲何這樣寫 this
應用就能夠正確拿到呢?「由於箭頭函數將 this
綁定到詞法做用域的上下文中了呀~」那誰來給我解釋一下這句話呢?反正我是歷來沒理解過這個「外層」的做用域,應該是綁定到哪裏。所以,只好另闢路徑,直接看源碼來理解這個寫法的含義。
我寫了個簡單的例子,足以復現這個問題:
class Button { constructor() { this.value = 1 } increment = () => { this.value += 2 } render() { const onClick = this.increment onClick() } }
當咱們調用 render()
時,increment()
這樣的調用方式會使 this
引用沒法被初始化,這也正是咱們傳入的 onClick
在 React 中會被調用的方式。而上圖的 increment
寫法能夠從新拯救失去的 this
引用!讓咱們來看看源代碼,一探究竟。
'use strict' var _createClass = (function() {})() function _classCallCheck(instance, Constructor) {} var Button = (function() { function Button() { var _this = this _classCallCheck(this, Button) this.increment = function() { _this.value += 2 } this.value = 1 } _createClass(Button, [ { key: 'render', value: function render() { var increment = this.increment increment() }, }, ]) return Button })()
我略去了你們耳熟能詳的代碼,只留下關鍵的部分。能夠看到,編譯後的代碼中,對 Button
實例的 this
引用被閉包保存了下來!這種寫法,與之前咱們 var that = this
的寫法是一致的,我也終於理解「再也不須要 that 引用了」以及各類語焉不詳的做用域啊最外層變量啊這些理論。其實,就是 this
引用會始終被綁定到構造函數上,而這底下是經過閉包實現的。只是把你之前手寫的代碼自動化生成而已。
在本文的第二個例子中,咱們留意到 Animal()
構造函數被額外包了一層,當時不得其解。看到這裏,咱們也許能夠理解它的意圖:就是爲了將你在類中編寫的箭頭函數作個閉包,將 this
引用存儲下來,以作後用。