摘要: ## 前言 在上一篇 [《 ES6 系列 Babel 是如何編譯 Class 的(上)》](https://github.com/mqyqingfeng/Blog/issues/105),咱們知道了 Babel 是如何編譯 Class 的,這篇咱們學習 Babel 是如何用 ES5 實現 Class 的繼承。 ## ES5 寄生組合式繼承 ```js function Paregit
在上一篇 《 ES6 系列 Babel 是如何編譯 Class 的(上)》,咱們知道了 Babel 是如何編譯 Class 的,這篇咱們學習 Babel 是如何用 ES5 實現 Class 的繼承。github
function Parent (name) { this.name = name; } Parent.prototype.getName = function () { console.log(this.name) } function Child (name, age) { Parent.call(this, name); this.age = age; } Child.prototype = Object.create(Parent.prototype); var child1 = new Child('kevin', '18'); console.log(child1);
原型鏈示意圖爲:express
關於寄生組合式繼承咱們在 《JavaScript深刻之繼承的多種方式和優缺點》 中介紹過。babel
引用《JavaScript高級程序設計》中對寄生組合式繼承的誇讚就是:異步
這種方式的高效率體現它只調用了一次 Parent 構造函數,而且所以避免了在 Parent.prototype 上面建立沒必要要的、多餘的屬性。與此同時,原型鏈還能保持不變;所以,還可以正常使用 instanceof 和 isPrototypeOf。開發人員廣泛認爲寄生組合式繼承是引用類型最理想的繼承範式。函數
Class 經過 extends 關鍵字實現繼承,這比 ES5 的經過修改原型鏈實現繼承,要清晰和方便不少。學習
以上 ES5 的代碼對應到 ES6 就是:this
class Parent { constructor(name) { this.name = name; } } class Child extends Parent { constructor(name, age) { super(name); // 調用父類的 constructor(name) this.age = age; } } var child1 = new Child('kevin', '18'); console.log(child1);
值得注意的是:spa
super 關鍵字表示父類的構造函數,至關於 ES5 的 Parent.call(this)。prototype
子類必須在 constructor 方法中調用 super 方法,不然新建實例時會報錯。這是由於子類沒有本身的 this 對象,而是繼承父類的 this 對象,而後對其進行加工。若是不調用 super 方法,子類就得不到 this 對象。
也正是由於這個緣由,在子類的構造函數中,只有調用 super 以後,纔可使用 this 關鍵字,不然會報錯。
在 ES6 中,父類的靜態方法,能夠被子類繼承。舉個例子:
class Foo { static classMethod() { return 'hello'; } } class Bar extends Foo { } Bar.classMethod(); // 'hello'
這是由於 Class 做爲構造函數的語法糖,同時有 prototype 屬性和 __proto__ 屬性,所以同時存在兩條繼承鏈。
(1)子類的 __proto__ 屬性,表示構造函數的繼承,老是指向父類。
(2)子類 prototype 屬性的 __proto__ 屬性,表示方法的繼承,老是指向父類的 prototype 屬性。
class Parent { } class Child extends Parent { } console.log(Child.__proto__ === Parent); // true console.log(Child.prototype.__proto__ === Parent.prototype); // true
ES6 的原型鏈示意圖爲:
咱們會發現,相比寄生組合式繼承,ES6 的 class 多了一個 Object.setPrototypeOf(Child, Parent)
的步驟。
extends 關鍵字後面能夠跟多種類型的值。
class B extends A { }
上面代碼的 A,只要是一個有 prototype 屬性的函數,就能被 B 繼承。因爲函數都有 prototype 屬性(除了 Function.prototype 函數),所以 A 能夠是任意函數。
除了函數以外,A 的值還能夠是 null,當 extend null
的時候:
class A extends null { } console.log(A.__proto__ === Function.prototype); // true console.log(A.prototype.__proto__ === undefined); // true
那 ES6 的這段代碼:
class Parent { constructor(name) { this.name = name; } } class Child extends Parent { constructor(name, age) { super(name); // 調用父類的 constructor(name) this.age = age; } } var child1 = new Child('kevin', '18'); console.log(child1);
Babel 又是如何編譯的呢?咱們能夠在 Babel 官網的 Try it out 中嘗試:
'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 Parent = function Parent(name) { _classCallCheck(this, Parent); this.name = name; }; var Child = function(_Parent) { _inherits(Child, _Parent); function Child(name, age) { _classCallCheck(this, Child); // 調用父類的 constructor(name) var _this = _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).call(this, name)); _this.age = age; return _this; } return Child; }(Parent); var child1 = new Child('kevin', '18'); console.log(child1);
咱們能夠看到 Babel 建立了 _inherits 函數幫助實現繼承,又建立了 _possibleConstructorReturn 函數幫助肯定調用父類構造函數的返回值,咱們來細緻的看一看代碼。
function _inherits(subClass, superClass) { // extend 的繼承目標必須是函數或者是 null if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } // 相似於 ES5 的寄生組合式繼承,使用 Object.create,設置子類 prototype 屬性的 __proto__ 屬性指向父類的 prototype 屬性 subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); // 設置子類的 __proto__ 屬性指向父類 if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
關於 Object.create(),通常咱們用的時候會傳入一個參數,實際上是支持傳入兩個參數的,第二個參數表示要添加到新建立對象的屬性,注意這裏是給新建立的對象即返回值添加屬性,而不是在新建立對象的原型對象上添加。
舉個例子:
// 建立一個以另外一個空對象爲原型,且擁有一個屬性 p 的對象 const o = Object.create({}, { p: { value: 42 } }); console.log(o); // {p: 42} console.log(o.p); // 42
再完整一點:
const o = Object.create({}, { p: { value: 42, enumerable: false, // 該屬性不可寫 writable: false, configurable: true } }); o.p = 24; console.log(o.p); // 42
那麼對於這段代碼:
subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } });
做用就是給 subClass.prototype 添加一個可配置可寫不可枚舉的 constructor 屬性,該屬性值爲 subClass。
函數裏是這樣調用的:
var _this = _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).call(this, name));
咱們簡化爲:
var _this = _possibleConstructorReturn(this, Parent.call(this, name));
_possibleConstructorReturn
的源碼爲:
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; }
在這裏咱們判斷 Parent.call(this, name)
的返回值的類型,咦?這個值還能有不少類型?
對於這樣一個 class:
class Parent { constructor() { this.xxx = xxx; } }
Parent.call(this, name) 的值確定是 undefined。但是若是咱們在 constructor 函數中 return 了呢?好比:
class Parent { constructor() { return { name: 'kevin' } } }
咱們能夠返回各類類型的值,甚至是 null:
class Parent { constructor() { return null } }
咱們接着看這個判斷:
call && (typeof call === "object" || typeof call === "function") ? call : self;
注意,這句話的意思並非判斷 call 是否存在,若是存在,就執行 (typeof call === "object" || typeof call === "function") ? call : self
由於 &&
的運算符優先級高於 ? :
,因此這句話的意思應該是:
(call && (typeof call === "object" || typeof call === "function")) ? call : self;
對於 Parent.call(this) 的值,若是是 object 類型或者是 function 類型,就返回 Parent.call(this),若是是 null 或者基本類型的值或者是 undefined,都會返回 self 也就是子類的 this。
這也是爲何這個函數被命名爲 _possibleConstructorReturn
。
var Child = function(_Parent) { _inherits(Child, _Parent); function Child(name, age) { _classCallCheck(this, Child); // 調用父類的 constructor(name) var _this = _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).call(this, name)); _this.age = age; return _this; } return Child; }(Parent);
最後咱們整體看下如何實現繼承:
首先執行 _inherits(Child, Parent)
,創建 Child 和 Parent 的原型鏈關係,即 Object.setPrototypeOf(Child.prototype, Parent.prototype)
和 Object.setPrototypeOf(Child, Parent)
。
而後調用 Parent.call(this, name)
,根據 Parent 構造函數的返回值類型肯定子類構造函數 this 的初始值 _this。
最終,根據子類構造函數,修改 _this 的值,而後返回該值。
ES6 系列目錄地址:https://github.com/mqyqingfeng/Blog
ES6 系列預計寫二十篇左右,旨在加深 ES6 部分知識點的理解,重點講解塊級做用域、標籤模板、箭頭函數、Symbol、Set、Map 以及 Promise 的模擬實現、模塊加載方案、異步處理等內容。