前言
一個多月前,滷煮讀了一篇翻譯過來的外國人寫的技術部落格。此君在部落格中將js中的類(構造)繼承和原型繼承做了一些比較,並且得出了結論:建議諸位在開發是用原型繼承。文中提到了各種原型繼承的優點,詳細的露珠不一一說明介紹了。這篇文章的名字是為什麼原型繼承很重要,有興趣的同學可以去看,此文有些深度,值得反覆閱讀。今天這篇文章也來談談js中的繼承方式以及它們的優缺點。
類式繼承(建構函式)
JS中其實是沒有類的概念的,所謂的類也是模擬出來的。特別是當我們是用new 關鍵字的時候,就使得“類”的概念就越像其他語言中的類了。類式繼承是在函式物件內呼叫父類的建構函式,使得自身獲得父類的方法和屬性。call和apply方法為類式繼承提供了支援。通過改變this的作用環境,使得子類本身具有父類的各種屬性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
var father = function() { this.age = 52; this.say = function() { alert('hello i am '+ this.name ' and i am '+this.age + 'years old'); } } var child = function() { this.name = 'bill'; father.call(this); } var man = new child(); man.say(); |
原型繼承
原型繼承在開發中經常用到。它有別於類繼承是因為繼承不在物件本身,而在物件的原型上(prototype)。每一個物件都有原型,在瀏覽器中它體現在一個隱藏的__proto__屬性上。在一些現代瀏覽器中你可以更改它們。比如在zepto中,就是通過新增zepto的fn物件到一個空的陣列的__proto__屬性上去,從而使得該陣列成為一個zepto物件並且擁有所有的方法。話說回來,當一個物件需要呼叫某個方法時,它回去最近的原型上查詢該方法,如果沒有找到,它會再次往下繼續查詢。這樣逐級查詢,一直找到了要找的方法。 這些查詢的原型構成了該物件的原型鏈條。原型最後指向的是null。我們說的原型繼承,就是將父對像的方法給子類的原型。子類的建構函式中不擁有這些方法和屬性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
var father = function() { } father.prototype.a = function() { } var child = function(){} //開始繼承 child.prototype = new father(); var man = new child(); man.a(); |
可以看到第七行實現了原型繼承。很多人並不陌生這種方式。通過在瀏覽器中列印man我們就可以檢視各個原型的繼承關係。
可以看到逐級的關係child->object(father例項化的物件)->father。child是通過中間層繼承了father的原型上的東西的。但是為什麼中間還有一層object呢,為什麼不把child.prototype = father.prototype。答案是如果這樣做child和father就沒有區別了。大家應該還記得在prototype中有個constructor屬性,指向的是建構函式。按照正常的情況我們要把constructor的值改回來指向child的建構函式。但如果直接把father.prototype賦值給child.prototype,那麼constructor應該指向誰呢?所以很顯然只能通過中間層才能使得child和father保持為獨立的物件。
對比
和原型對比起來,建構函式(類)式繼承有什麼不一樣呢?首先,建構函式繼承的方法都會存在父物件之中,每一次例項,都回將funciton儲存在記憶體中,這樣的做法毫無以為會帶來效能上的問題。其次類式繼承是不可變的。在執行時,無法修改或者新增新的方法,這種方式是一種固步自封的死方法。而原型繼承是可以通過改變原型連結而對子類進行修改的。另外就是類式繼承不支援多重繼承,而對於原型繼承來說,你只需要寫好extend對物件進行擴充套件即可。
組合模式
另外的一種模式,是結合類繼承和原型繼承的各自優點來進行對父類的繼承。用類式繼承屬性,而原型繼承方法。這種模式避免了屬性的公用,因為一般來說,每一個子類的屬性都是私有的,而方法得到了統一。這種模式稱為組合模式,也是繼承類式常用到的一種方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
function father() { this.a = 'father' } father.prototype.b = function() { alert(this.a) } var child = function() { father.call(this) } child.prototype = new father(); |
new 關鍵字和Obeject.create方法
在文章中,博主指出了使用new關鍵字的弊端。他說:“new關鍵字掩蓋了Javascript中真正的原型繼承,使得它更像是基於類的繼承。其實new關鍵字只是Javascript在為了獲得流行度而加入與Java類似的語法時期留下來的一個殘留物”。作者推薦我們使用Object.create方法建立或者例項化物件。露珠做過測試,使用new和使用object.create方法都是將物件新增到原型上去。我們可以看一下程式碼:
1 2 3 4 5 6 7 8 9 |
var father = function() { this.a = 'father' } father.prototype.b = function() {alert(this.a)} var obj = new father(); |
在瀏覽器中列印obj,可以觀察它的結構。它本身是一個物件,有自身屬性,同時在其__proto__熟悉上也有b方法。在__proto__的後面有father,可以看出原型是自father來的。
那麼objcet.create方法呢,我們也可以通過下面程式碼測試之:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
var father = { a: 'father', b: function() { alert(this.a); } } var obj = Object.create(father); console.dir(obj) |
下面是瀏覽器輸出的結果:
可以看到,用create的方法構造出來的物件,a屬性和b方法都是在物件的原型上,也就是說我們可以通過更改father的屬性動態改變obj的原型上的方法和屬性,而上面通過new關鍵字用建構函式生成的例項,a屬性是無法改變的。從這裡,我們也可以看到類繼承和原型基礎的一些區別。
結論
原型繼承比較符合js這種語言的特點。因為它本身就是js強大的原型的一部分。而類式繼承,與其稱它為繼承方式,毋寧說是一種函式的運用技巧來模擬繼承罷了。本文是滷煮的一己之見,錯誤偏頗在所難免,如果有之,請各位斧正。