摘要:使用構(gòu)造函數(shù)的原型繼承相比使用原型的原型繼承更加復(fù)雜,我們先看看使用原型的原型繼承上面的代碼很容易理解。相反的,使用構(gòu)造函數(shù)的原型繼承像下面這樣當(dāng)然,構(gòu)造函數(shù)的方式更簡(jiǎn)單。
五天之前我寫(xiě)了一個(gè)關(guān)于ES6標(biāo)準(zhǔn)中Class的文章。在里面我介紹了如何用現(xiàn)有的Javascript來(lái)模擬類(lèi)并且介紹了ES6中類(lèi)的用法,其實(shí)它只是一個(gè)語(yǔ)法糖。感謝Om Shakar以及Javascript Room中的各位,我的編程風(fēng)格從那時(shí)候開(kāi)始發(fā)生了改變;就像Dougla Crockford2006年做的一樣,我也學(xué)習(xí)了很多來(lái)完全理解基于原型的編程方式。
Javascript是一個(gè)多樣化的編程語(yǔ)言。它擁有面向?qū)ο蠛秃瘮?shù)式的編程特點(diǎn),你可以使用任何一種風(fēng)格來(lái)編寫(xiě)代碼。然而這兩個(gè)編程風(fēng)格并不能很好的融合。例如,你不無(wú)法同時(shí)使用new(典型的面向?qū)ο蟮奶攸c(diǎn))和apply(函數(shù)式編程的特點(diǎn)).原型繼承一直都作為連接這兩種風(fēng)格的橋梁。
基于類(lèi)繼承的問(wèn)題大部分Javascript程序員會(huì)告訴你基于類(lèi)的繼承不好。然而它們中只有很少一部分知道其中的原因。事實(shí)實(shí)際上是基于類(lèi)的基礎(chǔ)并沒(méi)有什么不好。Python是基于類(lèi)繼承的,并且它是一門(mén)很好的編程語(yǔ)言。但是,基于類(lèi)的繼承并不適合用于Javascript。Python正確的使用了類(lèi),它們只有簡(jiǎn)單的工廠方法不能當(dāng)成構(gòu)造函數(shù)使用。而在Javascript中任何函數(shù)都可以被當(dāng)成構(gòu)造函數(shù)使用。
Javascript中的問(wèn)題是由于每個(gè)函數(shù)都可以被當(dāng)成構(gòu)造函數(shù)使用,所以我們需要區(qū)分普通的函數(shù)調(diào)用和構(gòu)造函數(shù)調(diào)用;我們一般使用new關(guān)鍵字來(lái)進(jìn)行區(qū)別。然而,這樣就破壞了Javascript中的函數(shù)式特點(diǎn),因?yàn)?b>new是一個(gè)關(guān)鍵字而不是函數(shù)。因而函數(shù)式的特點(diǎn)無(wú)法和對(duì)象實(shí)例化一起使用。
function Person(firstname,lastname){ this.firstname = firstname ; this.lastname = lastname ; }
考慮上面這段程序。你可以通過(guò)new關(guān)鍵字來(lái)調(diào)用Person方法來(lái)創(chuàng)建一個(gè)函數(shù)Person的實(shí)例:
var author = new Person("Aadit","Shah") ;
然而,沒(méi)有任何辦法來(lái)使用apply方法來(lái)為構(gòu)造函數(shù)指定參數(shù)列表:
var author = new Person.apply(null,["Aadit","Shah"]);//error
但是,如果new是一個(gè)方法那么上面的需求就可以通過(guò)下面這種方式實(shí)現(xiàn)了:
var author = Person.new.apply(Person,["Aadit","Shah"]) ;
幸運(yùn)的是,因?yàn)镴avascript有原型繼承,所以我們可以實(shí)現(xiàn)一個(gè)new的函數(shù):
Function.prototype.new = function () { function functor() { return constructor.apply(this, args); } var args = Array.prototype.slice.call(arguments); functor.prototype = this.prototype; var constructor = this; return new functor; };
在像Java這樣對(duì)象只能通過(guò)new關(guān)鍵字來(lái)實(shí)例化的語(yǔ)言中,上面這種方式是不可能實(shí)現(xiàn)的。
下面這張表列出了原型繼承相比于基于類(lèi)的基礎(chǔ)的優(yōu)點(diǎn):
基于類(lèi)的繼承 | 原型繼承 |
---|---|
類(lèi)是不可變的。在運(yùn)行時(shí),你無(wú)法修改或者添加新的方法 | 原型是靈活的。它們可以是不可變的也可以是可變的 |
類(lèi)可能會(huì)不支持多重繼承 | 對(duì)象可以繼承多個(gè)原型對(duì)象 |
基于類(lèi)的繼承比較復(fù)雜。你需要使用抽象類(lèi),接口和final類(lèi)等等 | 原型繼承比較簡(jiǎn)潔。你只有對(duì)象,你只需要對(duì)對(duì)象進(jìn)行擴(kuò)展就可以了 |
到現(xiàn)在你應(yīng)該知道為什么我覺(jué)得new關(guān)鍵字是不會(huì)的了吧---你不能把它和函數(shù)式特點(diǎn)混合使用。然后,這并不代表你應(yīng)該停止使用它。new關(guān)鍵字有合理的用處。但是我仍然建議你不要再使用它了。new關(guān)鍵字掩蓋了Javascript中真正的原型繼承,使得它更像是基于類(lèi)的繼承。就像Raynos說(shuō)的:
new是Javascript在為了獲得流行度而加入與Java類(lèi)似的語(yǔ)法時(shí)期留下來(lái)的一個(gè)殘留物
Javascript是一個(gè)源于Self的基于原型的語(yǔ)言。然而,為了市場(chǎng)需求,Brendan Eich把它當(dāng)成Java的小兄弟推出:
并且我們當(dāng)時(shí)把Javascript當(dāng)成Java的一個(gè)小兄弟,就像在微軟語(yǔ)言家庭中Visual Basic相對(duì)于C++一樣。
這個(gè)設(shè)計(jì)決策導(dǎo)致了new的問(wèn)題。當(dāng)人們看到Javascript中的new關(guān)鍵字,他們就想到類(lèi),然后當(dāng)他們使用繼承時(shí)就遇到了傻了。就像Douglas Crockford說(shuō)的:
這個(gè)間接的行為是為了使傳統(tǒng)的程序員對(duì)這門(mén)語(yǔ)言更熟悉,但是卻失敗了,就像我們看到的很少Java程序員選擇了Javascript。Javascript的構(gòu)造模式并沒(méi)有吸引傳統(tǒng)的人群。它也掩蓋了Javascript基于原型的本質(zhì)。結(jié)果就是,很少的程序員知道如何高效的使用這門(mén)語(yǔ)言
因此我建議停止使用new關(guān)鍵字。Javascript在傳統(tǒng)面向?qū)ο蠹傧笙旅嬗兄訌?qiáng)大的原型系統(tǒng)。然大部分程序員并沒(méi)有看見(jiàn)這些還處于黑暗中。
理解原型繼承原型繼承很簡(jiǎn)單。在基于原型的語(yǔ)言中你只有對(duì)象。沒(méi)有類(lèi)。有兩種方式來(lái)創(chuàng)建一個(gè)新對(duì)象---“無(wú)中生有”對(duì)象創(chuàng)建法或者通過(guò)現(xiàn)有對(duì)象創(chuàng)建。在Javascript中Object.create方法用來(lái)創(chuàng)建新的對(duì)象。新的對(duì)象之后會(huì)通過(guò)新的屬性進(jìn)行擴(kuò)展。
“無(wú)中生有”對(duì)象創(chuàng)建法Javascript中的Object.create方法用來(lái)從0開(kāi)始創(chuàng)建一個(gè)對(duì)象,像下面這樣:
var object = Object.create(null) ;
上面例子中新創(chuàng)建的object沒(méi)有任何屬性。
克隆一個(gè)現(xiàn)有的對(duì)象Object.create方法也可以克隆一個(gè)現(xiàn)有的對(duì)象,像下面這樣:
var rectangle = { area : function(){ return this.width * this.height ; } } ; var rect = Object.create(rectangle) ;
上面例子中rect從rectangle中繼承了area方法。同時(shí)注意到rectangle是一個(gè)對(duì)象字面量。對(duì)象字面量是一個(gè)簡(jiǎn)潔的方法用來(lái)創(chuàng)建一個(gè)Object.prototype的克隆然后用新的屬性來(lái)擴(kuò)展它。它等價(jià)于:
var rectangle = Object.create(Object.prototype) ; rectangle.area = function(){ return this.width * this.height ; } ;擴(kuò)展一個(gè)新創(chuàng)建的對(duì)象
上面的例子中我們克隆了rectangle對(duì)象命名為rect,但是在我們使用rect的area方法之前我們需要擴(kuò)展它的width和height屬性,像下面這樣:
rect.width = 5 ; rect.height = 10 ; alert(rect.area()) ;
然而這種方式來(lái)創(chuàng)建一個(gè)對(duì)象的克隆然后擴(kuò)展它是一個(gè)非常傻缺的方法。我們需要在每個(gè)rectangle對(duì)象的克隆上手動(dòng)定義width和height屬性。如果有一個(gè)方法能夠?yàn)槲覀儊?lái)完成這些工作就很好了。是不是聽(tīng)起來(lái)有點(diǎn)熟悉?確實(shí)是。我要來(lái)說(shuō)說(shuō)構(gòu)造函數(shù)。我們把這個(gè)函數(shù)叫做create然后在rectangle對(duì)象上定義它:
var rectangle = { create : function(width,height){ var self = Object.create(this) ; self.height = height ; self.width = width ; return self ; } , area : function(){ return this.width * this.height ; } } ; var rect = rectangle.create(5,10) ; alert(rect.area()) ;構(gòu)造函數(shù) VS 原型
等等。這看起來(lái)很像Javascript中的正常構(gòu)造模式:
function Rectangle(width, height) { this.height = height; this.width = width; } ; Rectangle.prototype.area = function () { return this.width * this.height; }; var rect = new Rectangle(5, 10); alert(rect.area());
是的,確實(shí)很像。為了使得Javascript看起來(lái)更像Java原型模式被迫屈服于構(gòu)造模式。因此每個(gè)Javascript中的函數(shù)都有一個(gè)prototype對(duì)象然后可以用來(lái)作為構(gòu)造器(這里構(gòu)造器的意思應(yīng)該是說(shuō)新的對(duì)象是在prototype對(duì)象的基礎(chǔ)上進(jìn)行構(gòu)造的)。new關(guān)鍵字允許我們把函數(shù)當(dāng)做構(gòu)造函數(shù)使用。它會(huì)克隆構(gòu)造函數(shù)的prototype屬性然后把它綁定到this對(duì)象中,如果沒(méi)有顯式返回對(duì)象則會(huì)返回this。
原型模式和構(gòu)造模式都是平等的。因此你也許會(huì)懷疑為什么有人會(huì)困擾于是否應(yīng)該使用原型模式而不是構(gòu)造模式。畢竟構(gòu)造模式比原型模式更加簡(jiǎn)潔。但是原型模式相比構(gòu)造模式有許多優(yōu)勢(shì)。具體如下:
構(gòu)造模式 | 原型模式 |
---|---|
函數(shù)式特點(diǎn)無(wú)法與new關(guān)鍵字一起使用 | 函數(shù)式特點(diǎn)可以與create結(jié)合使用 |
忘記使用new會(huì)導(dǎo)致無(wú)法預(yù)期的bug并且會(huì)污染全局變量 | 由于create是一個(gè)函數(shù),所以程序總是會(huì)按照預(yù)期工作 |
使用構(gòu)造函數(shù)的原型繼承比較復(fù)雜并且混亂 | 使用原型的原型繼承簡(jiǎn)潔易懂 |
最后一點(diǎn)可能需要解釋一下。使用構(gòu)造函數(shù)的原型繼承相比使用原型的原型繼承更加復(fù)雜,我們先看看使用原型的原型繼承:
var square = Object.create(rectangle); square.create = function (side) { return rectangle.create.call(this, side, side); } ; var sq = square.create(5) ; alert(sq.area()) ;
上面的代碼很容易理解。首先我們創(chuàng)建一個(gè)rectangle的克隆然后命名為square。接著我們用新的create方法重寫(xiě)square對(duì)象的create方法。最終我們從新的create方法中調(diào)用rectangle的create函數(shù)并且返回對(duì)象。相反的,使用構(gòu)造函數(shù)的原型繼承像下面這樣:
function Square(){ Rectangle.call(this,side,side) ; } ; Square.prototype = Object.create(Rectangle.prototype) ; Square.prototype.constructor = Square ; var sq = new Square(5) ; alert(sq.area()) ;
當(dāng)然,構(gòu)造函數(shù)的方式更簡(jiǎn)單。然后這樣的話,向一個(gè)不了解情況的人解釋原型繼承就變得非常困難。如果想一個(gè)了解類(lèi)繼承的人解釋則會(huì)更加困難。
當(dāng)使用原型模式時(shí)一個(gè)對(duì)象繼承自另一個(gè)對(duì)象就變得很明顯。當(dāng)使用方法構(gòu)造模式時(shí)就沒(méi)有這么明顯,因?yàn)槟阈枰鶕?jù)其他構(gòu)造函數(shù)來(lái)考慮構(gòu)造繼承。
對(duì)象創(chuàng)建和擴(kuò)展相結(jié)合在上面的例子中我們創(chuàng)建一個(gè)rectangle的克隆然后命名為square。然后我們利用新的create屬性擴(kuò)展它,重寫(xiě)繼承自rectangle對(duì)象的create方法。如果把這兩個(gè)操作合并成一個(gè)就很好了,就像對(duì)象字面量是用來(lái)創(chuàng)建Object.prototype的克隆然后用新的屬性擴(kuò)展它。這個(gè)操作叫做extend,可以像下面這樣實(shí)現(xiàn):
Object.prototype.extend = function(extension){ var hasOwnProperty = Object.hasOwnProperty ; var object = Object.create(this) ; for(var property in extension){ if(hasOwnProperty.call(extension,property) || typeof obejct[property] === "undefined") //這段代碼有問(wèn)題,按照文章意思,這里應(yīng)該使用深復(fù)制,而不是簡(jiǎn)單的淺復(fù)制,deepClone(extension[property],object[property]),deepClone的實(shí)現(xiàn)可以看我之前關(guān)于繼承的博客 object[properyty] = extension[property] ; } return object ; } ;
譯者注:我覺(jué)得博主這里的實(shí)現(xiàn)有點(diǎn)不符合邏輯,正常extend的實(shí)現(xiàn)應(yīng)該是可以配置當(dāng)被擴(kuò)展對(duì)象和用來(lái)擴(kuò)展的對(duì)象屬性重復(fù)時(shí)是否覆蓋原有屬性,而博主的實(shí)現(xiàn)就只是簡(jiǎn)單的覆蓋。同時(shí)博主的實(shí)現(xiàn)在if判斷中的做法個(gè)人覺(jué)得是值得學(xué)習(xí)的,首先判斷extension屬性是否是對(duì)象自身的,如果是就直接復(fù)制到object上,否則再判斷object上是否有這個(gè)屬性,如果沒(méi)有那么也會(huì)把屬性復(fù)制到object上,這種實(shí)現(xiàn)的結(jié)果就使得被擴(kuò)展的對(duì)象不僅僅只擴(kuò)展了extension中的屬性,還包括了extension原型中的屬性。不難理解,extension原型中的屬性會(huì)在extension中表現(xiàn)出來(lái),所以它們也應(yīng)該作為extension所具有的特性而被用來(lái)擴(kuò)展object。所以我對(duì)這個(gè)方法進(jìn)行了改寫(xiě):
Object.prototype.extend = function(extension,override){ var hasOwnProperty = Object.hasOwnProperty ; var object = Object.create(this) ; for(var property in extension){ if(hasOwnProperty.call(extension,property) || typeof object[property] === "undefined"){ if(object[property] !== "undefined"){ if(override){ deepClone(extension[property],object[property]) ; } }else{ deepClone(extension[property],object[property]) ; } } } };
利用上面的extend方法,我們可以重寫(xiě)square的代碼:
var square = rectangle.extend({ create : function(side){ return rectangle.create.call(this,side,side) ; } }) ; var sq = square.create(5) ; alert(sq.area()) ;
extend方法是原型繼承中唯一需要的操作。它是Object.create函數(shù)的超集,因此它可以用在對(duì)象的創(chuàng)建和擴(kuò)展上。因此我們可以用extend來(lái)重寫(xiě)rectangle,使得create函數(shù)更加結(jié)構(gòu)化看起來(lái)就像模塊模式。
var rectangle = { create : function(width,height){ return this.extend({ height : height , width : width }) ; } } ; var rect = rectangle.create(5,10) ; alert(rect.area()) ;原型繼承的兩種方法
一些人可能已經(jīng)注意到extend函數(shù)返回的對(duì)象實(shí)際上是繼承了兩個(gè)對(duì)象的屬性,一個(gè)是被擴(kuò)展的對(duì)象,另一個(gè)是用來(lái)擴(kuò)展的對(duì)象。另外從兩個(gè)對(duì)象繼承屬性的方式也不一樣。第一種情況下是通過(guò)委派來(lái)繼承屬性(也就是使用Object.create()來(lái)繼承屬性),第二種情況下使用合并屬性的方式來(lái)繼承屬性。
委派(差異化繼承)很多Javascript程序員對(duì)于差別繼承比較熟悉。維基百科是這么解釋的:
大部分對(duì)象是從其他更一般的對(duì)象中得到的,只是在一些很小的地方進(jìn)行了修改。每個(gè)對(duì)象通常在內(nèi)部維護(hù)一個(gè)指向其他對(duì)象的引用列表,這些對(duì)象就是該對(duì)象本身進(jìn)行差異化繼承的對(duì)象。
Javascript中的原型繼承是基于差異化繼承的。每個(gè)對(duì)象都有個(gè)內(nèi)部指針叫做[[proto]] (在大部分瀏覽器中可以通過(guò)__proto__屬性訪問(wèn)),這個(gè)指針指向?qū)ο蟮脑?。多個(gè)對(duì)象之間通過(guò)內(nèi)部[[proto]]屬性鏈接起來(lái)形成了原型鏈,鏈的最后指向null。
當(dāng)你試圖獲取一個(gè)對(duì)象的屬性時(shí)Javascript引擎會(huì)首先查找對(duì)象自身的屬性。如果在對(duì)象上沒(méi)找到該屬性,那么它就會(huì)去對(duì)象的原型中去查找。以此類(lèi)推,它會(huì)沿著原型鏈一直查找知道找到或者到原型鏈的末尾。
function get(object,property){ if(!Object.hasOwnProperty.call(object,property)){ var prototype = Object.getPrototypeOf(object) ; if(prototype) return get(prototype,property) ; }else{ return object[property] ; } } ;
Javascript中屬性查找的過(guò)程就像上面的程序那樣。
克隆(合并式繼承)大多數(shù)Javascript程序員會(huì)覺(jué)得復(fù)制一個(gè)對(duì)象的屬性到另一個(gè)對(duì)象上并不是一個(gè)正確的繼承的方式,因?yàn)槿魏螌?duì)原始對(duì)象的修改都不會(huì)反映在克隆的對(duì)象上。五天前我會(huì)同意這個(gè)觀點(diǎn)。然而現(xiàn)在我相信合并式繼承是原型繼承的一種正確方式。對(duì)于原始對(duì)象的修改可以發(fā)送到它的副本來(lái)實(shí)現(xiàn)真正的原型繼承。
合并式繼承和代理有他們的優(yōu)點(diǎn)和缺點(diǎn)。下表列出了它們的優(yōu)缺點(diǎn):
代理 | 合并 |
---|---|
任何對(duì)于原型的修改都會(huì)反映在所有副本上 | 任何對(duì)于原型的修改都需要手動(dòng)更新到副本中 |
屬性查找效率較低因?yàn)樾枰M(jìn)行原型鏈查找 | 屬性查找更搞笑因?yàn)槔^承的屬性是通過(guò)復(fù)制的方式附加在對(duì)象本身的 |
使用Object.create()方法只能繼承單一對(duì)象 | 對(duì)象可以從任意數(shù)量的對(duì)象中通過(guò)復(fù)制繼承屬性 |
上表中最后一點(diǎn)告訴我們對(duì)象可以通過(guò)合并的方式從多個(gè)原型中繼承屬性。這是一個(gè)重要的特點(diǎn)因?yàn)檫@證明原型繼承比Java中的類(lèi)繼承更強(qiáng)大并且與C++中的類(lèi)繼承一樣強(qiáng)大。為了實(shí)現(xiàn)多重繼承,你只需要修改extend方法來(lái)從多個(gè)原型中復(fù)制屬性。
Object.prototype.extend = function(){ var hasOwnProperty = Object.hasOwnProperty ; var object = Object.create(this) ; var length = arguments.length ; var index = length ; while(index){ var extension = arguments[length - (index--)] ; for(var property in extension){ if(hasOwnProperty.call(extension,property)|| typeof object[property] === "undefined"){ //這里同樣應(yīng)該使用深復(fù)制 object[property] = extension[property] ; } } } return object; } ;
多重繼承是非常有用的因?yàn)樗岣吡舜a的可重用性和模塊化。對(duì)象通過(guò)委派繼承一個(gè)原型對(duì)象然后通過(guò)合并繼承其他屬性。比如說(shuō)你有一個(gè)事件發(fā)射器的原型,像下面這樣:
var eventEmitter = { on : function(event,listener){ if(typeof this[event] !== "undefined") this[event].push(listener) ; else this[event] = [listener] ; } , emit : function(event){ if(typeof this[event] !== "undefined"){ var listeners = this[event] ; var length = listeners.length,index = length ; var args = Array.prototype.slice.call(arguments,1) ; while(index){ var listener = listeners[length - (index--)] ; listener.apply(this,args) ; } } } } ;
現(xiàn)在你希望square表現(xiàn)得像一個(gè)事件發(fā)射器。因?yàn)?b>square已經(jīng)通過(guò)委派的方式繼承了rectangle,所以它必須通過(guò)合并的方式繼承eventEmitter。這個(gè)修改可以很容易地通過(guò)使用extend方法實(shí)現(xiàn):
var square = rectangle.extend(eventEmitter,{ create : function(side){ return rectangle.create.call(this,side,side) ; } , resize : function(newSize){ var oldSize = this.width ; this.width = this.height = newSize ; this.emit("resize",oldSize,newSize) ; } }) ; var sq = square.create(5) ; sq.on("resize",function(oldSize,newSize){ alert("sq resized from " + oldSize + "to" + newSize + ".") ; }) ; sq.resize(10) ; alert(sq.area()) ;
在Java中是不可能實(shí)現(xiàn)上面的程序的,因?yàn)樗恢С侄嘀乩^承。相應(yīng)的你必須另外再創(chuàng)建一個(gè)EventEmitter類(lèi)或者使用一個(gè)EventEmitter接口并且在每個(gè)實(shí)現(xiàn)該接口的類(lèi)中分別實(shí)現(xiàn)on和emit方法。當(dāng)然你在C++中不需要面對(duì)這個(gè)問(wèn)題。我們都知道Java sucks(呵呵呵)。
Mixin的藍(lán)圖(Buleprint)在上面的例子中你肯定注意到eventEmitter原型并沒(méi)有一個(gè)create方法。這是因?yàn)槟悴粦?yīng)該直接創(chuàng)建一個(gè)eventEmitter對(duì)象。相反eventEmitter是用來(lái)作為其他原型的原型。這類(lèi)原型稱(chēng)為mixin。它們等價(jià)于抽象類(lèi)。mixin用來(lái)通過(guò)提供一系列可重用的方法來(lái)擴(kuò)展對(duì)象的功能。
然而有時(shí)候mixin需要私有的狀態(tài)。例如eventEmitter如果能夠把它的事件監(jiān)聽(tīng)者列表放在私有變量中而不是放在this對(duì)象上會(huì)安全得多。但是mixin沒(méi)有create方法來(lái)封裝私有狀態(tài)。因此我們需要為mixin創(chuàng)建一個(gè)藍(lán)圖(blueprint)來(lái)創(chuàng)建閉包。藍(lán)圖(blueprint)看起來(lái)會(huì)像是構(gòu)造函數(shù)但是它們并不用像構(gòu)造函數(shù)那樣使用。例如:
function eventEmitter(){ var evnets = Object.create(null) ; this.on = function(event,listener){ if(typeof events[event] !== "undefined") events[event].push(listener) ; else events[event] = [listener] ; } ; this.emit = function(event){ if(typeof events[event] !== "undefined"){ var listeners = events[event] ; var length = listeners.length ,index = length ; var args = Array.prototype.slice.call(arguments,1) ; } } ; } ;
一個(gè)藍(lán)圖用來(lái)在一個(gè)對(duì)象創(chuàng)建之后通過(guò)合并來(lái)擴(kuò)展它(我覺(jué)得有點(diǎn)像裝飾者模式)。Eric Elliot把它們叫做閉包原型。我們可以使用藍(lán)圖版本的eventEmitter來(lái)重寫(xiě)square的代碼,如下:
var square = rectangle.extend({ create : function(side){ var self = rectangle.create.call(this,side,side) ; eventEmitter.call(self) ; return self ; } , resize : function(newSize){ var oldSize = this.width ; this.width = this.height = newSize ; this.emit("resize",oldSize,newSize) ; } }) ; var sq = square.create(5) ; sq.on("resize",function(oldSize,newSize){ alert("sq resized from " + oldSize + "to" + newSize + ".") ; }) ; sq.resize(10) ; alert(sq.area()) ;
藍(lán)圖在Javascript中是獨(dú)一無(wú)二的。它是一個(gè)很強(qiáng)大的特性。然而它們也有自己的缺點(diǎn)。下表列出了mixin和藍(lán)圖的優(yōu)缺點(diǎn):
Mixin | 藍(lán)圖 |
---|---|
它們用來(lái)擴(kuò)展對(duì)象的原型。因此對(duì)象共享同一個(gè)原型 | 它們用來(lái)擴(kuò)展新創(chuàng)建的對(duì)象。因此每個(gè)對(duì)象都是在自己對(duì)象本身進(jìn)行修改 |
因?yàn)槿鄙俜庋b方法所以不存在私有狀態(tài) | 它們是函數(shù),所以可以封裝私有狀態(tài) |
它們是靜態(tài)原型并且不能被自定義 | 它們可以傳遞參數(shù)來(lái)自定義對(duì)象,可以向藍(lán)圖函數(shù)傳遞一些用來(lái)自定義的參數(shù) |
許多Javascript程序員會(huì)覺(jué)得使用原型模式來(lái)繼承違背了語(yǔ)言的精髓。他們更偏向于構(gòu)造模式因?yàn)樗麄冇X(jué)得通過(guò)構(gòu)造函數(shù)創(chuàng)建的對(duì)象才是真正的實(shí)例,因?yàn)?b>instanceof操作會(huì)返回true。然而,這個(gè)爭(zhēng)論是沒(méi)有意義的,因?yàn)?b>instanceof操作可以像下面這樣實(shí)現(xiàn):
Object.prototype.instanceof = function(prototype){ var object = this ; do{ if(object === prototype) return true ; var object = Object.getPrototypeOf(object) ; }while(object) ; return false ; }
這個(gè)instanceof方法現(xiàn)在可以被用來(lái)測(cè)試一個(gè)對(duì)象是否是通過(guò)委派從一個(gè)原型繼承的。例如:
sq.instanceof(square) ;
然而還是沒(méi)有辦法判斷一個(gè)對(duì)象是否是通過(guò)合并的方式從一個(gè)原型繼承的,因?yàn)閷?shí)例的關(guān)聯(lián)信息丟失了。為了解決這個(gè)問(wèn)題我們將一個(gè)原型的所有克隆的引用保存在原型自身中,然后使用這個(gè)信息來(lái)判斷一個(gè)對(duì)象是否是一個(gè)原型的實(shí)例。這個(gè)可以通過(guò)修改extend方法來(lái)實(shí)現(xiàn):
Object.prototype.extend = function(){ var hasOwnProperty = Object.hasOwnProperty ; var object = Object.create(this) ; var length = arguments.lenght ; var index = length ; while(index){ var extension = arguments[length - (index--)] ; for(var property in extension){ if(property !== "clones" && hasOwnProperty.call(extension,property) || typeof object[property] === "undefined") object[property] = extension[property] ; if(hasOwnProperty.call(extension,"clones")}) extension.clones.unshift(object) ; else extension.clones = [object] ; } } return object; } ;
通過(guò)合并繼承自原型的對(duì)象形成了一個(gè)克隆樹(shù),這些樹(shù)從根對(duì)象開(kāi)始然后向下一直到葉子對(duì)象。一個(gè)克隆鏈?zhǔn)且粋€(gè)從根對(duì)象到葉子對(duì)象的單一路徑,這跟遍歷原型鏈很相似。我們可以使用這個(gè)信息來(lái)判斷一個(gè)對(duì)象是否是通過(guò)合并繼承自一個(gè)原型。
Object.prototype.instanceof = function(prototype){ if (Object.hasOwnProperty.call(prototype, "clones")) var clones = prototype.clones; var object = this; do { if (object === prototype || clones && clones.indexOf(object) >= 0) return true; var object = Object.getPrototypeOf(o bject); } while (object); return false; } ;
這個(gè)instanceof方法現(xiàn)在可以用來(lái)判斷一個(gè)對(duì)象是否是通過(guò)合并繼承自一個(gè)原型。例如:
sq.instanceof(eventEmitter);
在上面的程序中instanceof會(huì)返回true如果我媽使用mixin版本的eventEmitter。然而如果我們使用藍(lán)圖版本的eventEmitter它會(huì)返回false。為了解決這個(gè)問(wèn)題我創(chuàng)建了一個(gè)藍(lán)圖函數(shù),這個(gè)函數(shù)接收一個(gè)藍(lán)圖作為參數(shù),向它添加一個(gè)clones屬性然后返回一個(gè)記錄了它的克隆的新藍(lán)圖:
function blueprint(f){ var g = function(){ f.apply(this,arguments) ; g.clones.unshift(this) ; } ; g.clones = [] ; return g ; } ; var eventEmitter = blueprint(function(){ var events = Object.create(null); this.on = function (event, listener) { if (typeof events[event] !== "undefined") events[event].push(listener); else events[event] = [listener]; }; this.emit = function (event) { if (typeof events[event] !== "undefined") { var listeners = events[event]; var length = listeners.length, index = length; var args = Array.prototype.slice.call(arguments, 1); while (index) { var listener = listeners[length - (index--)]; listener.apply(this, args); } } }; }) ;向原型發(fā)送變化
上面例子中的clones屬性有雙重作用。它可以用來(lái)判斷一個(gè)對(duì)象是否是通過(guò)合并繼承自一個(gè)原型的,然后他可以用來(lái)發(fā)送原型改變給所有它的克隆。原型繼承相比類(lèi)繼承最大的優(yōu)勢(shì)就是你可以修改一個(gè)原型在它創(chuàng)建之后。為了使克隆可以繼承對(duì)于原型的修改,我們創(chuàng)建了一個(gè)叫做define的函數(shù):
Object.prototype.define = function (property, value) { this[property] = value; if (Object.hasOwnProperty.call(this, "clones")) { var clones = this.clones; var length = clones.length; while (length) { var clone = clones[--length]; if (typeof clone[property] === "undefined") clone.define(property, value); } } };
現(xiàn)在我們可以修改原型然后這個(gè)修改會(huì)反映在所有的克隆上。例如我們可以創(chuàng)建創(chuàng)建一個(gè)別名addEventListener針對(duì)eventEmitter上的on方法:
var square = rectangle.extend(eventEmitter, { create: function (side) { return rectangle.create.call(this, side, side); }, resize: function (newSize) { var oldSize = this.width; this.width = this.height = newSize; this.emit("resize", oldSize, newSize); } }); var sq = square.create(5); eventEmitter.define("addEventListener", eventEmitter.on); sq.addEventListener("resize", function (oldSize, newSize) { alert("sq resized from " + oldSize + " to " + newSize + "."); }); sq.resize(10); alert(sq.area());
藍(lán)圖需要特別注意。盡管對(duì)于藍(lán)圖的修改會(huì)被發(fā)送到它的克隆,但是藍(lán)圖的新的克隆并不會(huì)反映這些修改。幸運(yùn)的是這個(gè)問(wèn)題的解決方法很簡(jiǎn)單。我們只需要對(duì)blueprint方法進(jìn)行小小的修改,然后任何對(duì)于藍(lán)圖的修改就會(huì)反映在克隆上了。
function blueprint(f) { var g = function () { f.apply(this, arguments); g.clones.unshift(this); var hasOwnProperty = Object.hasOwnProperty; for (var property in g) if (property !== "clones" && hasOwnProperty.call(g, property)) this[property] = g[property]; }; g.clones = []; return g; };結(jié)論
恭喜你。如果你讀完了整篇文章并且理解了我所說(shuō)的東西,你現(xiàn)在就了解了 原型繼承并且為什么它很重要。很感謝你們看完了這篇文章。我希望這個(gè)博客能幫到你們。原型繼承是強(qiáng)大的并且值得更多的信任。然后大部分人從來(lái)不明白這個(gè)因?yàn)镴avascript中的原型繼承被構(gòu)造模式所掩蓋了。
譯者注這篇文章針對(duì)幾種繼承方式進(jìn)行了對(duì)比。文章中說(shuō)到的幾種擴(kuò)展的方法我覺(jué)得是比較有用的。藍(lán)圖(blueprint,這個(gè)實(shí)在不知道該怎么翻譯)的擴(kuò)展方式比較像設(shè)計(jì)模式中的裝飾者模式,通過(guò)函數(shù)對(duì)對(duì)象進(jìn)行擴(kuò)展,這個(gè)是一種比較好玩的擴(kuò)展方式,可以跟原型繼承配合使用。另外文中提到了new關(guān)鍵字的弊端,個(gè)人覺(jué)得主要的原因還是new關(guān)鍵字的出現(xiàn)掩蓋了Javascript本身原型繼承的特點(diǎn),人們自然而然就會(huì)想到傳統(tǒng)的類(lèi)繼承,這樣就無(wú)法發(fā)揮原型繼承的最大威力。最后說(shuō)到的屬性修改傳播的問(wèn)題也挺有意思的,應(yīng)該會(huì)有相應(yīng)的應(yīng)用場(chǎng)景??傊?,我覺(jué)得原型繼承相比于傳統(tǒng)的類(lèi)繼承提供了更大的靈活性,可以給我們開(kāi)發(fā)者提供很大的發(fā)揮空間,不過(guò)不管怎樣,到最后還是要涉及到基本的原型繼承的原理上,所以掌握了原型繼承的原理就可以根據(jù)不同的應(yīng)用場(chǎng)景使用各種各樣的擴(kuò)展方式。
最后,安利下我的個(gè)人博客,歡迎訪問(wèn): http://bin-playground.top原文地址:http://aaditmshah.github.io/why-prototypal-inheritance-matters/
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/85552.html
摘要:避免脆弱的基類(lèi)問(wèn)題。紅牌警告沒(méi)有提到上述任何問(wèn)題。單向數(shù)據(jù)流意味著模型是單一的事實(shí)來(lái)源。單向數(shù)據(jù)流是確定性的,而雙向綁定可能導(dǎo)致更難以遵循和理解的副作用。原文地址 1. 你能說(shuō)出兩種對(duì) JavaScript 應(yīng)用開(kāi)發(fā)者而言的編程范式嗎? 希望聽(tīng)到: 2. 什么是函數(shù)編程? 希望聽(tīng)到: 3. 類(lèi)繼承和原型繼承的不同? 希望聽(tīng)到 4. 函數(shù)式編程和面向?qū)ο缶幊痰膬?yōu)缺點(diǎn)? ...
摘要:操作符構(gòu)造步驟有三步構(gòu)造一個(gè)類(lèi)的實(shí)例這個(gè)實(shí)例是一個(gè)空對(duì)象,并且他的屬性指向構(gòu)造函數(shù)的原型。不優(yōu)化原生的或自定義的作為構(gòu)造函數(shù)是及其不高效的。 原文地址:Javascript – How Prototypal Inheritance really works 在網(wǎng)上可以看到各種關(guān)于Javascript原型繼承的文章,但Javascript規(guī)范中只提供了new操作符這一種實(shí)現(xiàn)原型繼承的方法...
摘要:比如,我們可以監(jiān)聽(tīng)事件由實(shí)例發(fā)出,然后在任何瀏覽器中就是變化的時(shí)候都會(huì)得到通知,如下所示每一個(gè)作用域?qū)ο蠖紩?huì)有這個(gè)方法,可以用來(lái)注冊(cè)一個(gè)作用域事件的偵聽(tīng)器。這個(gè)函數(shù)所扮演的偵聽(tīng)器在被調(diào)用時(shí)會(huì)有一個(gè)對(duì)象作為第一個(gè)參數(shù)。 上一篇:【譯】《精通使用AngularJS開(kāi)發(fā)Web App》(二) 下一篇:【譯】《精通使用AngularJS開(kāi)發(fā)Web App》(四) 書(shū)名:Mastering W...
摘要:盡管特定環(huán)境下有各種各樣的設(shè)計(jì)模式,開(kāi)發(fā)者還是傾向于使用一些習(xí)慣性的模式。原型設(shè)計(jì)模式依賴(lài)于原型繼承原型模式主要用于為高性能環(huán)境創(chuàng)建對(duì)象。對(duì)于一個(gè)新創(chuàng)建的對(duì)象,它將保持構(gòu)造器初始化的狀態(tài)。這樣做主要是為了避免訂閱者和發(fā)布者之間的依賴(lài)。 2016-10-07 每個(gè)JS開(kāi)發(fā)者都力求寫(xiě)出可維護(hù)、復(fù)用性和可讀性高的代碼。隨著應(yīng)用不斷擴(kuò)大,代碼組織的合理性也越來(lái)越重要。設(shè)計(jì)模式為特定環(huán)境下的常見(jiàn)...
摘要:忍者級(jí)別的函數(shù)操作對(duì)于什么是匿名函數(shù),這里就不做過(guò)多介紹了。我們需要知道的是,對(duì)于而言,匿名函數(shù)是一個(gè)很重要且具有邏輯性的特性。通常,匿名函數(shù)的使用情況是創(chuàng)建一個(gè)供以后使用的函數(shù)。 JS 中的遞歸 遞歸, 遞歸基礎(chǔ), 斐波那契數(shù)列, 使用遞歸方式深拷貝, 自定義事件添加 這一次,徹底弄懂 JavaScript 執(zhí)行機(jī)制 本文的目的就是要保證你徹底弄懂javascript的執(zhí)行機(jī)制,如果...
閱讀 697·2021-11-22 09:34
閱讀 3830·2021-09-22 15:42
閱讀 1342·2021-09-03 10:28
閱讀 1081·2021-08-26 14:13
閱讀 1911·2019-08-29 15:41
閱讀 1438·2019-08-29 14:12
閱讀 3375·2019-08-26 18:36
閱讀 3320·2019-08-26 13:47