前言
距離上一篇js的繼承系列已經過去了四年,時不時還有新的讀者評論和回覆,開心之餘也想著更新一下內容,因為當時的內容裡沒有涉及到es6的 extend
實現,所以現在抽空補上。 當然,如果是0基礎的同學或者對於基本的繼承有些遺忘的同學,可以先回顧一下前兩篇:
正文
基礎回顧 & 預備知識
為了使後面的學習過程更絲滑,在開始之前,一起再回顧一下這個建構函式-原型物件-例項模型:
當訪問 a
的屬性時,會先從a
本身的屬性(或方法)去找,如果找不到,會沿著 __proto__
屬性找到原型物件A.prototype
,在原型物件上查詢對應的屬性(或方法);如果再找不到,繼續沿著原型物件的__proto__
繼續找,這也就是最早我們介紹過的原型鏈的內容。
function A (){
this.type = 'A'
}
const a = new A();
當然,圖上的原型鏈可以繼續找,我們知道 A
雖然是函式,但是本質也是 Object
,沿著__proto__
屬性 不斷上溯,最終會返回 null
;
a.__proto__ === A.prototype; // true
a.__proto__.__proto__ === Object.prototype; // true
a.__proto__.__proto__.__proto__ === null; // true
extend實現原始碼解析
進入正題, 學過 es6
的同學都知道,可以通過關鍵字 extend
直接實現繼承,比如:
// 首先建立一個Animal類
class Animal {
name: string;
constructor(theName: string) { this.name = theName; };
move(distanceInMeters: number = 0) {
console.log(`Animal moved ${distanceInMeters}m.`);
}
}
// 子類Dog繼承於Animal
class Dog extends Animal {
age: number;
constructor(name: string, age: number) {
super(name);
this.age = age;
}
bark() {
console.log('Woof! Woof!');
}
}
const dog = new Dog('wangwang', 12);
dog.bark();// 'Woof! Woof!'
dog.move(10);//`Animal moved 10m.`
那麼這個 extend
究竟做了哪些事情呢? 這裡藉助安裝 typescript
這個 npm
包,然後在本地執行 tsc [檔案路徑]
,把ts以及es6的程式碼轉換成原生js的程式碼來進行研究,(當然也有個缺點是轉換的程式碼為了追求程式碼極簡 有時可能會影響可讀性 比如 undefined
寫作 void 0
之類的),上面的程式碼轉換之後長這樣:
// 第一部分
var __extends = (this && this.__extends) || (function () {
var extendStatics = function (d, b) {
extendStatics = Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };
return extendStatics(d, b);
};
return function (d, b) {
if (typeof b !== "function" && b !== null)
throw new TypeError("Class extends value " + String(b) + " is not a constructor or null");
extendStatics(d, b);
function __() { this.constructor = d; }
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
};
})();
// 第二部分
// 首先建立一個Animal類
var Animal = /** @class */ (function () {
function Animal(theName) {
this.name = theName;
}
;
Animal.prototype.move = function (distanceInMeters) {
if (distanceInMeters === void 0) { distanceInMeters = 0; }
console.log("Animal moved ".concat(distanceInMeters, "m."));
};
return Animal;
}());
// 第三部分
// 子類Dog繼承於Animal
var Dog = /** @class */ (function (_super) {
__extends(Dog, _super);
function Dog(name, age) {
var _this = _super.call(this, name) || this;
_this.age = age;
return _this;
}
Dog.prototype.bark = function () {
console.log('Woof! Woof!');
};
Dog.prototype.move = function (distanceInMeters) {
if (distanceInMeters === void 0) { distanceInMeters = 5; }
console.log("Dog moved ".concat(distanceInMeters, "m."));
};
return Dog;
}(Animal));
// 第四部分 無需解析
var dog = new Dog('wangwang', 12);
dog.bark(); // 'Woof! Woof!'
dog.move(10); // Dog moved 10m.
程式碼看起來有些複雜,我們按照程式碼註釋裡,各部分內容複雜程度從簡單到複雜進行分析:
- 先看第二部分,首先是用匿名立即執行函式(IIFE)包裹了一層,這一點我們在聊閉包的時候說過,這樣寫的好處是避免汙染到全域性名稱空間;然後在內部,就是之前第一篇說過的建構函式-原型物件的經典模型-- 屬性放在建構函式裡,方法繫結在原型物件上, 所以這一部分其實就是 es6的
Class
對應的原生js寫法; 第三部分,
Dog
類的寫法和第二部分大體相同,但是還是有幾處區別:_super.call(this, name)
,_super
代表父類,所以這一步是使用父類的建構函式生成一個物件,之後再根據自身的建構函式,修改該物件;__extends
方法,也是本文的核心內容。
最後來介紹第一部分,也就是
__extends
的具體實現。這部分的外層也是一個簡單的避免重複定義以及匿名立即執行函式(IIFE),這一點就不贅述了。 核心內容是extendStatics
的實現:首先介紹下
Object.setPrototypeOf
這個方法,這個方法的作用是為某個物件重新指定原型,用法如下:Object.setPrototypeOf(d, b) // 等價於d.__proto__ = b;
後續每個
||
分隔符後面,都可以理解為一種polyfill
寫法,只是為了相容不同的執行環境;接下來返回一個新的函式,前面提到,直接轉換過來的可能有點晦澀,所以我在這裡稍微整理成可讀性更強的寫法:
return function (d, b) { // 當b不是建構函式或者null時,丟擲錯誤 if (typeof b !== "function" && b !== null) { throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); } // 修改d原型鏈指向 extendStatics(d, b); // 模擬原型鏈的繼承 function Temp() { this.constructor = d; } if(b === null){ d.prototype = {}; // Object.create(null) 此時返回一個新的空物件{} } else { Temp.prototype = b.prototype; var temp = new Temp(); d.prototype = temp; } }; 此處第一個 `if` 比較好理解,不多解釋;
接下來的 extendStatics(d, b)
也介紹了效果是 d.__proto__ = b;
再接著就是比較有意思了,為了方便大家看懂,還是畫一下相關的關係圖:
首先, d和b
各自獨立(當然)這裡請注意!!!,我們用大寫字母B和D分表表示b和d的建構函式,而b和d本身也可能還是一個函式,也還有自己對應的原型物件,只是圖上沒有標出。(眼神不太好或者不太仔細的同學務必要認真 否則很容易理解出錯)
舉個例子,前文的 Animal
對應圖上的b, 那麼 B
則對應 Function
, 即 Animal.__proto__ = Function.prototype
, 但是與此同時,Animal
還有自己的原型物件Animal.protptype
:
執行extendStatics(d, b)
後,原型關係如下(D的建構函式和原型物件變成不可訪問了,所以用灰色表示):
再接著 執行以下程式碼之後:
function Temp() { this.constructor = d; }
Temp.prototype = b.prototype;
var temp = new Temp();
d.prototype = temp;
結構圖如下:
從圖上可以看到,這個臨時變數temp
最後變成了d
的原型物件, 同時也是一個b的例項。 這一點和我們最早學過的原型鏈繼承其實是類似的,區別在於多了一個 d.__proto__ = b
.
那麼,如果執行 var dog = new Dog('wangwang', 12);
其實,這裡的 Dog
就對應上圖的 d
, dog
的原型鏈其實就是 dog.__proto__ === temp
,再向上也就是 b.prototype
,自然也就可以呼叫到定義在b.prototype
的方法了。
自測環節
那麼在完成 extend
之後,回答幾個問題,測試下自己的理解程度。
Q1: 首先,屬性是怎麼繼承的,和ES5有何區別?
A1: extend是通過呼叫父類的方法建立初始物件,在此基礎上,再根據子類的建構函式對該物件進行調整; ES5 的繼承(組合繼承),實質是先創造子類的例項物件 this
,再利用 call
或者 apply
,將父類的屬性新增到 this
.
Q2: dog
是如何呼叫到 move
方法的?
A2: 這個問題其實就是前面剛剛分析的原型鏈模型,方法的查詢順序是: dog.move(不存在) > dog.__proto__(temp變數).move (不存在) > dog.__proto__.__proto__.move (找到)
Q3: 多出來的d.__proto__ = b
有何作用?
A3: 可以繼承父類的靜態方法,例如新增方法: Animail.sayHello = function() {console.log('hello')};
,那麼Dog.sayHello()
同樣生效,可以參照上圖進行理解,查詢順序: d.hello(不存在) > d.__proto__.hello (找到)
小結
本文是繼承系列的後續文章,主要針對ES6
裡Extend
做個簡單的原始碼分析和原理介紹,最關鍵的還是原型鏈的圖解部分,希望能對讀者有幫助。
歡迎大家關注專欄,也希望大家對於喜愛的文章,能夠不吝點贊和收藏,對於行文風格和內容有任何意見的,都歡迎私信交流。
(想來外企的小夥伴歡迎私信或者新增主頁聯絡方式諮詢詳情~)