JavaScript中實現繼承的方式總結

我們在對象創建模式中討論過,對象創建的模式就是定義對象模板的方式。有了模板以后,我們就可以輕松地創建多個結構相同的對象了。
繼承就是對象創建模式的擴展,我們需要在舊模板的基礎上,添加新的特性,使之成為一個新的模板。

為了更加迎合程序員的“直覺”,本篇文章有時使用“父類”來指代“父對象的模板”。但是讀者應該明確:JavaScript沒有類系統。

1. 純原型鏈繼承

純原型鏈繼承的思想就是讓所有需要繼承的屬性都在原型鏈上(而不會在實例對象自己身上)。
原型鏈是實現繼承的重要工具,以下是單純使用原型鏈實現繼承的方式:

function SuperType() {
    this.property = 1;
}
// 將方法放在原型上,以便所有實例共用,避免重復定義
SuperType.prototype.getSuperValue = function() {
    return this.property;
};

function SubType() {
    this.subproperty = 2;
}
SubType.prototype = new SuperType();
// 將方法放在原型上,以便所有實例共用,避免重復定義
SubType.prototype.getSubValue = function() {
    return this.subproperty;
};
var instance = new SubType();
console.log(instance.getSuperValue());  // 1
console.log(instance instanceof SubType);   // true
console.log(instance instanceof SuperType); // true

雖然上面使用了構造函數與new關鍵字,但是在純原型鏈繼承中,繼承屬性的原理與它們一點關系也沒有,父類的所有屬性都在原型鏈上。使用構造函數只是為了代碼簡潔,同時還可以讓我們使用instanceof進行對象識別(詳見上一篇文章)。
實際上,我們可以不使用構造函數與new關鍵字來實現純原型鏈繼承:

// 定義父對象的原型
var superPrototype = {
    getSuperValue: function() {
        return this.property;
    }
};
// 定義父對象的工廠方法
function createSuper() {
    var superInstance = Object.create(superPrototype);
    superInstance.property = 1;
    return superInstance;
}
// 定義子對象的原型
var subPrototype = createSuper();
subPrototype.getSubValue = function() {
    return this.subproperty;
};
// 定義子對象的工廠方法
function createSub() {
    var subInstance = Object.create(subPrototype);
    subInstance.subproperty = 2;
    return subInstance;
}
// 創建子對象
var instance = createSub();
console.log(instance.getSuperValue());  // 1
console.log(instance.getSubValue());  // 2

通過這種方式實現純原型鏈繼承的作用完全相同,原型鏈也完全相同,只不過不能使用instanceof和constructor指針了(因為它們需要構造函數)。

所有需要繼承的屬性都在原型鏈上(而不會在實例對象自己身上),這是純原型鏈繼承與后面繼承方式的最大不同。

純原型鏈繼承的缺陷

  • 在使用構造函數的實現方式中,我們無法給父類的構造函數傳遞參數。也就是上例中的這條語句:SubType.prototype = new SuperType();,這句話是在創建子對象之前就執行的,所以我們無法在實例化的時候決定父類構造函數的參數。
  • 如果不選擇構造函數的那種實現方式,會出現與創建對象模式相同的對象識別問題。
  • 原型鏈的改變會影響所有已經構造出的實例。這是一種靈活性,也是一種危險。如果我們不小心通過實例對象改變了原型鏈上的屬性,會影響所有的實例對象。
  • 父類可能有一些屬性不適合共享,但是純原型鏈繼承強迫所有父類屬性都要共享。比如People是Student的父類,People的name屬性顯然是每個子對象都應該獨享的。如果使用純原型鏈繼承,我們必須在每次得到子類對象以后手動給子對象添加name屬性。

2. 純構造函數繼承

這種技術通過在子類構造函數中調用父類的構造函數,使所有需要繼承的屬性都定義在實例對象上

function SuperType(fatherName) {
    this.fatherName = fatherName;
    this.fatherArray = ["red", "blue", "green"];
}

