是時候寫一寫 “繼承”了,為什麼加引號,因為當你閱讀完這篇文章,你會知道,說是 繼承 其實是不準確的。

一、類
1、傳統的面向類的語言中的類:
類/繼承 描述了一種程式碼的組織結構形式。舉個例子:
“汽車”可以被看作是“交通工具”的一種特例。
我們可以定義一個 Vehicle 類和一個 Car 類來對這種關係進行描述。
Vehicle 的定義可能包含引擎、載人能力等,也就是 所有交通工具,比如飛機、火車和汽車等都有的通用的功能描述。
在對 Car 類進行定義的時候,重複定義“載人能力”是沒有意義的,我們只需要宣告 Car 類繼承了 Vehicle 的這個基礎類就可以了。 Car 實際上是對通用 Vehicle 定義的特殊化。
Car 現在只是個類,當我們把它更加形象化,比如說是 保時捷、賓士的時候,就是一個例項化的過程。
我們也有可能 會在 Car 中定義一個 和 Vehicle 中相同的方法,這是子類對父類針對特定方法的重寫,為了可以更加特殊化,更加符合對子類的描述,這個被稱作是 多型。

以上就是 類、繼承、例項化和多型。

再舉個例子:比如房屋的構建
建築師設計出來建築藍圖,然後由建築工人按照建築藍圖建造出真正的建築。建築就是藍圖的物理例項,本質上是對建築藍圖的複製。之後建築工人就可以到下一個地方,把所有的工作重複一遍,再建立一份副本。
建築和藍圖之間的關係是間接的。你可以通過藍圖瞭解建築的結構,只觀察建築本身是無法獲得這些資訊的。但是如果你想開啟一扇門,那就必須接觸真實的建築才行--藍圖只能表示門應該在哪,但並不是真正的門。
一個類就是一張藍圖。為了獲得真正可以互動的物件,我們必須按照類來建造(例項化)一個東西,這個東西通常被稱為例項。這個物件就是類中描述的所有特性的一份副本。

在傳統的面向類的語言中,類的繼承,例項化其實就是複製,用一張圖:

箭頭表示複製操作。

子類 Bar 相對於 父類 Foo 來說是一個獨立並完全不同的類。子類會包含父類行為的副本,也可以通過在子類中定義於父類中相同的方法名來改寫某個繼承的行為,這種改寫不會影響父類中的方法,這兩個方法互不影響。對於 類 Bar 和 例項 b1 、b2 之間也同樣是 類通過複製操作被例項化為物件形式。

2、javascript 中的類
然而 javascript 其實並沒有類的概念,但是 我們早已經習慣用類來思考,所以 javascript 也提供了一些近似類的語法,我們用它模擬出了類,然而這種 “類”還是與傳統面向類語言中的類有不同。
在繼承和例項化的過程中,javascript的物件機制並不會自動執行復制行為。簡單來說,javascript中只有物件,並不存在可以被例項化的“類”。一個物件並不會被複制到其它物件,他們只會被關聯起來,也就是複製的是其實是引用。(對於複製引用,具體的可以看【 js 基礎 】 深淺拷貝 一文,第一部分)

二、javascript 原型鏈、“類”和 繼承
1、 [[prototype]]:javascript 中的物件都有一個特殊的 [[prototype]] 內建屬性,其實就是對於其他物件的引用。他的作用是什麼呢?
當你試圖訪問物件的屬性的時候,就會觸發物件的內建操作 [[Get]],[[Get]] 操作就是從物件中找到你要的屬性。然而他是怎麼找的呢?
例子:

 var testObject = {
a:2
};
console.log(testObject.a) //

在上面的程式碼中,當你 console 的時候,會觸發 [[Get]] 操作,查詢 testObject 中的 a 屬性。對於預設的 [[Get]]操作,第一步是檢查物件本身是否有這個屬性,如果有的話就直接使用它。第二步,如果 a 不在 testObject 中,也就是 無法在物件本身中找到需要的屬性,就會繼續訪問物件的 [[prototype]] 鏈。

例子:

 var anotherObject  = {
a : 2
}; var testObject = Object.create(anotherObject); console.log(testObject.a); //

