前言
在上一篇 《 ES6 系列 Babel 是如何編譯 Class 的(上)》,我們知道了 Babel 是如何編譯 Class 的,這篇我們學習 Babel 是如何用 ES5 實現 Class 的繼承。
ES5 寄生組合式繼承
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);
複製程式碼
原型鏈示意圖為:
關於寄生組合式繼承我們在 《JavaScript深入之繼承的多種方式和優缺點》 中介紹過。
引用《JavaScript高階程式設計》中對寄生組合式繼承的誇讚就是:
這種方式的高效率體現它只呼叫了一次 Parent 建構函式,並且因此避免了在 Parent.prototype 上面建立不必要的、多餘的屬性。與此同時,原型鏈還能保持不變;因此,還能夠正常使用 instanceof 和 isPrototypeOf。開發人員普遍認為寄生組合式繼承是引用型別最理想的繼承正規化。
ES6 extend
Class 通過 extends 關鍵字實現繼承,這比 ES5 的通過修改原型鏈實現繼承,要清晰和方便很多。
以上 ES5 的程式碼對應到 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);
複製程式碼
值得注意的是:
super 關鍵字表示父類的建構函式,相當於 ES5 的 Parent.call(this)。
子類必須在 constructor 方法中呼叫 super 方法,否則新建例項時會報錯。這是因為子類沒有自己的 this 物件,而是繼承父類的 this 物件,然後對其進行加工。如果不呼叫 super 方法,子類就得不到 this 物件。
也正是因為這個原因,在子類的建構函式中,只有呼叫 super 之後,才可以使用 this 關鍵字,否則會報錯。
子類的 __proto__
在 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
複製程式碼
Babel 編譯
那 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 函式幫助確定呼叫父類建構函式的返回值,我們來細緻的看一看程式碼。
_inherits
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。
_possibleConstructorReturn
函式裡是這樣呼叫的:
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 系列
ES6 系列目錄地址:github.com/mqyqingfeng…
ES6 系列預計寫二十篇左右,旨在加深 ES6 部分知識點的理解,重點講解塊級作用域、標籤模板、箭頭函式、Symbol、Set、Map 以及 Promise 的模擬實現、模組載入方案、非同步處理等內容。
如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。如果喜歡或者有所啟發,歡迎 star,對作者也是一種鼓勵。