本文首發於我的 Github,歡迎 issue / fxxk。git
ES6
的class
語法糖你是否已經用得是否爐火純青呢?那若是迴歸到ES5
呢?本文,將繼續上一篇 《萬物皆空之 JavaScript 原型》 篇尾提出的疑問如何用 JavaScript 實現類的繼承
來展開闡述:github
經過本文,你將學到:typescript
JavaScript
模擬類中的私有變量;JavaScript
繼承方法,原理及其優缺點;fancy
的JavaScript
繼承方法。此外,若是你徹底明白了文末的終極版繼承
,你也就懂了這兩篇所要講的核心知識,同時,也能說明你擁有不錯的JavaScript
基礎。瀏覽器
咱們來回顧一下ES6 / TypeScript / ES5
類的寫法以做對比。首先,咱們建立一個GithubUser
類,它擁有一個login
方法,和一個靜態方法getPublicServices
, 用於獲取public
的方法列表:app
class GithubUser {
static getPublicServices() {
return ['login']
}
constructor(username, password) {
this.username = username
this.password = password
}
login() {
console.log(this.username + '要登陸Github,密碼是' + this.password)
}
}
複製代碼
實際上,ES6
這個類的寫法有一個弊病,實際上,密碼password
應該是Github
用戶一個私有變量,接下來,咱們用TypeScript
重寫一下:函數
class GithubUser {
static getPublicServices() {
return ['login']
}
public username: string
private password: string
constructor(username, password) {
this.username = username
this.password = password
}
public login(): void {
console.log(this.username + '要登陸Github,密碼是' + this.password)
}
}
複製代碼
如此一來,password
就只能在類的內部訪問了。好了,問題來了,若是結合原型講解那一文的知識,來用ES5
實現這個類呢?just show you my code
:post
function GithubUser(username, password) {
// private屬性
let _password = password
// public屬性
this.username = username
// public方法
GithubUser.prototype.login = function () {
console.log(this.username + '要登陸Github,密碼是' + _password)
}
}
// 靜態方法
GithubUser.getPublicServices = function () {
return ['login']
}
複製代碼
值得注意的是,咱們通常都會把
共有方法
放在類的原型上,而不會採用this.login = function() {}
這種寫法。由於只有這樣,才能讓多個實例引用同一個共有方法,從而避免重複建立方法的浪費。測試
是否是很直觀!留下2
個疑問:優化
private方法
呢?protected屬性/方法
呢?用掘金的用戶都應該知道,咱們能夠選擇直接使用 Github
登陸,那麼,結合上一節,咱們若是建立了一個 JuejinUser
來繼承 GithubUser
,那麼 JuejinUser
及其實例就能夠調用 Github
的 login
方法了。首先,先寫出這個簡單 JuejinUser
類:ui
function JuejinUser(username, password) {
// TODO need implementation
this.articles = 3 // 文章數量
JuejinUser.prototype.readArticle = function () {
console.log('Read article')
}
}
複製代碼
因爲ES6/TS
的繼承太過直觀,本節將忽略。首先概述一下本文將要講解的幾種繼承方法:
看起來不少,咱們一一論述。
由於咱們已經得知:
若經過
new Parent()
建立了Child
,則Child.__proto__ = Parent.prototype
,而原型鏈則是順着__proto__
依次向上查找。所以,能夠經過修改子類的原型爲父類的實例來實現繼承。
第一直覺的實現以下:
function GithubUser(username, password) {
let _password = password
this.username = username
GithubUser.prototype.login = function () {
console.log(this.username + '要登陸Github,密碼是' + _password)
}
}
function JuejinUser(username, password) {
this.articles = 3 // 文章數量
JuejinUser.prototype = new GithubUser(username, password)
JuejinUser.prototype.readArticle = function () {
console.log('Read article')
}
}
const juejinUser1 = new JuejinUser('ulivz', 'xxx', 3)
console.log(juejinUser1)
複製代碼
在瀏覽器中查看原型鏈:
誒,不對啊,很明顯 juejinUser1.__proto__
並非 GithubUser
的一個實例。
實際上,這是由於以前咱們爲了可以在類的方法中讀取私有變量,將JuejinUser.prototype
的從新賦值放在了構造函數中,而此時實例已經建立,其__proto__
還還指向老的JuejinUser.prototype
。因此,從新賦值一下實例的__proto__
就能夠解決這個問題:
function GithubUser(username, password) {
let _password = password
this.username = username
GithubUser.prototype.login = function () {
console.log(this.username + '要登陸Github,密碼是' + _password)
}
}
function JuejinUser(username, password) {
this.articles = 3 // 文章數量
const prototype = new GithubUser(username, password)
// JuejinUser.prototype = prototype // 這一行已經沒有意義了
prototype.readArticle = function () {
console.log('Read article')
}
this.__proto__ = prototype
}
const juejinUser1 = new JuejinUser('ulivz', 'xxx', 3)
console.log(juejinUser1)
複製代碼
接着查看原型鏈:
Perfect!原型鏈已經出來,問題「好像」獲得了完美解決!但實際上仍是有明顯的問題:
__proto__
,致使 juejinUser1.__proto__ === JuejinUser.prototype
不成立!從而致使 juejinUser1 instanceof JuejinUser
也不成立😂。這不該該發生!細心的同窗會發現,形成這種問題的根本緣由在於咱們在實例化的時候動態修改了原型,那有沒有一種方法能夠在實例化以前就固定好類的原型的refernce
呢?
事實上,咱們能夠考慮把類的原型的賦值挪出來:
function JuejinUser(username, password) {
this.articles = 3 // 文章數量
}
// 此時構造函數還未運行,沒法訪問 username 和 password !!
JuejinUser.prototype = new GithubUser()
prototype.readArticle = function () {
console.log('Read article')
}
複製代碼
可是這樣作又有更明顯的缺點:
舉例說明缺點2
:
function GithubUser(username) {
this.username = 'Unknown'
}
function JuejinUser(username, password) {
}
JuejinUser.prototype = new GithubUser()
const juejinUser1 = new JuejinUser('ulivz', 'xxx', 3)
const juejinUser2 = new JuejinUser('egoist', 'xxx', 0)
// 這就是把屬性定義在原型鏈上的致命缺點,你能夠直接訪問,但修改就是一件難事了!
console.log(juejinUser1.username) // 'Unknown'
juejinUser1.__proto__.username = 'U'
console.log(juejinUser1.username) // 'U'
// 臥槽,無情地影響了另外一個實例!!!
console.log(juejinUser2.username) // 'U'
複製代碼
因而可知,類式繼承
的兩種方式缺陷太多!
經過 call()
來實現繼承 (相應的, 你也能夠用apply
)。
function GithubUser(username, password) {
let _password = password
this.username = username
GithubUser.prototype.login = function () {
console.log(this.username + '要登陸Github,密碼是' + _password)
}
}
function JuejinUser(username, password) {
GithubUser.call(this, username, password)
this.articles = 3 // 文章數量
}
const juejinUser1 = new JuejinUser('ulivz', 'xxx')
console.log(juejinUser1.username) // ulivz
console.log(juejinUser1.username) // xxx
console.log(juejinUser1.login()) // TypeError: juejinUser1.login is not a function
複製代碼
固然,若是繼承真地如此簡單,那麼本文就沒有存在的必要了,本繼承方法也存在明顯的缺陷—— 構造函數式繼承
並無繼承父類原型上的方法。
既然上述兩種方法各有缺點,可是又各有所長,那麼咱們是否能夠將其結合起來使用呢?沒錯,這種繼承方式就叫作——組合式繼承
:
function GithubUser(username, password) {
let _password = password
this.username = username
GithubUser.prototype.login = function () {
console.log(this.username + '要登陸Github,密碼是' + _password)
}
}
function JuejinUser(username, password) {
GithubUser.call(this, username, password) // 第二次執行 GithubUser 的構造函數
this.articles = 3 // 文章數量
}
JuejinUser.prototype = new GithubUser(); // 第二次執行 GithubUser 的構造函數
const juejinUser1 = new JuejinUser('ulivz', 'xxx')
複製代碼
雖然這種方式彌補了上述兩種方式的一些缺陷,但有些問題仍然存在:
本方法很明顯執行了兩次父類的構造函數,所以,這也不是咱們最終想要的繼承方式。
原型繼承其實是對類式繼承
的一種封裝,只不過其獨特之處在於,定義了一個乾淨的中間類,以下:
function createObject(o) {
// 建立臨時類
function f() {
}
// 修改類的原型爲o, 因而f的實例都將繼承o上的方法
f.prototype = o
return new f()
}
複製代碼
熟悉ES5
的同窗,會注意到,這不就是 Object.create 嗎?沒錯,你能夠認爲是如此。
既然只是類式繼承
的一種封裝,其使用方式天然以下:
JuejinUser.prototype = createObject(GithubUser)
複製代碼
也就仍然沒有解決類式繼承
的一些問題。
PS:我我的以爲
原型繼承
和類式繼承
應該直接歸爲一種繼承!但無賴衆多JavaScript
書籍均是如此命名,算是follow legacy
的標準吧。
寄生繼承
是依託於一個對象而生的一種繼承方式,所以稱之爲寄生
。
const juejinUserSample = {
username: 'ulivz',
password: 'xxx'
}
function JuejinUser(obj) {
var o = Object.create(obj)
o.prototype.readArticle = function () {
console.log('Read article')
}
return o;
}
var myComputer = new CreateComputer(computer);
複製代碼
因爲實際生產中,繼承一個單例對象的場景實在是太少,所以,咱們仍然沒有找到最佳的繼承方法。
看起來很玄乎,先上代碼:
// 寄生組合式繼承的核心方法
function inherit(child, parent) {
// 繼承父類的原型
const p = Object.create(parent.prototype)
// 重寫子類的原型
child.prototype = p
// 重寫被污染的子類的constructor
p.constructor = child
}
// GithubUser, 父類
function GithubUser(username, password) {
let _password = password
this.username = username
}
GithubUser.prototype.login = function () {
console.log(this.username + '要登陸Github,密碼是' + _password)
}
// GithubUser, 子類
function JuejinUser(username, password) {
GithubUser.call(this, username, password) // 繼承屬性
this.articles = 3 // 文章數量
}
// 實現原型上的方法
inherit(JuejinUser, GithubUser)
// 在原型上添加新方法
JuejinUser.prototype.readArticle = function () {
console.log('Read article')
}
const juejinUser1 = new JuejinUser('ulivz', 'xxx')
console.log(juejinUser1)
複製代碼
來瀏覽器中查看結果:
簡單說明一下:
Nice!這纔是咱們想要的繼承方法。然而,仍然存在一個美中不足的問題:
因此,咱們能夠將其優化一下:
function inherit(child, parent) {
// 繼承父類的原型
const parentPrototype = Object.create(parent.prototype)
// 將父類原型和子類原型合併,並賦值給子類的原型
child.prototype = Object.assign(parentPrototype, child.prototype)
// 重寫被污染的子類的constructor
p.constructor = child
}
複製代碼
但實際上,使用Object.assign
來進行copy
仍然不是最好的方法,根據MDN的描述:
Object.assign()
method is used to copy the values of all enumerable own properties from one or more source objects to a target object. It will return the target object.其中有個很關鍵的詞:enumerable
,這已經不是本節討論的知識了,不熟悉的同窗能夠參考 MDN - Object.defineProperty 補習。簡答來講,上述的繼承方法只適用於copy
原型鏈上可枚舉的方法,此外,若是子類自己已經繼承自某個類,以上的繼承將不能知足要求。
爲了讓代碼更清晰,我用ES6
的一些API,寫出了這個我所認爲的最合理的繼承方法:
Reflect
代替了Object
;Reflect.getPrototypeOf
來代替ob.__ptoto__
;Reflect.ownKeys
來讀取全部可枚舉/不可枚舉/Symbol的屬性;Reflect.getOwnPropertyDescriptor
讀取屬性描述符;Reflect.setPrototypeOf
來設置__ptoto__
。源代碼以下:
/*! * fancy-inherit * (c) 2016-2018 ULIVZ */
// 不一樣於object.assign, 該 merge方法會複製全部的源鍵
// 無論鍵名是 Symbol 或字符串,也不論是否可枚舉
function fancyShadowMerge(target, source) {
for (const key of Reflect.ownKeys(source)) {
Reflect.defineProperty(target, key, Reflect.getOwnPropertyDescriptor(source, key))
}
return target
}
// Core
function inherit(child, parent) {
const objectPrototype = Object.prototype
// 繼承父類的原型
const parentPrototype = Object.create(parent.prototype)
let childPrototype = child.prototype
// 若子類沒有繼承任何類,直接合並子類原型和父類原型上的全部方法
// 包含可枚舉/不可枚舉的方法
if (Reflect.getPrototypeOf(childPrototype) === objectPrototype) {
child.prototype = fancyShadowMerge(parentPrototype, childPrototype)
} else {
// 若子類已經繼承子某個類
// 父類的原型將在子類原型鏈的盡頭補全
while (Reflect.getPrototypeOf(childPrototype) !== objectPrototype) {
childPrototype = Reflect.getPrototypeOf(childPrototype)
}
Reflect.setPrototypeOf(childPrototype, parent.prototype)
}
// 重寫被污染的子類的constructor
parentPrototype.constructor = child
}
複製代碼
測試:
// GithubUser
function GithubUser(username, password) {
let _password = password
this.username = username
}
GithubUser.prototype.login = function () {
console.log(this.username + '要登陸Github,密碼是' + _password)
}
// JuejinUser
function JuejinUser(username, password) {
GithubUser.call(this, username, password)
WeiboUser.call(this, username, password)
this.articles = 3
}
JuejinUser.prototype.readArticle = function () {
console.log('Read article')
}
// WeiboUser
function WeiboUser(username, password) {
this.key = username + password
}
WeiboUser.prototype.compose = function () {
console.log('compose')
}
// 先讓 JuejinUser 繼承 GithubUser,而後就能夠用github登陸掘金了
inherit(JuejinUser, GithubUser)
// 再讓 JuejinUser 繼承 WeiboUser,而後就能夠用weibo登陸掘金了
inherit(JuejinUser, WeiboUser)
const juejinUser1 = new JuejinUser('ulivz', 'xxx')
console.log(juejinUser1)
console.log(juejinUser1 instanceof GithubUser) // true
console.log(juejinUser1 instanceof WeiboUser) // true
複製代碼
最後用一個問題來檢驗你對本文的理解:
inherit(A, B, C ...)
, 實現類A
依次繼承後面全部的類,但除了A
之外的類不產生繼承關係。function
來模擬一個類;JavaScript
類的繼承是基於原型的, 一個完善的繼承方法,其繼承過程是至關複雜的;ES6
的繼承,但仍建議深刻了解內部繼承機制。最後放一個彩蛋,爲何我會在寄生組合式繼承中尤爲強調enumerable
這個屬性描述符呢,由於:
ES6
的class
中,默認全部類的方法是不可枚舉的!😅以上,全文終)
注:本文屬於我的總結,部分表達可能會有疏漏之處,若是您發現本文有所欠缺,爲避免誤人子弟,請放心大膽地在評論中指出,或者給我提 issue,感謝~