Object.create() 方法會建立一個物件並把這個物件的 [[prototype]] 關聯 到指定的物件(anotherObject)。
例子中,testObject 物件的 [[prototype]] 關聯到了 anotherObject。 testObject 本身並沒有 a 屬性,然而還是可以 console 出testObject.a 為 2,這是 [[Get]] 從 testObject 的 [[prototype]] 鏈中找到的,即 anotherObject 中的屬性a。但是倘若 anotherObject中也沒有屬性 a,並且 [[prototype]]不為空,就會繼續查詢下去。這個過程會持續到找到匹配的屬性名,或者查詢完整條 [[prototype]] 鏈未找到,[[Get]] 操作的返回值是 undefined 。

那麼哪裡是原型鏈的盡頭呢?
所有的普通的 [[prototype]] 鏈最終都會指向 內建的 Object.prototype 。

2、“類”:在上一部分的內容中,我們已經說到,javascript 中其實是沒有傳統意義上的“類”的,但我們一直在試圖模仿類,主要是利用了 函式的一種特殊特性:所有的函式預設都會有一個名為 prototype 的公有並且不可列舉的屬性,它會指向另一個物件。
例子:

 function foo(){
//…
} foo.prototype; // {} var a = new foo();
Object.getPrototypeOf(a) === foo.prototype;//true

這個物件是在呼叫 new foo () 時建立的,最後會被關聯到 foo.prototype 上。就像例子中的呼叫 new foo () 時會建立 a ,然後 將 a 內部的 [[prototype]] 連結到 foo.prototype 所指向的物件。

在傳統的面向類的語言中,類可以被複制多次,每次例項化的過程都是一次複製。但在 javascript 中沒有類似的複製機制。你不能建立一個類的多個例項,只能建立多個物件,他們的 [[prototype]] 關聯的是同一個物件。因為在預設情況下,並不會進行復制,所以這些物件之間並不會完全失去聯絡,他們是互相關聯的。

就像上面的例子 new foo () 會生成一個新的物件,稱為 a,這個新的物件的內部的 [[prototype]] 關聯的是 foo.prototype 物件。最後我們得到兩個物件,他們之間互相關聯。我們並沒有真正意義上初始化一個類,實際上我們並沒有從 “類” 中複製任何行為到一個物件中,只是讓兩個物件互相關聯著。

再強調一下: 在 javascript 中,並不會將一個物件(“類”)複製到另一個物件(“例項”),只是將它們關聯起來。看一個圖:

箭頭表示 關聯。
這個圖就表達了 [[prototype]] 機制,即 原型繼承。
但是說是 繼承其實是不準確的,因為傳統面向類的語言中 繼承 意味著複製操作,而 javascript (預設)並不會複製物件屬性,而是在兩個物件之間建立一個關聯,這樣一個物件可以 委託 訪問另一個物件的屬性和函式。委託 可以更加準確的描述 javascript 中物件的關聯機制。

3、 (原型)繼承
來看個例子:

 function foo(name){
this.name = name;
} foo.prototype.myName = function(){
return this.name;
} function bar(name,label){
foo.call(this,name);
this.label = label;
} // 建立了一個新的 bar.prototype 物件並把它關聯到了 foo.prototype。
bar.prototype = Object.create(foo.prototype); bar.prototype.myLabel = function(){
return this.label;
} var a = new Bar(“a”,”obj a”);
console.log(a.name) // “a"
console.log(a.label) // "obj a"

宣告 function bar(){} 時,bar 會有一個預設的 .prototype 關聯到預設的物件,但是這個物件不是我們想要的 foo.prototype 。因此 我們通過 Object.create() 建立了一個新的物件並把它關聯到我們希望的物件上,即 foo.prototype,直接把原始的關聯物件拋棄掉。

如果你說為什麼不用下面這種方式關聯?

bar.prototype = foo.prototype

因為 這種方式並不會建立一個關聯到 foo.prototype 的新物件,它只是讓 bar.prototype 直接引用 foo.prototype 。因此當你執行 bar.prototype.myLabel 的賦值語句時會直接修改 foo.prototype 物件本身。

或者說為什麼不用new?

bar.prototype = new foo();

這樣的確會建立一個關聯到 foo.prototype 的新物件。 但是它同時 也執行了對 foo 函式的呼叫,如果 foo 函式中有給this新增屬性、修改狀態、寫日誌等,就會影響到 bar() 的 “後代” 。

這裡補充兩點關於 new ,方便理解:

function foo(){
console.log(“test”);
} var a = new foo(); // test