function SubType(childName, fatherName) {
    SuperType.call(this, fatherName);
    this.childName = childName;
}
var child1 = new SubType('childName1', 'fatherName1');
var child2 = new SubType('childName2', 'fatherName2');

child1.fatherArray.push("black");
console.log(child1.fatherArray); //"red,blue,green,black"
console.log(child2.fatherArray); //"red,blue,green"

由上例可知,純構造函數繼承不會出現純原型鏈繼承的問題:

  • 不存在“原型鏈的改變影響所有已經構造出的實例”的問題。這是因為不管是子類的屬性還是父類的屬性,在子對象上都有一個副本,因此改變一個對象的屬性不會影響另一個對象。
  • 可以在構造子對象實例的時候給父類構造函數傳遞參數。在純構造函數繼承中,父類構造函數是在子類構造函數中調用的,每次調用時,傳遞給父構造函數的參數可以通過子構造函數的參數控制。

純構造函數繼承的缺陷

  • 創建對象的構造函數模式相同,低效率,函數重復定義,無法復用。
  • 如果子類要使用這種繼承方式,父類必須也要使用這種繼承方式。因為使用這種方式的前提就是所有需要繼承的屬性都在父構造函數上。如果父類有一些屬性是通過原型鏈繼承來的,那么子類僅僅通過調用父構造函數無法得到這些屬性。
  • 對象識別功能不全。無法使用 instanceof 來判斷某個對象是否繼承自某個父類。但是還是有基本的對象識別功能:可以使用 instanceof 來判斷某個對象是不是某個類的實例。

3. 組合繼承

組合繼承就是同時使用原型鏈和構造函數繼承:

  • 對于那些適合共享的屬性(一般是函數),將它們放在原型鏈上。
  • 對于需要每個子對象獨享的屬性,在構造函數中定義。
// 定義父類
function SuperType(name) {
    this.name = name;
    this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() {
    console.log(this.name);
};

// 定義子類
function SubType(name, age) {
    // 這里調用了父對象的構造函數(構造函數繼承)
    SuperType.call(this, name);
    this.age = age;
}
// 這里創建了一個父對象來當作原型(原型鏈繼承)
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
    console.log(this.age);
};

var instance1 = new SubType("Nicholas", 29);
var instance2 = new SubType("Greg", 27);

