class
衆所周知,JavaScript
是沒有類的,class
也只是語法糖,這篇文章旨在於理清咱們經常掛着嘴邊的語法糖,究竟指的是什麼。javascript
ES6
與 ES5
寫法對比class Parent { static nation = 'China' isAdult = true get thought() { console.log('Thought in head is translate to Chinese.') return this._thought } set thought(newVal) { this._thought = newVal } constructor(name) { this.name = name } static live() { console.log('live') } talk() { console.log('talk') } }
這是一個很完整的寫法,咱們已經習慣於這麼方便地寫出一個類了,那麼對應到 ES5
中的寫法又是如何呢java
function Parent(name) { this.name = name this.isAdult = true } Parent.nation = 'China' Parent.live = function() { console.log('live') } Parent.prototype = { get thought() { return this._thought }, set thought(newVal) { this._thought = newVal }, talk: function() { console.log('talk') } }
能夠很清晰地看到react
ES6
中 Parent
類的 constructor
對應的就是 ES5
中的構造函數 Parent
;name
和 isAdult
,不管在 ES6
中採用何種寫法,在 ES5
中依然都是掛在 this
下;ES6
中經過關鍵字 static
修飾的靜態屬性和方法 nation
和 live
,則都被直接掛在類 Parent
上;tought
和 方法 talk
是被掛在 原型對象 Parent.prototype
上的。Babel
是如何進行編譯的咱們能夠經過將代碼輸入到 Babel
官網的 Try it out 來查看編譯後的代碼,這個部分咱們按部就班,一步一步來進行編譯,拆解 Babel
的編譯過程:git
咱們此時只觀察 屬性 相關的編譯結果,
編譯前:es6
class Parent { static nation = 'China' isAdult = true constructor(name) { this.name = name } }
編譯後:github
'use strict' // 封裝後的 instanceof 操做 function _instanceof(left, right) { if ( right != null && typeof Symbol !== 'undefined' && right[Symbol.hasInstance] ) { return !!right[Symbol.hasInstance](left) } else { return left instanceof right } } // ES6 的 class,必須使用 new 操做來調用, // 這個方法的做用就是檢查是否經過 new 操做調用,使用到了上面封裝的 _instanceof 方法 function _classCallCheck(instance, Constructor) { if (!_instanceof(instance, Constructor)) { throw new TypeError('Cannot call a class as a function') } } // 封裝後的 Object.defineProperty function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }) } else { obj[key] = value } return obj } var Parent = function Parent(name) { // 檢查是否經過 new 操做調用 _classCallCheck(this, Parent) // 初始化 isAdult _defineProperty(this, 'isAdult', true) // 根據入參初始化 name this.name = name } // 初始化靜態屬性 nation _defineProperty(Parent, 'nation', 'China')
從編譯後的代碼中能夠發現,Babel
爲了其嚴謹度,封裝了一些方法,其中 可能有點迷惑的是 _instanceof(left, right)
這個方法裏的 Symbol.hasInsance
,從 MDN 和 ECMAScript6入門 中能夠知道,這個屬性能夠用來自定義 instanceof
操做符在某個類上的行爲。這裏還有一個重點關注對象 _classCallCheck(instance, Constructor)
,這個方法用來檢查是否經過 new 操做調用。express
編譯前:segmentfault
class Parent { static nation = 'China' isAdult = true get thought() { console.log('Thought in head is translate to Chinese.') return this._thought } set thought(newVal) { this._thought = newVal } constructor(name) { this.name = name } static live() { console.log('live') } talk() { console.log('talk') } }
編譯後:babel
'use strict' // 封裝後的 instanceof 操做 function _instanceof(left, right) { // ..... } // ES6 的 class,必須使用 new 操做來調用, // 這個方法的做用就是檢查是否經過 new 操做調用,使用到了上面封裝的 _instanceof 方法 function _classCallCheck(instance, Constructor) { // ...... } // 封裝 Object.defineProperty 來添加屬性 function _defineProperties(target, props) { // 遍歷 props for (var i = 0; i < props.length; i++) { var descriptor = props[i] // enumerable 默認爲 false descriptor.enumerable = descriptor.enumerable || false descriptor.configurable = true if ('value' in descriptor) descriptor.writable = true Object.defineProperty(target, descriptor.key, descriptor) } } // 爲 Constructor 添加原型屬性或者靜態屬性並返回 function _createClass(Constructor, protoProps, staticProps) { // 若是是原型屬性,添加到原型對象上 if (protoProps) _defineProperties(Constructor.prototype, protoProps) // 若是是靜態屬性,添加到構造函數上 if (staticProps) _defineProperties(Constructor, staticProps) return Constructor } // 封裝後的 Object.defineProperty function _defineProperty(obj, key, value) { // ...... } var Parent = /*#__PURE__*/ (function() { // 添加 getter/setter _createClass(Parent, [ { key: 'thought', get: function get() { console.log('Thought in head is translate to Chinese.') return this._thought }, set: function set(newVal) { this._thought = newVal } } ]) function Parent(name) { // 檢查是否經過 new 操做調用 _classCallCheck(this, Parent) // 初始化 isAdult _defineProperty(this, 'isAdult', true) // 根據入參初始化 name this.name = name } // 添加 talk 和 live 方法 _createClass( Parent, [ { key: 'talk', value: function talk() { console.log('talk') } } ], [ { key: 'live', value: function live() { console.log('live') } } ] ) return Parent })() // 初始化靜態屬性 nation _defineProperty(Parent, 'nation', 'China')
與過程一相比,編譯後的代碼, Babel
多生成了一個 _defineProperties(target, props)
和 _createClass(Constructor, protoProps, staticProps)
的輔助函數,這兩個主要用來添加原型屬性和靜態屬性,而且經過 Object.defineProperty
的方法,對數據描述符和存取描述符均可以進行控制。
值得注意的是,ES6
中的 class
裏的全部方法都是不可遍歷的(enumerable: false
),這裏有一個小細節: 若是有使用 TypeScript
,在設置 compileOptions
中的 target
時,若是設置爲 es5
,那麼會發現編譯後的 方法能夠經過 Object.keys()
遍歷到,而設置爲es6
時就沒法被遍歷。函數
Babel
經過 AST
抽象語法樹分析,而後添加如下
_instanceof(left, right) // 封裝後的 instanceof 操做
_classCallCheck(instance, Constructor) // 檢查是否經過 new 操做調用
_defineProperties(target, props) // 封裝 Object.defineProperty 來添加屬性
_createClass(Constructor, protoProps, staticProps) // 爲 Constructor 添加原型屬性或者靜態屬性並返回
_defineProperty(obj, key, value) // // 封裝後的 Object.defineProperty
五個輔助函數,來爲 Parent
構造函數添加屬性和方法,轉換 名爲 class
的語法糖爲 ES5
的代碼。
extends
既然 ES6
沒有類,那又應該如何實現繼承呢,相信聰明的你已經知道了,其實和 class
同樣,extends
也是語法糖,接下來咱們一步一步接着把這層語法糖也拆開。
ES5
的 寄生組合式繼承從 從 Prototype 開始提及(上)—— 圖解 ES5 繼承相關 這裏知道,相對完美的繼承實現是 寄生組合式繼承,爲了方便閱讀,這裏再次附上源碼和示意例圖:
function createObject(o) { function F() {} F.prototype = o return new F() } function Parent(name) { this.name = name } function Child(name) { Parent.call(this, name) } Child.prototype = createObject(Parent.prototype) Child.prototype.constructor = Child var child = new Child('child')
ES6
和 ES5
寫法對比若是參考上面的繼承實現,咱們能夠輕鬆地寫出兩種版本的繼承形式
class Child extends Parent { constructor(name, age) { super(name); // 調用父類的 constructor(name) this.age = age; } }
function Child (name, age) { Parent.call(this, name) this.age = age } Child.prototype = createObject(Parent.prototype) Child.prototype.constructor = Child
Babel
是如何進行編譯的
- 子類必須在
constructor
方法中調用super
方法,不然新建實例時會報錯。這是由於子類沒有本身的this
對象,而是繼承父類的this
對象,而後對其進行加工。若是不調用super
方法,子類就得不到this
對象。也正是由於這個緣由,在子類的構造函數中,只有調用
super
以後,纔可使用this
關鍵字,不然會報錯。
- 在
ES6
中,父類的靜態方法,能夠被子類繼承。class
做爲構造函數的語法糖,同時有prototype
屬性和__proto__
屬性,所以同時存在兩條繼承鏈。
一樣的,咱們將代碼輸入到 Babel
官網的 Try it out 來查看編譯後的代碼:
'use strict' // 封裝後的 typeof function _typeof(obj) { if ( typeof Symbol === 'function' && typeof Symbol.iterator === 'symbol' ) { _typeof = function _typeof(obj) { return typeof obj } } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === 'function' && obj.constructor === Symbol && obj !== Symbol.prototype ? 'symbol' : typeof obj } } return _typeof(obj) } // 調用父類的 constructor(),並返回子類的 this function _possibleConstructorReturn(self, call) { if ( call && (_typeof(call) === 'object' || typeof call === 'function') ) { return call } return _assertThisInitialized(self) } // 檢查 子類的 super() 是否被調用 function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError( "this hasn't been initialised - super() hasn't been called" ) } return self } // 封裝後的 getPrototypeOf function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o) } return _getPrototypeOf(o) } // 實現繼承的輔助函數 function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError( 'Super expression must either be null or a function' ) } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }) if (superClass) _setPrototypeOf(subClass, superClass) } // 封裝後的 setPrototypeOf function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p return o } return _setPrototypeOf(o, p) } // 檢查是否經過 new 操做調用 function _classCallCheck(instance, Constructor) { if (!_instanceof(instance, Constructor)) { throw new TypeError('Cannot call a class as a function') } } var Child = /*#__PURE__*/ (function(_Parent) { // 繼承操做 _inherits(Child, _Parent) function Child(name, age) { var _this _classCallCheck(this, Child) // 調用父類的 constructor(),並返回子類的 this _this = _possibleConstructorReturn( this, _getPrototypeOf(Child).call(this, name) ) // 根據入參初始化子類本身的屬性 _this.age = age return _this } return Child })(Parent)
_inherits(subClass, superClass)
咱們來細看一下這個實現繼承的輔助函數的細節:
function _inherits(subClass, superClass) { // 1. 檢查 extends 的繼承目標(即父類),必須是函數或者是 null if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError( 'Super expression must either be null or a function' ) } // 2. 相似於 ES5 的寄生組合式繼承,使用 Object.create, // 設置子類 prototype 屬性的 __proto__ 屬性指向父類的 prototype 屬性 subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }) // 3. 設置子類的 __proto__ 屬性指向父類 if (superClass) _setPrototypeOf(subClass, superClass) }
這個方法主要分爲3步,其中第2步,經過寄生組合式繼承在實現繼承的同時,新增了一個名爲 constructor
的不可枚舉的屬性;第3步實現了上文說的第二條原型鏈,從而達到靜態方法也能被繼承的效果。
_possibleConstructorReturn(self, call)
這個輔助函數主要是用來實現 super()
的效果,對應到寄生組合式繼承上則是借用構造函數繼承的部分,有所不一樣的是,該方法返回一個 this
並賦給子類的 this
。具體細節能夠在 ES6 系列之 Babel 是如何編譯 Class 的(下) 查看。
和 class
同樣,Babel
經過 AST
抽象語法樹分析,而後添加一組輔助函數,在我看來能夠分爲兩類,第一類:
_typeof(obj) // 封裝後的 typeof
_getPrototypeOf(o) // 封裝後的 getPrototypeOf
_setPrototypeOf(o, p) // 封裝後的 setPrototypeOf
這種爲了健壯性的功能輔助函數
第二類:
_assertThisInitialized(self) // 檢查 子類的 super() 是否被調用
_possibleConstructorReturn(self, call) // 調用父類的 constructor(),並返回子類的 this
_classCallCheck(instance, Constructor) // 檢查是否經過 new 操做調用
_inherits(subClass, superClass) // 實現繼承的輔助函數
這種爲了實現主要功能的流程輔助函數,從而實現更完善的寄生組合式繼承。
從 Prototype 開始提及 一共分爲兩篇,從兩個角度來說述 JavaScript 原型相關的內容。