深入理解JavaScript系列(17):面向?qū)ο缶幊讨耪撛敿?xì)介紹
介紹
在本篇文章,我們考慮在ECMAScript中的面向?qū)ο缶幊痰母鱾€(gè)方面(雖然以前在許多文章中已經(jīng)討論過(guò)這個(gè)話(huà)題)。我們將更多地從理論方面看這些問(wèn)題。 特別是,我們會(huì)考慮對(duì)象的創(chuàng)建算法,對(duì)象(包括基本關(guān)系 - 繼承)之間的關(guān)系是如何,也可以在討論中使用(我希望將消除之前對(duì)于JavaScript中OOP的一些概念歧義)。
英文原文:http://dmitrysoshnikov.com/ecmascript/chapter-7-1-oop-general-theory/
概論、范式與思想
在進(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ì)的差異。
基于類(lèi)特性和基于原型
注意,在前面一句很重要的一點(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è)列舉一下。 讓我們考慮總則和這些范式的主要概念。
基于靜態(tài)類(lèi)
在基于類(lèi)的模型中,有個(gè)關(guān)于類(lèi)和實(shí)例的概念。 類(lèi)的實(shí)例也常常被命名為對(duì)象或范例 。
類(lèi)與對(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ì)象根本不需要的屬性的話(huà),那子類(lèi)也有擁有這些屬性。
基于類(lèi)的關(guān)鍵概念
因此,我們有如下關(guān)鍵概念:
1.創(chuàng)建一個(gè)對(duì)象之前,必須聲明類(lèi),首先有必要界定其類(lèi)
2.因此,該對(duì)象將由抽象成自身“象形和相似性”(結(jié)構(gòu)和行為)的類(lèi)里創(chuàng)建
3.方法是通過(guò)了嚴(yán)格的,直接的,一成不變的繼承鏈來(lái)處理
4.子類(lèi)包含了繼承鏈中所有的屬性(即使其中的某些屬性是子類(lèi)不需要的);
5.創(chuàng)建類(lèi)實(shí)例,類(lèi)不能(因?yàn)殪o態(tài)模型)來(lái)改變其實(shí)例的特征(屬性或方法);
6.實(shí)例(因?yàn)閲?yán)格的靜態(tài)模型)除了有該實(shí)例所對(duì)應(yīng)類(lèi)里聲明的行為和屬性以外,是不能額外的行為或?qū)傩缘摹?/p>
讓我們看看在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中的method_missing;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)里看到的,他們也有自身的特性。
Concatenative模型
老實(shí)說(shuō),有必要在說(shuō)句話(huà)關(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)存使用率搞。
Duck類(lè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)型。
基于原型的關(guān)鍵概念
讓我們來(lái)看一下這種方式的主要特點(diǎn):
1.基本概念是對(duì)象
2.對(duì)象是完全動(dòng)態(tài)可變的(理論上完全可以從一個(gè)類(lèi)型轉(zhuǎn)化到另一個(gè)類(lèi)型)
3.對(duì)象沒(méi)有描述自己的結(jié)構(gòu)和行為的嚴(yán)格類(lèi),對(duì)象不需要類(lèi)
4.對(duì)象沒(méi)有類(lèi)類(lèi)但可以可以有原型,他們?nèi)绻荒茼憫?yīng)消息的話(huà)可以委托給原型
5.在運(yùn)行時(shí)隨時(shí)可以改變對(duì)象的原型;
6.在基于委托的模型中,改變?cè)偷奶攸c(diǎn),將影響到與該原型相關(guān)的所有對(duì)象;
7.在concatenative原型模型中,原型是從其他對(duì)象克隆的原始副本,并進(jìn)一步成為完全獨(dú)立的副本原件,原型特性的變換不會(huì)影響從它克隆的對(duì)象
8.如果不能響應(yīng)消息,它的調(diào)用者可以采取額外的措施(例如,改變調(diào)度)
9.對(duì)象的失敗可以不由它們的層次和所屬哪個(gè)類(lèi)來(lái)決定,而是由當(dāng)前特性來(lái)決定
不過(guò),還有一個(gè)模型,我們也應(yīng)該考慮。
基于動(dòng)態(tài)類(lèi)
我們認(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í)例里訪(fǎng)問(wèn)到
a.b = 30 # 創(chuàng)建a自身的屬性
print(a.b) # 30
del a.b # 刪除自身的屬性
print(a.b) # 20 - 再次從類(lèi)里獲?。ㄔ停?br />
# 就像基于原型的模型
# 可以在運(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)的特性呢。
各種OOP實(shí)現(xià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ì)滴。
多態(tài)
在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ì)象的訪(fǎng)問(wèn)級(jí)別或訪(fǎng)問(wèn)修飾符)。
在這里我要提醒一下封裝的主要目的:封裝是一個(gè)抽象的增加,而不是選拔個(gè)直接往你的類(lèi)里寫(xiě)入一些東西的隱藏“惡意黑客”。
這是一個(gè)很大的錯(cuò)誤:為了隱藏使用隱藏。
訪(fǎng)問(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ò)下劃線(xiàn)這個(gè)命名規(guī)范),從外部不可訪(fǎng)問(wèn)。 另一方面,Python可以通過(guò)特殊的規(guī)則從外部訪(fǎng)問(wèn)(_ClassName__field_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)訪(fǎng)問(wèn)
print(a._A__private) # 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)里訪(fǎng)問(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ò)訪(fǎng)問(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ò)在變量前加下劃線(xiàn)來(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是代碼重用的一種便捷方式。 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
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ì)象組合
對(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)系。
由于顯示組合的缺乏(與繼承相比的靈活性),增加中間代碼也是可以的。
AOP特性
作為面向方面的一個(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'
結(jié)論
在這篇文章,我們理清了OOP的概論(我希望這些資料已經(jīng)對(duì)你有用了),下一章節(jié)我們將繼續(xù)面向?qū)ο缶幊讨瓻CMAScript的實(shí)現(xiàn) 。
相關(guān)文章
JavaScript 頁(yè)面坐標(biāo)相關(guān)知識(shí)整理
對(duì)于頁(yè)面的一些坐標(biāo)與位置分析,一般需要控制層的位置的朋友有幫助。需要的朋友可以參考下。2010-01-01詳解JavaScript中undefined與null的區(qū)別
大多數(shù)計(jì)算機(jī)語(yǔ)言,有且僅有一個(gè)表示"無(wú)"的值,比如,C語(yǔ)言的NULL,Java語(yǔ)言的null,Python語(yǔ)言的none,Ruby語(yǔ)言的nil2014-03-03js split 的用法和定義 js split分割字符串成數(shù)組的實(shí)例代碼
關(guān)于js split的用法,我們經(jīng)常用來(lái)將字符串分割為數(shù)組方便后續(xù)操作,今天寫(xiě)一段廣告判斷代碼的時(shí)候,竟然忘了split的用法了,特整理下,方便需要的朋友2012-05-05Javascript學(xué)習(xí)筆記2 函數(shù)
在Javascript中,function才是Javascript的第一型。當(dāng)我們寫(xiě)下一段函數(shù)時(shí),其實(shí)不過(guò)是建立了一個(gè)function類(lèi)型的實(shí)體。2010-01-01javascript對(duì)象之內(nèi)置對(duì)象Math使用方法
Math對(duì)象的一些方法能實(shí)現(xiàn)我們課本上的某些數(shù)學(xué)計(jì)算,比較常用的方法有如下幾個(gè)2010-04-04