前言
距離上一篇js的繼承系列已經過去了四年,時不時還有新的讀者評論和回覆,開心之餘也想着更新一下內容,因為當時的內容裏沒有涉及到es6的 extend 實現,所以現在抽空補上。 當然,如果是0基礎的同學或者對於基本的繼承有些遺忘的同學,可以先回顧一下前兩篇:
詳解js中的繼承(一)
詳解js中的繼承(二)
正文
基礎回顧 & 預備知識
為了使後面的學習過程更絲滑,在開始之前,一起再回顧一下這個構造函數-原型對象-實例模型:
當訪問 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做個簡單的源碼分析和原理介紹,最關鍵的還是原型鏈的圖解部分,希望能對讀者有幫助。
歡迎大家關注專欄,也希望大家對於喜愛的文章,能夠不吝點贊和收藏,對於行文風格和內容有任何意見的,都歡迎私信交流。
(想來外企的小夥伴歡迎私信或者添加主頁聯繫方式諮詢詳情~)