歡迎光臨
每天分享高質量文章

繼承的實現方式及原型概述

作者:名一的博客

網址:http://segmentfault.com/a/1190000002989513

點擊“閱讀原文”可查看本文網頁版

對於 OO 語言,有一句話叫“Everything is object”,雖然 JavaScript 不是嚴格意義上的面向物件語言,但如果想要理解 JS 中的繼承,這句話必須時刻銘記於心。

JS 的語法非常靈活,所以有人覺得它簡單,因為怎麼寫都是對的;也有人覺得它難,因為很難解釋某些語法的設計,誰能告訴我為什麼 typeof null 是 object 而 typeof undefined 是 undefined 嗎?並且這是在 null == undefined 的前提下。很多我們自認為“懂”了的知識點,細細琢磨起來,還是會發現有很多盲點,“無畏源於無知”吧……

1. 簡單物件

既然是講繼承,自然是從最簡單的物件說起:

var dog = {

name: ‘tom’

}

這便是物件直接量了。每一個物件直接量都是 Object 的子類,即

dog instanceof Object; // true

2. 建構式

JS 中的建構式與普通函式並沒有什麼兩樣,只不過在呼叫時,前面加上了 new 關鍵字,就當成是建構式了。

function Dog(name) {

this.name = name;

}

var dog = new Dog(‘tom’);

dog instanceof Dog; // true

兩個問題,第一,不加 new 關鍵字有什麼後果?

那麼 Dog 函式中的 this 在背景關係(Context)中被解釋為全域性變數,具體在瀏覽器端的話是 window 物件,在 node 環境下是一個 global 物件。

第二,dog 的值是什麼?很簡單,undefined 。Dog 函式沒有傳回任何值,執行結束後,dog 的值自然是 undefined 。

關於 new 的過程,這裡也順便介紹一下,這個對後面理解原型(prototype)有很大的幫助:

  1. 創建一個空的物件,僅包含 Object 的屬性和方法。
  2. 將 prototype 中的屬性和方法創建一份取用,賦給新物件。
  3. 將 this 上的屬性和方法新建一份,賦給新物件。
  4. 傳回 this 物件,忽略 return 陳述句。

需要明確的是,prototype 上的屬性和方法是實體間共享的,this 上的屬性和方法是每個實體獨有的。

3. 引入 prototype

現在為 Dog 函式加上 prototype,看一個例子:

function Dog(name) {

this.name = name;

this.bark = function() {};

}

Dog.prototype.jump = function() {};

Dog.prototype.species = ‘Labrador’;

Dog.prototype.teeth = [‘1’, ‘2’, ‘3’, ‘4’];

var dog1 = new Dog(‘tom’),

dog2 = new Dog(‘jerry’);

dog1.bark !== dog2.bark; // true

dog1.jump === dog2.jump; // true

dog1.teeth.push(‘5’);

dog2.teeth; // [‘1’, ‘2’, ‘3’, ‘4’, ‘5’]

看到有註釋的那三行應該可以明白“取用”和“新建”的區別了。

那麼我們經常說到的“原型鏈”到底是什麼呢?這個術語出現在繼承當中,它用於表示物件實體中的屬性和方法來自於何處(哪個父類)。好吧,這是筆者的解釋。

– Object

bark: Dog/this.bark()

name: ‘tom’

– __proto__: Object

jump: Dog.prototype.jump()

species: ‘Labrador’

+ teeth: Array[4]

+ constructor: Dog()

+ __proto__: Object

上面的是 dog1 的原型鏈,不知道夠不夠直觀地描述“鏈”這個概念。

其中,bark 和 name 是定義在 this 中的,所以最頂層可以看到它倆。

然後,每一個物件都會有一個 __proto__ 屬性(IE 11+),它表示定義在原型上的屬性和方法,所以 jump、species 和 teeth 自然就在這兒了。

最後就一直向上找 __proto__ 中的屬性和方法。

4. 繼承的幾種實現

4.1 通過 call 或者 apply

繼承在編程中有兩種說法,一個叫 inherit,另一個是 extend 。前者是嚴格意義上的繼承,即存在父子關係,而後者僅僅是一個類擴展了另一個類的屬性和方法。那麼 call 和 apply 就屬於後者的範疇。怎麼說?

function Animal(gender) {

this.gender = gender;

}

function Dog(name, gender) {

Animal.call(this, gender);

this.name = name;

}

var dog = new Dog(‘tom’, ‘male’);

dog instanceof Animal; // false

雖然在 dog 物件中有 gender 屬性,但 dog 卻不是 Animal 型別。甚至,這種方式只能“繼承”父類在 this 上定義的屬性和方法,並不能繼承 Animal.prototype 中的屬性和方法。

4.2 通過 prototype 實現繼承

要實現繼承,必須包含“原型”的概念。下麵是很常用的繼承方式。

function Dog(name) {

Animal.call(this);

}

Dog.prototype = new Animal(); // 先假設 Animal 函式沒有引數

Dog.prototype.constructor = Dog;

var dog = new Dog(‘tom’);

dog instanceof Animal; // true

繼承的結果有兩個:一、獲得父類的屬性和方法;二、正確通過 instanceof 的測試。

prototype 也是物件,它是創建實體時的裝配機,這個在前面有提過。new Animal() 的值包含 Animal 實體所有的屬性和方法,既然它賦給了 Dog 的 prototype,那麼 Dog 的實體自然就獲得了父類的所有屬性和方法。

並且,通過這個例子可以知道,改變 Dog 的 prototype 屬性可以改變 instanceof 的測試結果,也就是改變了父類。

然後,為什麼要在 Dog 的建構式中呼叫 Animal.call(this)?

因為 Animal 中可能在 this 上定義了方法和函式,如果沒有這句話,那麼所有的這一切都會給到 Dog 的 prototype 上,根據前面的知識我們知道,prototype 中的屬性和方法在實體間是共享的。

我們希望將這些屬性和方法依然保留在實體自身的空間,而不是共享,因此需要重寫一份。

至於為什麼要修改 constructor,只能說是為了正確的顯示原型鏈吧,它並不會影響 instanceof 的判斷。或者有其他更深的道理我並不知道……

4.3 利用空物件實現繼承

上面的繼承方式已經近乎完美了,除了兩點:

一、Animal 有構造引數,並且使用了這些引數怎麼辦?

二、在 Dog.prototype 中多了一份定義在 Animal 實體中冗餘的屬性和方法。

function Animal(name) {

name.doSomething();

}

function Dog(name) {

Animal.call(this, name);

}

Dog.prototype = new Animal(); // 由於沒有傳入name變數,在呼叫Animal的建構式時,會出錯

Dog.prototype.constructor = Dog;

這個問題可以通過一個空物件來解決(改自 Douglas Crockford)。

function DummyAnimal() {}

DummyAnimal.prototype = Animal.prototype;

Dog.prototype = new DummyAnimal();

Dog.prototype.constructor = Dog;

他的原始方法是下麵的 object:

function object(o) {

function F() {}

F.prototype = o;

return new F();

}

Dog.prototype = object(Animal.prototype);

Dog.prototype.constructor = Dog;

4.4 利用 __proto__ 實現繼承

現在就只剩下一個問題了,如何把冗餘屬性和方法去掉?

其實,從第 3 小節介紹原型的時候就提到了 __proto__ 屬性,instanceof 運算子是通過它來判斷是否屬於某個型別的。

所以我們可以這麼繼承:

function Dog() {

Animal.call(this);

}

Dog.prototype = {

__proto__: Animal.prototype,

constructor: Dog

};

如果不考慮兼容性的話,這應該是從 OO 的角度來看最貼切的繼承方式了。

4.5 拷貝繼承

這個方式也只能稱之為 extend 而不是 inherit,所以也沒必要展開說。

像 Backbone.Model.extend、jQuery.extend 或者 _.extend 都是拷貝繼承,可以稍微看一下它們是怎麼實現的。(或者等我自己再好好研究之後過來把這部分補上吧)

5. 個人小結

當我們在討論繼承的實現方式時,給我的感覺就像孔乙己在炫耀“茴香豆”的“茴”有幾種寫法一樣。繼承是 JS 中占比很大的一塊內容,所以很多庫都有自己的實現方式,它們並沒有使用我認為的“最貼切”的方法,為什麼?JS 就是 JS,它生來就設計得非常靈活,所以我們為什麼不利用這個特性,而非得將 OO 的做法強加於它呢?

通過繼承,我們更多的是希望獲得父類的屬性和方法,至於是否要保證嚴格的父類/子類關係,很多時候並不在乎,而拷貝繼承最能體現這一點。對於基於原型的繼承,會在代碼中看到各種用 function 定義的型別,而拷貝繼承更通用,它只是將一個物件的屬性和方法拷貝(擴展)到另一個物件而已,並不關心原型鏈是什麼。

當然,在我鼓吹拷貝繼承多麼多麼好時,基於原型的繼承自然有它不可取代的理由。所以具體問題得具體分析,當具體的使用場景沒定下來時,就不存在最好的方法。

個人見解,能幫助大家更加理解繼承一點就最好,如果有什麼不對的,請多多指教!

赞(0)

分享創造快樂