當你執行 var a = new foo(); 也就是使用 new 來呼叫函式,會執行下面四步操作:
1、建立一個全新的物件
2、這個新物件會被執行 [[prtotype]] 連線
3、這個新物件會繫結到函式呼叫的 this 上
4、如果函式沒有返回值,那麼 new 表示式中的函式呼叫會自動返回這個新物件。

另一點,當你執行 var a = new foo(); 時 ,console 打出 test。foo 只是個普通的函式,當使用 new 呼叫時,它就會創造一個新物件並賦值給 a,當然也會呼叫自身。

綜上,要建立一個合適的關聯物件,最好的方式就是用 Object.create(),這樣做也有缺點:就是建立了新物件,然後把舊物件拋棄掉,不能直接修改預設的已有物件了。
Object.create() 會建立一個 擁有空 [[prototype]] 連線的物件。它是 es5 新增的方法,讓我們來看看在老的環境中如何實現它:

if(!Object.create){
Object.create = function(o){
function F(){}
F.prototype = o;
return new F();
}
}

我們使用了一個空函式 F,通過改寫它的 .prototype 屬性使其指向想要關聯的物件,然後再使用 new F() 來構造一個新物件來進行關聯。

三、類式繼承設計模式 和 委託設計模式
這兩種模式都是用來實現繼承,本質上也就是 關聯。

1、類式繼承設計模式:
這個應該是大家最熟悉的,主要就是運用建構函式和原型鏈實現繼承,也就是所謂的面向物件風格。

 function Foo(who){
this.me = who;
} Foo.prototype.identify = function(){
return "i am “ + this.me;
} function Bar(who){
Foo.call(this,who);
} Bar.prototype = object.create(Foo.prototype); Bar.prototype.speak = function(){
alert(“Hello,”+ this.identify()+”.”);
} var b1 = new Bar(“b1”);
var b2 = new Bar(“b2”); b1.speak();
b2.speak();

子類 Bar 繼承了 父類 Foo,然後生成了 b1 和 b2 兩個例項。 b1 繼承了 Bar. prototype , Bar.prototype 繼承了 Foo.prototype。

2、 委託設計模式
物件關聯風格:

 Foo = {
init:function(who){
this.me = who;
},
identify:function(){
return “i am” +this.me;
}
}; Bar = Object.create(Foo);
Bar.speak = function(){
alert(“hello,” + this.identify())
}; var b1 = Object.create(Bar);
b1.init(“b1”);
var b2 = Object.create(Bar);
b2.init(“b2”); b1.speak();
b2.speak();

這段程式碼同樣 利用 [[prototype]] 把 b1 委託給 Bar 並把 Bar 委託給 Foo,和上一段程式碼一摸一樣,同樣實現了三個物件的關聯。

3、以上兩種模式都實現了三個物件的關聯,那麼它們的區別是什麼呢?
首先是思維方式的不同:
      類式繼承設計模式:定義一個通用的父類,可以將其命名為 Task,在 Task 中定義所有任務都有的行為。接著定義子類 A 和 B,他們都繼承子 Task,並且會新增一些特殊的行為來處理對應的人物。然後你例項化子類,這些例項擁有 父類 Task 的通用方法,也擁有 子類 A 的特殊行為。

委託設計模式:首先定義一個名為Task 的物件,它包含所有任務都可以使用的行為。接著對於每個任務 A 和 B,都會定義一個物件來儲存對應的資料和行為。執行 任務 A 需要兩個兄弟物件(Task 和 A)協作完成,只是在需要某些通用行為的時候 可以允許 A 物件委託給 Task。在上面的例子中,也就是 Bar 通過 Object.create(Foo); 建立,它的 [[prototype]] 委託給了 Foo 物件。這就是一種物件關聯的風格。委託行為意味著某些物件(Bar)在找不到屬性或者方法引用時會把這個請求委託給另一個物件(Foo)。

委託設計模式不是按照父類到子類的關係垂直組織的,而是通過任意方向的委託關聯並排組織的。

其次,程式碼實現明顯不同,可以感覺到 委託設計模式,也就是物件關聯風格更加簡潔,這種設計模式只關注物件之間的關聯關係。
最後,委託設計模式更加貼近 javascript 的 “繼承”機制 —— 委託機制。

ε-(´∀`; )

學習並感謝
《你不知道的JavaScript》上卷  (炒雞推薦大家看)