Javascript 「繼承」
是時候寫一寫 「繼承」了,為什麼加引號,因為當你閱讀完這篇文章,你會知道,說是 繼承 其實是不準確的。
一、類
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]] 操作就是從對象中找到你要的屬性。然而他是怎麼找的呢?
例子:
1 var testObject = {
2 a:2
3 };
4 console.log(testObject.a) // 2
在上面的代碼中,當你 console 的時候,會觸發 [[Get]] 操作,查找 testObject 中的 a 屬性。對於默認的 [[Get]]操作,第一步是檢查對象本身是否有這個屬性,如果有的話就直接使用它。第二步,如果 a 不在 testObject 中,也就是 無法在對象本身中找到需要的屬性,就會繼續訪問對象的 [[prototype]] 鏈。
例子:
1 var anotherObject = {
2 a : 2
3 };
4
5 var testObject = Object.create(anotherObject);
6
7 console.log(testObject.a); // 2
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 的公有並且不可枚舉的屬性,它會指向另一個對象。例子:
1 function foo{
2 //…
3 }
4
5 foo.prototype; // {}
6
7 var a = new foo;
8 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、 (原型)繼承
來看個例子:
1 function foo(name){
2 this.name = name;
3 }
4
5 foo.prototype.myName = function{
6 return this.name;
7 }
8
9 function bar(name,label){
10 foo.call(this,name);
11 this.label = label;
12 }
13
14 // 創建了一個新的 bar.prototype 對象並把它關聯到了 foo.prototype。
15 bar.prototype = Object.create(foo.prototype);
16
17 bar.prototype.myLabel = function{
18 return this.label;
19 }
20
21 var a = new Bar(「a」,」obj a」);
22 console.log(a.name) // 「a"
23 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、類式繼承設計模式:這個應該是大家最熟悉的,主要就是運用構造函數和原型鏈實現繼承,也就是所謂的面向對象風格。
1 function Foo(who){
2 this.me = who;
3 }
4
5 Foo.prototype.identify = function{
6 return "i am 「 + this.me;
7 }
8
9 function Bar(who){
10 Foo.call(this,who);
11 }
12
13 Bar.prototype = object.create(Foo.prototype);
14
15 Bar.prototype.speak = function{
16 alert(「Hello,」+ this.identify+」.」);
17 }
18
19 var b1 = new Bar(「b1」);
20 var b2 = new Bar(「b2」);
21
22 b1.speak;
23 b2.speak;
子類 Bar 繼承了 父類 Foo,然後生成了 b1 和 b2 兩個實例。 b1 繼承了 Bar. prototype , Bar.prototype 繼承了 Foo.prototype。
2、 委託設計模式對象關聯風格:
1 Foo = {
2 init:function(who){
3 this.me = who;
4 },
5 identify:function{
6 return 「i am」 +this.me;
7 }
8 };
9
10 Bar = Object.create(Foo);
11 Bar.speak = function{
12 alert(「hello,」 + this.identify)
13 };
14
15 var b1 = Object.create(Bar);
16 b1.init(「b1」);
17 var b2 = Object.create(Bar);
18 b2.init(「b2」);
19
20 b1.speak;
21 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》上卷 (炒雞推薦大家看)
TAG:達人科技 |