instance1.colors.push("black");
console.log(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Nicholas";
instance1.sayAge(); //29

console.log(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg";
instance2.sayAge(); //27

使用原型鏈的時候,我們要作出合理的選擇:

  • 哪些屬性是每個實例對象獨享的,需要在每次實例化的時候添加到實例對象上
  • 哪些屬性是所有實例共享的,只需要定義在原型對象上。這可以減少資源的浪費。

組合繼承的代碼與純原型鏈繼承的代碼看起來很相似,它們的核心區別在于:組合繼承要在子類構造函數中調用父類構造函數。

組合繼承避免了兩者的缺陷,結合了兩者的優點,并且可以使用instanceof進行對象識別,因此組合繼承在JavaScript中經常被使用。

組合繼承的缺陷

  • 得到一個子對象要執行兩次父構造函數,重復定義屬性。在組合繼承中,我們要執行兩次構造函數
    1. 在定義原型鏈的時候創建原型對象
    2. 在子類的構造函數中調用父構造函數來初始化新對象的屬性。

調用兩次構造函數的結果就是重復定義了父類的屬性,第一次定義在原型鏈上,第二次定義在子對象上。

如果你在Chrome控制臺中打印上面例子的instance2,你會發現,在原型對象和子對象上都有"name"和"colors"屬性。


這樣的重復定義顯然不夠優雅,后面的寄生組合式繼承解決了這個問題。

4. 原型式繼承

原型式繼承的核心是以父對象為原型直接創建子對象
原型式繼承(Prototypal Inheritance)是由Douglas Crockford在他的一篇文章Prototypal Inheritance in JavaScript中提出的。js是一種基于原型的語言,然而Douglas卻發現,當時沒有操作符能夠方便地以一個對象為原型,創建一個新對象。js的原型的天性被構造函數給掩蓋了,因為構造函數被很多人當作“類”來使用。因此Douglas提出一個簡單的函數,以父對象為原型直接創建子對象,沒有類、沒有構造函數、沒有new操作符,只有對象繼承對象,回歸js的本質:

function object(o) {
    function F() {}
    F.prototype = o;
    return new F();
}

在原型式繼承被提出的時候,Object.create方法還沒有引入。ES5引入的Object.create()方法實際就是object函數的規范化。

有了object函數以后,為了能夠方便地創建對象,你可能需要使用工廠模式,來為得到的子對象增加屬性:

// 父對象
var superObject = {
    superValue:1,
    showSuperValue: function() {
        console.log(this.superValue);
    }
};

// 子對象的工廠方法
function createSub() {
    var instance = Object.create(superObject);
    instance.subValue = 2;
    return instance;
}

var subObject = createSub();
subObject.showSuperValue();  // 1

我們有時不一定要大量制造子對象,此時就沒必要定義一個工廠函數了,直接使用Object.create()就好。原型式繼承不一定要定義一個工廠函數,只要以父對象為原型直接創建子對象,就是一種原型式繼承

原型式繼承是一種純原型鏈繼承,因為原型式繼承也將所有要繼承的屬性放在了原型鏈上,我們在純原型鏈繼承中的第二種實現方式,實際上也是原型式繼承。

如果要給子對象添加函數,不要在工廠函數中定義它,而要將函數放在原型對象上,避免低效率代碼。我們在純原型鏈繼承中的第二種實現方式就是一個值得學習的例子。

5. 寄生式繼承

寄生式繼承的思想是增強父對象,得到子對象。新特性“寄生”在父對象上。
寄生式繼承使用工廠函數封裝了以下步驟:

  1. 使用父對象的創建方法(工廠函數或構造函數),創建父對象
  2. 增強父對象(給它增加屬性)
  3. 返回這個對象,它就是子對象
function Super() {
    this.superProperty = 1;
}
Super.prototype.sayHi = function() {
    console.log('hi');
};


function subFactory() {
    var instance = new Super();
    instance.subProperty = 2;
    instance.sayGoodBye = function() {
        console.log('goodbye');
    };
    return instance;
}

var sub = subFactory();

繼承得到的屬性可能在子對象上,也可能在子對象的原型上,這取決于父對象的屬性在不在原型鏈上。

寄生式繼承的缺陷

  • 如果在工廠函數中為對象添加函數(如上面的例子),那么會出現與純構造函數繼承一樣的低效率問題,函數重復定義。
  • 不能使用進行對象識別,因為子對象并不是通過構造函數創建的。

6. 寄生組合式繼承

我們前面說過,雖然組合繼承很常用,但是它也有自己的不足:調用兩次父構造函數,重復定義父屬性。第一次是在定義原型鏈的時候創建原型對象,第二次是在子類的構造函數中調用父構造函數來初始化新對象的屬性。以下是我們給出過的組合式繼承

// 定義父類
function SuperType(name) {
    this.name = name;
    this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() {
    console.log(this.name);
};

// 定義子類
function SubType(name, age) {
    // 第二次調用父構造函數
    SuperType.call(this, name);
    this.age = age;
}
// 第一次調用父構造函數
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
    console.log(this.age);
};

var instance1 = new SubType("Nicholas", 29);
var instance2 = new SubType("Greg", 27);
// instance的本身和原型對象上都有“name”和“colors”屬性

instance1.colors.push("black");
console.log(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Nicholas";
instance1.sayAge(); //29

console.log(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg";
instance2.sayAge(); //27

我們顯然應該舍棄其中的一次。
可以舍棄第二次調用嗎?不行,那樣就變成純原型鏈繼承了,你可以回去看看這種繼承的缺陷。
既然如此,我們只能舍棄第一次調用了。第一次調用構造函數只是為了獲得父對象原型鏈上的屬性,父對象實例上的屬性交給第二次調用來添加了。實際上我們不需要創建一個父對象也可以獲得父對象的原型鏈:

也就是說,我們可以把
SubType.prototype = new SuperType();
改成
SubType.prototype = Object.create(SuperType.prototype);
創建上圖中的藍色空對象,并將它作為子類的原型。子對象一樣可以繼承到父對象原型鏈上的所有屬性

因此,寄生組合式繼承的代碼就是這樣:

// 定義父類
function SuperType(name) {
    this.name = name;
    this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() {
    console.log(this.name);
};

// 定義子類
function SubType(name, age) {
    // 獲得父對象實例上的屬性
    SuperType.call(this, name);
    this.age = age;
}
// 獲得父對象原型鏈上的屬性
SubType.prototype = Object.create(SuperType.prototype);
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
    console.log(this.age);
};

var instance1 = new SubType("Nicholas", 29);
var instance2 = new SubType("Greg", 27);

instance1.colors.push("black");
console.log(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Nicholas";
instance1.sayAge(); //29

console.log(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg";
instance2.sayAge(); //27

現在我們在Chrome控制臺查看instance2的原型,發現原型鏈上已經沒有“name”和“colors”屬性了。寄生組合式繼承十分完美!

為什么不直接SubType.prototype = SuperType.prototype;呢?因為我們后面還要在SubType.prototype上添加子對象的共享函數,如果使用Object.create創建一個新對象,會改變父對象的原型鏈!


寄生組合式繼承真的與“寄生式繼承”有關嗎?對于這一點我持懷疑態度。按照《JavaScript高級程序設計(第3版)》的說法,因為繼承的過程可以封裝成以下函數:

function inheritPrototype(subType, superType) {
    var prototype = Object.create(superType.prototype); // 創建對象
    prototype.constructor = subType; // 增強對象
    subType.prototype = prototype; // 指定對象
}

而作者認為這個函數是一種“寄生式繼承”:prototype“寄生式繼承”自superType.prototype。

我認為這是一種原型式繼承。不能因為在工廠函數中獲取對象、增強對象,就認為它是寄生式繼承。原型式繼承一樣可以用工廠函數來封裝。原型式繼承與寄生式繼承的區別在于獲取對象的方式

  • 通過object函數或Object.create,以父對象為原型直接創建一個空對象。這是原型式繼承。
  • 通過父類的構造函數或工廠函數獲得對象。這是寄生式繼承。

在Douglas Crockford的文章Prototypal Inheritance in JavaScript中,在介紹原型式繼承的時候他說到:
"For convenience, we can create functions which will call the object function for us, and provide other customizations such as augmenting the new objects with privileged functions. I sometimes call these maker functions. If we have a maker function that calls another maker function instead of calling the object function, then we have a parasitic inheritance pattern."
原型式繼承一樣可以使用工廠函數。當調用另一個工廠函數而不是object函數的時候,原型式繼承才變成寄生式繼承。

名字叫什么并不重要,能夠將這些模式中的編程思想融會貫通就夠了。將來我們不一定有機會寫“形式完全規范”的某種模式,但是其中的思想肯定會在一些零零散散的地方用上。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內容

  • 1、構造函數模式 [url=]file:///C:/Users/i037145/AppData/Local/Tem...
    橫沖直撞666閱讀 868評論 0 0
  • 基于這篇文章的一些名稱約定: 上面的約定應該是比較合理的,如果難以理解,可以查看黯羽輕揚:JS學習筆記2_面向對象...
    一直玩編程閱讀 534評論 1 7
  • 博客內容:什么是面向對象為什么要面向對象面向對象編程的特性和原則理解對象屬性創建對象繼承 什么是面向對象 面向對象...
    _Dot912閱讀 1,441評論 3 12
  • 本章內容 理解對象屬性 理解并創建對象 理解繼承 面向對象語言有一個標志,那就是它們都有類的概念,而通過類可以創建...
    悶油瓶小張閱讀 860評論 0 1
  • 這些年過得不好不壞 我已還清了所有的債 已開始試著慢慢攢錢 每個月都會和朋友見見面 吹吹牛,侃侃大山 我有時還是會...
    我是阿麥閱讀 227評論 0 2