在本篇文章,我們考慮在ECMAScript中的面向?qū)ο缶幊痰母鱾€(gè)方面(雖然以前在許多文章中已經(jīng)討論過(guò)這個(gè)話題)。我們將更多地從理論方面看這些問(wèn)題。 特別是,我們會(huì)考慮對(duì)象的創(chuàng)建算法,對(duì)象(包括基本關(guān)系 - 繼承)之間的關(guān)系是如何,也可以在討論中使用(我希望將消除之前對(duì)于JavaScript中OOP的一些概念歧義)。
在進(jìn)行ECMAScript中的OOP技術(shù)分析之前,我們有必要掌握一些OOP基本的特征,并澄清概論中的主要概念。
ECMAScript支持包括結(jié)構(gòu)化、面向?qū)ο?、函?shù)式、命令式等多種編程方式,某些情況下還支持面向方面編程;但本文是討論面向?qū)ο缶幊?,所以?lái)給出ECMAScript中面向?qū)ο缶幊痰亩x:
ECMAScript是基于原型實(shí)現(xiàn)的面向?qū)ο缶幊陶Z(yǔ)言。
基于原型的OOP和基于靜態(tài)類(lèi)的方式直接有很多差異。 讓我們一起來(lái)看看他們直接詳細(xì)的差異。
注意,在前面一句很重要的一點(diǎn)已經(jīng)指出的那樣-完全基于靜態(tài)類(lèi)。 隨著“靜態(tài)”一詞,我們了解靜態(tài)對(duì)象和靜態(tài)類(lèi),強(qiáng)類(lèi)型(雖然不是必需的)。
關(guān)于這種情況,很多論壇上的文檔都有強(qiáng)調(diào)這是他們反對(duì)將在JavaScript里將“類(lèi)與原型”進(jìn)行比較的主要原因,盡管他們?cè)趯?shí)現(xiàn)上的有所不同(例如基于動(dòng)態(tài)類(lèi)的Python和Ruby)不是太反對(duì)的重點(diǎn)(某些條件寫(xiě),盡管思想上有一定不同,但JavaScript沒(méi)有變得那么另類(lèi)),但他們反對(duì)的重點(diǎn)是靜態(tài)類(lèi)和動(dòng)態(tài)原型(statics + classes vs. dynamics + prototypes),確切地說(shuō),一個(gè)靜態(tài)類(lèi)(例如:C + +,JAVA)和他的屬下及方法定義的機(jī)制可以讓我們看到它和基于原型實(shí)現(xiàn)的準(zhǔn)確區(qū)別。
但是,讓我們來(lái)一個(gè)一個(gè)列舉一下。 讓我們考慮總則和這些范式的主要概念。
在基于類(lèi)的模型中,有個(gè)關(guān)于類(lèi)和實(shí)例的概念。 類(lèi)的實(shí)例也常常被命名為對(duì)象或范例 。
類(lèi)代表了一個(gè)實(shí)例(也就是對(duì)象)的抽象。在這方面有點(diǎn)像數(shù)學(xué),但我們一把稱(chēng)之為類(lèi)型(type)或分類(lèi)(classification)。
例如(這里和下面的例子都是偽代碼):
C = Class {a, b, c} // 類(lèi)C, 包括特性a, b, c
實(shí)例的特點(diǎn)是:屬性(對(duì)象描述 )和方法(對(duì)象活動(dòng))。特性本身也可視為對(duì)象:即屬性是否可寫(xiě)的,可配置,可設(shè)置的(getter/setter)等。因此,對(duì)象存儲(chǔ)了狀態(tài) (即在一個(gè)類(lèi)中描述的所有屬性的具體值),類(lèi)為他們的實(shí)例定義了嚴(yán)格不變的結(jié)構(gòu)(屬性)和嚴(yán)格不變的行為(方法)。
C = Class {a, b, c, method1, method2} c1 = {a: 10, b: 20, c: 30} // 類(lèi)C是實(shí)例:對(duì)象с1 c2 = {a: 50, b: 60, c: 70} // 類(lèi)C是實(shí)例:對(duì)象с2,擁有自己的狀態(tài)(也就是屬性值)
為了提高代碼重用,類(lèi)可以從一個(gè)擴(kuò)展為另一個(gè),在加上額外的信息。 這種機(jī)制被稱(chēng)為(分層)繼承 。
D = Class extends C = {d, e} // {a, b, c, d, e} d1 = {a: 10, b: 20, c: 30, d: 40, e: 50}
在類(lèi)的實(shí)例上調(diào)用方的時(shí)候,通常會(huì)現(xiàn)在原生類(lèi)本書(shū)就查找該方法,如果沒(méi)找到就到直接父類(lèi)去查找,如果還沒(méi)找到,就到父類(lèi)的父類(lèi)去查找(例如嚴(yán)格的繼承鏈上),如果查到繼承的頂部還沒(méi)查到,那結(jié)果就是:該對(duì)象沒(méi)有類(lèi)似的行為,也沒(méi)辦法獲取結(jié)果。
d1.method1() // D.method1 (no) -> C.method1 (yes) d1.method5() // D.method5 (no) -> C.method5 (no) -> no result
與在繼承里方法不復(fù)制到一個(gè)子類(lèi)相比,屬性總是被復(fù)雜到子類(lèi)里的。 我們可以看到子類(lèi)D繼承自父類(lèi)C類(lèi):屬性a,b,c是復(fù)制過(guò)去了,D的結(jié)構(gòu)是{a, b, c, d, e} } 。然而,方法{method1, method2}是沒(méi)有復(fù)制過(guò)去,而是繼承過(guò)去的。 因此,也就是說(shuō)如果一個(gè)很深層次的類(lèi)有一些對(duì)象根本不需要的屬性的話,那子類(lèi)也有擁有這些屬性。
因此,我們有如下關(guān)鍵概念:
讓我們看看在JavaScript里如何替代OOP模型,也就是我們所建議的基于原型的OOP。
這里的基本概念是動(dòng)態(tài)可變對(duì)象。轉(zhuǎn)換(完整轉(zhuǎn)換,不僅包括值,還包括特性)和動(dòng)態(tài)語(yǔ)言有直接關(guān)系。下面這樣的對(duì)象可以獨(dú)立存儲(chǔ)他們所有的特性(屬性,方法)而不需要的類(lèi)。
object = {a: 10, b: 20, c: 30, method: fn}; object.a; // 10 object.c; // 30 object.method();
此外,由于動(dòng)態(tài)的,他們可以很容易地改變(添加,刪除,修改)自己的特性:
object.method5 = function () {...}; // 添加新方法 object.d = 40; // 添加新屬性 "d" delete object.c; // 刪除屬性 "с" object.a = 100; // 修改屬性 "а" // 結(jié)果是: object: {a: 100, b: 20, d: 40, method: fn, method5: fn};
也就是說(shuō),在賦值的時(shí)候,如果某些特性不存在,則創(chuàng)建它并且將賦值與它進(jìn)行初始化,如果它存在,就只是更新。
在這種情況下,代碼重用不是通過(guò)擴(kuò)展類(lèi)來(lái)實(shí)現(xiàn)的,(請(qǐng)注意,我們沒(méi)有說(shuō)類(lèi)沒(méi)辦法改變,因?yàn)檫@里根本沒(méi)有類(lèi)的概念),而是通過(guò)原型來(lái)實(shí)現(xiàn)的。
原型是一個(gè)對(duì)象,它是用來(lái)作為其他對(duì)象的原始copy,或者如果一些對(duì)象沒(méi)有自己的必要特性,原型可以作為這些對(duì)象的一個(gè)委托而當(dāng)成輔助對(duì)象。
任何對(duì)象都可以被用來(lái)作為另一個(gè)對(duì)象的原型對(duì)象,因?yàn)閷?duì)象可以很容易地在運(yùn)行時(shí)改變它的原型動(dòng)態(tài)。
注意,目前我們正在考慮的是概論而不是具體實(shí)現(xiàn),當(dāng)我們?cè)贓CMAScript中討論具體實(shí)現(xiàn)時(shí),我們將看到他們自身的一些特點(diǎn)。
例(偽代碼):
x = {a: 10, b: 20}; y = {a: 40, c: 50}; y.[[Prototype]] = x; // x是y的原型 y.a; // 40, 自身特性 y.c; // 50, 也是自身特性 y.b; // 20 – 從原型中獲取: y.b (no) -> y.[[Prototype]].b (yes): 20 delete y.a; // 刪除自身的"а" y.a; // 10 – 從原型中獲取 z = {a: 100, e: 50} y.[[Prototype]] = z; // 將y的原型修改為z y.a; // 100 – 從原型z中獲取 y.e // 50, 也是從從原型z中獲取 z.q = 200 // 添加新屬性到原型上 y.q // 修改也適用于y
這個(gè)例子展示了原型作為輔助對(duì)象屬性的重要功能和機(jī)制,就像是要自己的屬性一下,和自身屬性相比,這些屬性是委托屬性。這個(gè)機(jī)制被稱(chēng)為委托,并且基于它的原型模型是一個(gè)委托的原型(或基于委托的原型 ) 。引用的機(jī)制在這里稱(chēng)為發(fā)送信息到對(duì)象上,如果這個(gè)對(duì)象得不到響應(yīng)就會(huì)委托給原型來(lái)查找(要求它嘗試響應(yīng)消息)。
在這種情況下的代碼重用被稱(chēng)為基于委托的繼承或基于原型的繼承。由于任何對(duì)象可以當(dāng)成原型,也就是說(shuō)原型也可以有自己的原型。 這些原型連接在一起形成一個(gè)所謂的原型鏈。 鏈也像靜態(tài)類(lèi)中分層次的,但是它可以很容易地重新排列,改變層次和結(jié)構(gòu)。
x = {a: 10} y = {b: 20} y.[[Prototype]] = x z = {c: 30} z.[[Prototype]] = y z.a // 10 // z.a 在原型鏈里查到: // z.a (no) -> // z.[[Prototype]].a (no) -> // z.[[Prototype]].[[Prototype]].a (yes): 10
如果一個(gè)對(duì)象和它的原型鏈不能響應(yīng)消息發(fā)送,該對(duì)象可以激活相應(yīng)的系統(tǒng)信號(hào),可能是由原型鏈上其它的委托進(jìn)行處理。
該系統(tǒng)信號(hào),在許多實(shí)現(xiàn)里都是可用的,包括基于括動(dòng)態(tài)類(lèi)的系統(tǒng):Smalltalk中的#doesNotUnderstand,Ruby中的??methodmissing;Python中的getattr,PHP中的call;和ECMAScript中的noSuchMethod_實(shí)現(xiàn),等等。
例(SpiderMonkey的ECMAScript的實(shí)現(xiàn)):
var object = { // catch住不能響應(yīng)消息的系統(tǒng)信號(hào) noSuchMethod: function (name, args) { alert([name, args]); if (name == 'test') { return '.test() method is handled'; } return delegate[name].apply(this, args); } }; var delegate = { square: function (a) { return a * a; } }; alert(object.square(10)); // 100 alert(object.test()); // .test() method is handled
也就是說(shuō),基于靜態(tài)類(lèi)的實(shí)現(xiàn),在不能響應(yīng)消息的情況下,得出的結(jié)論是:目前的對(duì)象不具有所要求的特性,但是如果嘗試從原型鏈里獲取,依然可能得到結(jié)果,或者該對(duì)象經(jīng)過(guò)一系列變化以后擁有該特性。
關(guān)于ECMAScript,具體的實(shí)現(xiàn)就是:使用基于委托的原型。 然而,正如我們將從規(guī)范和實(shí)現(xiàn)里看到的,他們也有自身的特性。
老實(shí)說(shuō),有必要在說(shuō)句話關(guān)于另外一種情況(盡快在ECMASCript沒(méi)有用到):當(dāng)原型從其它對(duì)象復(fù)雜原來(lái)代替原生對(duì)象這種情況。這種情況代碼重用是在對(duì)象創(chuàng)建階段對(duì)一個(gè)對(duì)象的真正復(fù)制(克?。┒皇俏?。這種原型被稱(chēng)為concatenative原型。復(fù)制對(duì)象所有原型的特性,可以進(jìn)一步完全改變其屬性和方法,同樣作為原型可以改變自己(在基于委托的模型中,這個(gè)改變不會(huì)改變現(xiàn)有存在的對(duì)象行為,而是改變它的原型特性)。 這種方法的優(yōu)點(diǎn)是可以減少調(diào)度和委托的時(shí)間,而缺點(diǎn)是內(nèi)存使用率搞。
回來(lái)動(dòng)態(tài)弱類(lèi)型變化的對(duì)象,與基于靜態(tài)類(lèi)的模型相比,檢驗(yàn)它是否可以做這些事和對(duì)象有什么類(lèi)型(類(lèi))無(wú)關(guān),而是是否能夠相應(yīng)消息有關(guān)(即在檢查以后是否有能力做它是必須的) 。
例如:
// 在基于靜態(tài)來(lái)的模型里 if (object instanceof SomeClass) { // 一些行為是運(yùn)行的 } // 在動(dòng)態(tài)實(shí)現(xiàn)里 // 對(duì)象在此時(shí)是什么類(lèi)型并不重要 // 因?yàn)橥蛔?、?lèi)型、特性可以自由重復(fù)的轉(zhuǎn)變。 // 重要的對(duì)象是否可以響應(yīng)test消息 if (isFunction(object.test)) // ECMAScript if object.respond_to?(:test) // Ruby if hasattr(object, 'test'): // Python
這就是所謂的Dock類(lèi)型 。 也就是說(shuō),物體在check的時(shí)候可以通過(guò)自己的特性來(lái)識(shí)別,而不是對(duì)象在層次結(jié)構(gòu)中的位置或他們屬于任何具體類(lèi)型。
讓我們來(lái)看一下這種方式的主要特點(diǎn):
不過(guò),還有一個(gè)模型,我們也應(yīng)該考慮。
我們認(rèn)為,在上面例子里展示的區(qū)別“類(lèi)VS原型 ”在這個(gè)基于動(dòng)態(tài)類(lèi)的模型中不是那么重要,(尤其是如果原型鏈?zhǔn)遣蛔兊?,為更?zhǔn)確區(qū)分,還是有必要考慮一個(gè)靜態(tài)類(lèi))。 作為例子,它也可以使用Python或Ruby(或其他類(lèi)似的語(yǔ)言)。 這些語(yǔ)言都使用基于動(dòng)態(tài)類(lèi)的范式。 然而,在某些方面,我們是可以看到基于原型實(shí)現(xiàn)的某些功能。
在下面例子中,我們可以看到僅僅是基于委托的原型,我們可以放大一個(gè)類(lèi)(原型),從而影響到所有與這個(gè)類(lèi)相關(guān)的對(duì)象,我們也可以在運(yùn)行時(shí)動(dòng)態(tài)地改變這個(gè)對(duì)象的類(lèi)(為委托提供一個(gè)新對(duì)象)等等。
# Python class A(object): def init(self, a): self.a = a def square(self): return self.a * self.a a = A(10) # 創(chuàng)建實(shí)例 print(a.a) # 10 A.b = 20 # 為類(lèi)提供一個(gè)新屬性 print(a.b) # 20 – 可以在"a"實(shí)例里訪問(wèn)到 a.b = 30 # 創(chuàng)建a自身的屬性 print(a.b) # 30 del a.b # 刪除自身的屬性 print(a.b) # 20 - 再次從類(lèi)里獲?。ㄔ停? # 就像基于原型的模型 # 可以在運(yùn)行時(shí)改變對(duì)象的原型 class B(object): # 空類(lèi)B pass b = B() # B的實(shí)例 b.class = A # 動(dòng)態(tài)改變類(lèi)(原型) b.a = 10 # 創(chuàng)建新屬性 print(b.square()) # 100 - A類(lèi)的方法這時(shí)候可用 # 可以顯示刪除類(lèi)上的引用 del A del B # 但對(duì)象依然有隱式的引用,并且這些方法依然可用 print(b.square()) # 100 # 但這時(shí)候不能再改變類(lèi)了 # 這是實(shí)現(xiàn)的特性 b.class = dict # error
Ruby中的實(shí)現(xiàn)也是類(lèi)似的:也使用了完全動(dòng)態(tài)的類(lèi)(順便說(shuō)一下在當(dāng)前版本的Python中,與Ruby和ECMAScript的對(duì)比,放大類(lèi)(原型)不行的),我們可以徹底改變對(duì)象(或類(lèi))的特性(在類(lèi)上添加方法/屬性,而這些變化會(huì)影響已經(jīng)存在的對(duì)象),但是,它不能的動(dòng)態(tài)改變一個(gè)對(duì)象的類(lèi)。
但是,這篇文章不是專(zhuān)門(mén)針對(duì)Python和Ruby的,因此我們不多說(shuō)了,我們來(lái)繼續(xù)討論ECMAScript本身。
但在此之前,我們還得再看一下在一些OOP里有的“語(yǔ)法糖”,因?yàn)楹芏嘀瓣P(guān)于JavaScript的文章往往會(huì)文這些問(wèn)題。
本節(jié)唯一需要注意的錯(cuò)誤句子是:“JavaScript不是類(lèi),它有原型,可以代替類(lèi)”。 非常有必要知道并非所有基于類(lèi)的實(shí)現(xiàn)都是完全不一樣的,即便我們可能會(huì)說(shuō)“JavaScript是不同的”,但也有必要考慮(除了“類(lèi)”的概念)還有其他相關(guān)的特性呢。
本節(jié)我們簡(jiǎn)要介紹一下其它特性和各種OOP實(shí)現(xiàn)中關(guān)于代碼重用的方式,也包括ECMAScript中的OOP實(shí)現(xiàn)。 原因是,之前出現(xiàn)的關(guān)于JavaScript中關(guān)于OOP的實(shí)現(xiàn)是有一些習(xí)慣性的思維限制,唯一主要的要求是,應(yīng)該在技術(shù)上和思想上加以證明。不能說(shuō)沒(méi)發(fā)現(xiàn)和其它OOP實(shí)現(xiàn)里的語(yǔ)法糖功能,就草率認(rèn)為JavaScript不是不是純粹的OOP語(yǔ)言,這是不對(duì)滴。
在ECMAScript中對(duì)象有幾種含義的多態(tài)性。
例如,一個(gè)函數(shù)可以應(yīng)用于不同的對(duì)象,就像原生對(duì)象的特性(因?yàn)檫@個(gè)值在進(jìn)入執(zhí)行上下文時(shí)確定的):
function test() { alert([this.a, this.b]); } test.call({a: 10, b: 20}); // 10, 20 test.call({a: 100, b: 200}); // 100, 200 var a = 1; var b = 2; test(); // 1, 2
不過(guò),也有例外:Date.prototype.getTime()方法,根據(jù)標(biāo)準(zhǔn)這個(gè)值總是應(yīng)該有一個(gè)日期對(duì)象,否則就會(huì)拋出異常。
alert(Date.prototype.getTime.call(new Date())); // time alert(Date.prototype.getTime.call(new String(''))); // TypeError
所謂函數(shù)定義時(shí)的參數(shù)多態(tài)性也就等價(jià)于所有數(shù)據(jù)類(lèi)型,只不過(guò)接受多態(tài)性參數(shù)(例如數(shù)組的.sort排序方法和它的參數(shù)——多態(tài)的排序功能)。順便說(shuō)一下,上面的例子也可以被視為是一種參數(shù)多態(tài)性。
原型里方法可以被定義為空,所有創(chuàng)建的對(duì)象應(yīng)重新定義(實(shí)現(xiàn))該方法(即“一個(gè)接口(簽名),多個(gè)實(shí)現(xiàn)”)。
多態(tài)性和我們上面提到的Duck類(lèi)型是有關(guān)的:即對(duì)象的類(lèi)型和在層次結(jié)構(gòu)中的位置不是那么重要,但如果它有所有必要的特征,它可以很容易地接受(即通用接口很重要,實(shí)現(xiàn)則可以多種多樣)。
關(guān)于封裝,往往會(huì)有錯(cuò)誤的看法。本節(jié)我們討論一下一些OOP實(shí)現(xiàn)里的語(yǔ)法糖——也就是眾所周知的修飾符:在這種情況下,我們將討論一些OOP實(shí)現(xiàn)便捷的“糖” -眾所周知的修飾符:private,protected和public(或者稱(chēng)為對(duì)象的訪問(wèn)級(jí)別或訪問(wèn)修飾符)。
在這里我要提醒一下封裝的主要目的:封裝是一個(gè)抽象的增加,而不是選拔個(gè)直接往你的類(lèi)里寫(xiě)入一些東西的隱藏“惡意黑客”。
這是一個(gè)很大的錯(cuò)誤:為了隱藏使用隱藏。
訪問(wèn)級(jí)別(private,protected和public),為了方便編程在很多面向?qū)ο罄锒家呀?jīng)實(shí)現(xiàn)了(真的是非常方便的語(yǔ)法糖),更抽象地描述和構(gòu)建系統(tǒng)。
這些可以在一些實(shí)現(xiàn)里看出(如已經(jīng)提到的Python和Ruby)。一方面(在Python中),這些private _protected屬性(通過(guò)下劃線這個(gè)命名規(guī)范),從外部不可訪問(wèn)。 另一方面,Python可以通過(guò)特殊的規(guī)則從外部訪問(wèn)(_ClassNamefield_name)。
class A(object): def init(self): self.public = 10 self.private = 20 def get_private(self): return self.private # outside: a = A() # A的實(shí)例 print(a.public) # OK, 30 print(a.get_private()) # OK, 20 print(a.private) # 失敗,因?yàn)橹荒茉贏里可用 # 但在Python里,可以通過(guò)特殊規(guī)則來(lái)訪問(wèn) print(a._Aprivate) # OK, 20
在Ruby里:一方面有能力來(lái)定義private和protected的特性,另一方面,也有特殊的方法( 例如instance_variable_get,instance_variable_set,send等)獲取封裝的數(shù)據(jù)。
class A def initialize @a = 10 end def public_method private_method(20) end private def private_method(b) return @a + b end end a = A.new # 新實(shí)例 a.public_method # OK, 30 a.a # 失敗, @a - 是私有的實(shí)例變量 # "private_method"是私有的,只能在A類(lèi)里訪問(wèn) a.private_method # 錯(cuò)誤 # 但是有特殊的元數(shù)據(jù)方法名,可以獲取到數(shù)據(jù) a.send(:private_method, 20) # OK, 30 a.instance_variable_get(:@a) # OK, 10
最主要的原因是,程序員自己想要獲得的封裝(請(qǐng)注意,我特別不使用“隱藏”)的數(shù)據(jù)。 如果這些數(shù)據(jù)會(huì)以某種方式不正確地更改或有任何錯(cuò)誤,則全部責(zé)任都是程序員,但不是簡(jiǎn)單的“拼寫(xiě)錯(cuò)誤”或“隨便改變某些字段”。 但如果這種情況很頻繁,那就是很不好的編程習(xí)慣和風(fēng)格 ,因?yàn)橥ǔV涤霉驳腁PI來(lái)和對(duì)象“交談”。
重復(fù)一下,封裝的基本目的是一個(gè)從輔助數(shù)據(jù)的用戶(hù)中抽象出來(lái),而不是一個(gè)防止黑客隱藏?cái)?shù)據(jù)。 更嚴(yán)重的,封裝不是用private修飾數(shù)據(jù)而達(dá)到軟件安全的目的。
封裝輔助對(duì)象(局部),我們用最小的代價(jià)、本地化和預(yù)測(cè)性變化來(lái)問(wèn)為公共接口的行為變化提供可行性,這也正是封裝的目的。
另外setter方法??的重要目的是抽象復(fù)雜的計(jì)算。 例如,element.innerHTML這個(gè)setter——抽象的語(yǔ)句——“現(xiàn)在這個(gè)元素內(nèi)的HTML是如下內(nèi)容”,而在 innerHTML屬性的setter函數(shù)將難以計(jì)算和檢查。 在這種情況下,問(wèn)題大多涉及到抽象 ,但封裝也會(huì)發(fā)生。
封裝的概念不僅僅只與OOP相關(guān)。 例如,它可以是一個(gè)簡(jiǎn)單的功能,只封裝了各種計(jì)算,使得其抽象(沒(méi)有必要讓用戶(hù)知道,例如函數(shù)Math.round(... ...)是如何實(shí)現(xiàn)的,用戶(hù)只是簡(jiǎn)單地調(diào)用它)。 它是一種封裝,注意,我沒(méi)有說(shuō)他是“private, protected和public”。
ECMAScript規(guī)范的當(dāng)前版本,沒(méi)有定義private, protected和public修飾符。
然而,在實(shí)踐中是有可能看到有些東西被命名為“模仿JS封裝”。 一般該上下文的目的是(作為一個(gè)規(guī)則,構(gòu)造函數(shù)本身)使用。 不幸的是,經(jīng)常實(shí)施這種“模仿”,程序員可以產(chǎn)生偽絕對(duì)非抽象的實(shí)體設(shè)置“getter / setter方法”(我再說(shuō)一遍,它是錯(cuò)誤的):
function A() { var _a; // "private" a this.getA = function _getA() { return _a; }; this.setA = function _setA(a) { _a = a; }; } var a = new A(); a.setA(10); alert(a._a); // undefined, "private" alert(a.getA()); // 10
因此,每個(gè)人都明白,對(duì)于每個(gè)創(chuàng)建的對(duì)象,對(duì)于的getA/setA方法也創(chuàng)建了,這也是導(dǎo)致內(nèi)存增加的原因(和原型定義相比)。 雖然,理論上第一種情況下可以對(duì)對(duì)象進(jìn)行優(yōu)化。
另外,一些JavaScript的文章經(jīng)常提到“私有方法”的概念,注意:ECMA-262-3標(biāo)準(zhǔn)里沒(méi)有定義任何關(guān)于“私有方法”的概念。
但是,某些情況下它可以在構(gòu)造函數(shù)中創(chuàng)建,因?yàn)镴S是意識(shí)形態(tài)的語(yǔ)言——對(duì)象是完全可變的并且有獨(dú)特的特性(在構(gòu)造函數(shù)里某些條件下,有些對(duì)象可以得到額外的方法,而其他則不行)。
此外,在JavaScript里,如果還是把封裝曲解成為了不讓惡意黑客在某些自動(dòng)寫(xiě)入某些值的一種理解來(lái)代替使用setter方法,那所謂的“隱藏(hidden)”和“私有(private)”其實(shí)沒(méi)有很“隱藏”,,有些實(shí)現(xiàn)可以通過(guò)調(diào)用上下文到eval函數(shù)(可以在SpiderMonkey1.7上測(cè)試)在相關(guān)的作用域鏈(以及相應(yīng)的所有變量對(duì)象)上獲取值)。
eval('_a = 100', a.getA); // 或者a.setA,因?yàn)?_a"兩個(gè)方法的[[Scope]]上 a.getA(); // 100
或者,在實(shí)現(xiàn)中允許直接進(jìn)入活動(dòng)對(duì)象(例如Rhino),通過(guò)訪問(wèn)該對(duì)象的相應(yīng)屬性可以改變內(nèi)部變量的值:
// Rhino var foo = (function () { var x = 10; // "private" return function () { print(x); }; })(); foo(); // 10 foo.parent.x = 20; foo(); // 20
有時(shí),在JavaScript里通過(guò)在變量前加下劃線來(lái)達(dá)到“private”和“protected”數(shù)據(jù)的目的(但與Python相比,這里只是命名規(guī)范):
var _myPrivateData = 'testString';
對(duì)于括號(hào)括住執(zhí)行上下文是經(jīng)常使用,但對(duì)于真正的輔助數(shù)據(jù),則和對(duì)象沒(méi)有直接關(guān)聯(lián),只是方便從外部的API抽象出來(lái):
(function () { // 初始化上下文 })();
多繼承是代碼重用改進(jìn)的一個(gè)很方便的語(yǔ)法糖(如果我們一次能繼承一個(gè)類(lèi),為什么不能一次繼承10個(gè)?)。 然而由于多重繼承有一些不足,才導(dǎo)致在實(shí)現(xiàn)中沒(méi)有流行起來(lái)。
ECMAScript不支持多繼承(即只有一個(gè)對(duì)象,可以用來(lái)作為一個(gè)直接原型),雖然其祖先自編程語(yǔ)言有這樣的能力。 但在某些實(shí)現(xiàn)中(如SpiderMonkey)使用noSuchMethod可以用于管理調(diào)度和委托來(lái)替代原型鏈。
Mixins是代碼重用的一種便捷方式。 Mixins已建議作為多重繼承的替代品。 這些獨(dú)立的元素都可以與任何對(duì)象進(jìn)行混合來(lái)擴(kuò)展它們的功能(因此對(duì)象也可以混合多個(gè)Mixins)。 ECMA-262-3規(guī)范沒(méi)有定義“Mixins”的概念,但根據(jù)Mixins定義以及ECMAScript擁有動(dòng)態(tài)可變對(duì)象,所以使用Mixins簡(jiǎn)單地?cái)U(kuò)充特性是沒(méi)有障礙的。
典型的例子:
// helper for augmentation Object.extend = function (destination, source) { for (property in source) if (source.hasOwnProperty(property)) { destination[property] = source[property]; } return destination; }; var X = {a: 10, b: 20}; var Y = {c: 30, d: 40}; Object.extend(X, Y); // mix Y into X alert([X.a, X.b, X.c, X.d]); 10, 20, 30, 40
請(qǐng)注意,我采取在ECMA-262-3中被提及過(guò)的引號(hào)中的這些定義(“mixin”,“mix”),在規(guī)范里并沒(méi)有這樣的概念,而且不是mix而是常用的通過(guò)新特性去擴(kuò)展對(duì)象。(Ruby中mixins的概念是官方定義的,mixin創(chuàng)建了一個(gè)包含模塊的一個(gè)引用來(lái)代替簡(jiǎn)單復(fù)制該模塊的所有屬性到另外一個(gè)模塊上——事實(shí)上是:為委托創(chuàng)建一個(gè)額外的對(duì)象(原型))。
Traits和mixins的概念相似,但它有很多功能(根據(jù)定義,因?yàn)榭梢詰?yīng)用mixins所以不能包含狀態(tài),因?yàn)樗锌赡軐?dǎo)致命名沖突)。 根據(jù)ECMAScript說(shuō)明Traits和mixins遵循同樣的原則,所以該規(guī)范沒(méi)有定義“Traits”的概念。
在一些OOP中實(shí)現(xiàn)的接口和mixins及traits類(lèi)似。然而,與mixins及traits相比,接口強(qiáng)制實(shí)現(xiàn)類(lèi)必須實(shí)現(xiàn)其方法簽名的行為。
接口完全可以被視為抽象類(lèi)。不過(guò)與抽象類(lèi)相比(抽象類(lèi)里的方法可以只實(shí)現(xiàn)一部分,另外一部分依然定義為簽名),繼承只能是單繼承基類(lèi),但可以繼承多個(gè)接口,節(jié)約這個(gè)原因,可以接口(多個(gè)混合)可以看做是多繼承的替代方案。
ECMA-262-3標(biāo)準(zhǔn)既沒(méi)有定義“接口”的概念,也沒(méi)有定義“抽象類(lèi)”的概念。 然而,作為模仿,它是可以由“空”的方法(或空方法中拋出異常,告訴開(kāi)發(fā)人員這個(gè)方法需要被實(shí)現(xiàn))的對(duì)象來(lái)實(shí)現(xiàn)。
對(duì)象組合也是一個(gè)動(dòng)態(tài)代碼重用技術(shù)之一。 對(duì)象組合不同于高靈活性的繼承,它實(shí)現(xiàn)了一個(gè)動(dòng)態(tài)可變的委托。而這,也是基于委托原型的基本。 除了動(dòng)態(tài)可變?cè)?,該?duì)象可以為委托聚合對(duì)象(創(chuàng)建一個(gè)組合作為結(jié)果——聚合 ),并進(jìn)一步發(fā)送消息到對(duì)象上,委托到該委托上。這可以?xún)蓚€(gè)以上的委托,因?yàn)樗膭?dòng)態(tài)特性決定著它可以在運(yùn)行時(shí)改變。
已經(jīng)提到的noSuchMethod例子是這樣,但也讓我們展示了如何明確地使用委托:
例如:
var _delegate = { foo: function () { alert('_delegate.foo'); } }; var agregate = { delegate: _delegate, foo: function () { return this.delegate.foo.call(this); } }; agregate.foo(); // delegate.foo agregate.delegate = { foo: function () { alert('foo from new delegate'); } }; agregate.foo(); // foo from new delegate
這種對(duì)象關(guān)系稱(chēng)為“has-a”,而集成是“is-a“的關(guān)系。
由于顯示組合的缺乏(與繼承相比的靈活性),增加中間代碼也是可以的。
作為面向方面的一個(gè)功能,可以使用function decorators。ECMA-262-3規(guī)格沒(méi)有明確定義的“function decorators”的概念(和Python相對(duì),這個(gè)詞是在Python官方定義了)。 不過(guò),擁有函數(shù)式參數(shù)的函數(shù)在某些方面是可以裝飾和激活的(通過(guò)應(yīng)用所謂的建議):
最簡(jiǎn)單的裝飾者例子:
function checkDecorator(originalFunction) { return function () { if (fooBar != 'test') { alert('wrong parameter'); return false; } return originalFunction(); }; } function test() { alert('test function'); } var testWithCheck = checkDecorator(test); var fooBar = false; test(); // 'test function' testWithCheck(); // 'wrong parameter' fooBar = 'test'; test(); // 'test function' testWithCheck(); // 'test function'
在這篇文章,我們理清了OOP的概論(我希望這些資料已經(jīng)對(duì)你有用了),下一章節(jié)我們將繼續(xù)面向?qū)ο缶幊讨瓻CMAScript的實(shí)現(xiàn) 。
更多建議: