感謝社區(qū)中各位的大力支持,譯者再次奉上一點(diǎn)點(diǎn)福利:阿里云產(chǎn)品券,享受所有官網(wǎng)優(yōu)惠,并抽取幸運(yùn)大獎:點(diǎn)擊這里領(lǐng)取
在第五章中,我們詳細(xì)地討論了[[Prototype]]
機(jī)制,和 為什么 對于描述“類”或“繼承”來說它是那么使人糊涂和不合適。我們一路跋涉,不僅涉及了相當(dāng)繁冗的語法(使代碼凌亂的.prototype
),還有各種陷阱(比如使人吃驚的.constructor
解析和難看的假想多態(tài)語法)。我們探索了許多人試圖用抹平這些粗糙的區(qū)域而使用的各種“mixin”方法。
這時一個常見的反應(yīng)是,想知道為什么這些看起來如此簡單的事情這么復(fù)雜。現(xiàn)在我們已經(jīng)拉開帷幕看到了它是多么麻煩,這并不奇怪:大多數(shù)JS開發(fā)者從不探究得這么深,而將這一團(tuán)糟交給一個“類”包去幫他們處理。
我希望到現(xiàn)在你不會甘心于敷衍了事并把這樣的細(xì)節(jié)丟給一個“黑盒”庫。現(xiàn)在我們來深入講解我們 如何與應(yīng)當(dāng)如何 以一種比類造成的困惑 簡單得多而且更直接的方式 來考慮JS中對象的[[Prototype]]
機(jī)制。
簡單地復(fù)習(xí)一下第五章的結(jié)論,[[Prototype]]
機(jī)制是一種存在于一個對象上的內(nèi)部鏈接,它指向一個其他對象。
當(dāng)一個屬性/方法引用在第一個對象上發(fā)生,而這樣的屬性/方法又不存在時,這個鏈接就會被使用。在這種情況下,[[Prototype]]
鏈接告訴引擎去那個被鏈接的對象上尋找該屬性/方法。接下來,如果那個對象也不能滿足查詢,就沿著它的[[Prototype]]
查詢,如此繼續(xù)。這種對象間一系列的鏈接構(gòu)成了所謂的“原形鏈”。
換句話說,對于我們能在JavaScript中利用的功能的實(shí)際機(jī)制來說,其重要的實(shí)質(zhì) 全部在于被連接到其他對象的對象。
這個觀點(diǎn)是理解本章其余部分的動機(jī)和方法的重要基礎(chǔ)!
邁向面相委托的設(shè)計(jì)
為了將我們的思想恰當(dāng)?shù)丶性谌绾斡米钪苯亓水?dāng)?shù)姆椒ㄊ褂?code>[[Prototype]],我們必須認(rèn)識到它代表一種根本上與類不同的設(shè)計(jì)模式(見第四章)。
注意* 某些 面相類的設(shè)計(jì)依然是很有效的,所以不要扔掉你知道的每一件事(扔掉大多數(shù)就行了!)。比如,封裝 就十分強(qiáng)大,而且與委托兼容的(雖然不那么常見)。
我們需要試著將我們的思維從類/繼承的設(shè)計(jì)模式轉(zhuǎn)變?yōu)樾袨榇碓O(shè)計(jì)模式。如果你已經(jīng)用在教育/工作生涯中思考類的方式做了大多數(shù)或所有的編程工作,這可能感覺不舒服或不自然。你可能需要嘗試這種思維過程好幾次,才能適應(yīng)這種非常不同的思考方式。
我將首先帶你進(jìn)行一些理論練習(xí),之后我們會一對一地看一些更實(shí)際的例子來為你自己的代碼提供實(shí)踐環(huán)境。
類理論
比方說我們有幾個相似的任務(wù)(“XYZ”,“ABC”,等)需要在我們的軟件中建模。
使用類,你設(shè)計(jì)這個場景的方式是:定義一個泛化的父類(基類)比如Task
,為所有的“同類”任務(wù)定義共享的行為。然后,你定義子類XYZ
和ABC
,它們都繼承自Task
,每個都分別添加了特化的行為來處理各自的任務(wù)。
重要的是, 類設(shè)計(jì)模式將鼓勵你發(fā)揮繼承的最大功效,當(dāng)你在XYZ
任務(wù)中覆蓋Task
的某些泛化方法的定義時,你將會想利用方法覆蓋(和多態(tài)),也許會利用super
來調(diào)用這個方法泛化版本,為它添加更多的行為。你很可能會找到幾個可以“抽象”到父類中,或在子類中特化(覆蓋)的地方。
這是一些關(guān)于這個場景的假想代碼:
class Task {
id;
// `Task()`構(gòu)造器
Task(ID) { id = ID; }
outputTask() { output( id ); }
}
class XYZ inherits Task {
label;
// `XYZ()`構(gòu)造器
XYZ(ID,Label) { super( ID ); label = Label; }
outputTask() { super(); output( label ); }
}
class ABC inherits Task {
// ...
}
現(xiàn)在,你可以初始化一個或多個XYZ
子類的 拷貝,并且使用這些實(shí)例來執(zhí)行“XYZ”任務(wù)。這些實(shí)例已經(jīng) 同時拷貝 了泛化的Task
定義的行為和具體的XYZ
定義的行為。類似地,ABC
類的實(shí)例將拷貝Task
的行為和具體的ABC
的行為。在構(gòu)建完成之后,你一般會僅與這些實(shí)例互動(而不是類),因?yàn)槊總€實(shí)例都拷貝了完成計(jì)劃任務(wù)的所有行為。
委托理論
但是現(xiàn)在然我們試著用 行為委托 代替 類 來思考同樣的問題。
你將首先定義一個稱為Task
的 對象(不是一個類,也不是一個大多數(shù)JS開發(fā)者想讓你相信的function
),而且它將擁有具體的行為,這些行為包含各種任務(wù)可以使用的(讀作:委托至!)工具方法。然后,對于每個任務(wù)(“XYZ”,“ABC”),你定義一個 對象 來持有這個特定任務(wù)的數(shù)據(jù)/行為。你 鏈接 你的特定任務(wù)對象到Task
工具對象,允許它們在必要的時候可以委托到它。
基本上,你認(rèn)為執(zhí)行任務(wù)“XYZ”就是從兩個兄弟/對等的對象(XYZ
和Task
)中請求行為來完成它。與其通過類的拷貝將它們組合在一起,我們可以將他們保持在分離的對象中,而且可以在需要的情況下允許XYZ
對象來 委托到 Task
。
這里是一些簡單的代碼,示意你如何實(shí)現(xiàn)它:
var Task = {
setID: function(ID) { this.id = ID; },
outputID: function() { console.log( this.id ); }
};
// 使`XYZ`委托到`Task`
var XYZ = Object.create( Task );
XYZ.prepareTask = function(ID,Label) {
this.setID( ID );
this.label = Label;
};
XYZ.outputTaskDetails = function() {
this.outputID();
console.log( this.label );
};
// ABC = Object.create( Task );
// ABC ... = ...
在這段代碼中,Task
和XYZ
不是類(也不是函數(shù)),它們 僅僅是對象。XYZ
通過Object.create()
創(chuàng)建,來[[Prototype]]
委托到Task
對象(見第五章)。
作為與面相類(也就是,OO——面相對象)的對比,我稱這種風(fēng)格的代碼為 “OLOO”(objects-linked-to-other-objects(鏈接到其他對象的對象))。所有我們 真正 關(guān)心的是,對象XYZ
委托到對象Task
(對象ABC
也一樣)。
在JavaScript中,[[Prototype]]
機(jī)制將 對象 鏈接到其他 對象。無論你多么想說服自己這不是真的,JavaScript沒有像“類”那樣的抽象機(jī)制。這就像逆水行舟:你 可以 做到,但你 選擇 了逆流而上,所以很明顯地,你會更困難地達(dá)到目的地。
OLOO風(fēng)格的代碼 中有一些需要注意的不同:
- 前一個類的例子中的
id
和label
數(shù)據(jù)成員都是XYZ
上的直接數(shù)據(jù)屬性(它們都不在Task
上)。一般來說,當(dāng)[[Prototype]]
委托引入時,你想使?fàn)顟B(tài)保持在委托者上(XYZ
,ABC
),不是在委托上(Task
)。 - 在類的設(shè)計(jì)模式中,我們故意在父類(
Task
)和子類(XYZ
)上采用相同的命名outputTask
,以至于我們可以利用覆蓋(多態(tài))。在委托的行為中,我們反其道而行之:我們盡一切可能避免在[[Prototype]]
鏈的不同層級上給出相同的命名(稱為“遮蔽”——見第五章),因?yàn)檫@些命名沖突會導(dǎo)致尷尬/脆弱的語法來消除引用的歧義(見第四章),而我們想避免它。
這種設(shè)計(jì)模式不那么要求那些傾向于被覆蓋的泛化的方法名,而是要求針對于每個對象的 具體 行為類型給出更具描述性的方法名。這實(shí)際上會產(chǎn)生更易于理解/維護(hù)的代碼,因?yàn)榉椒ú粌H在定義的位置,而是擴(kuò)散到其他代碼中)變得更加明白(代碼即文檔)。 -
this.setID(ID);
位于對象XYZ
的一個方法內(nèi)部,它首先在XYZ
上查找setID(..)
,但因?yàn)樗荒茉?code>XYZ上找到叫這個名稱的方法,[[Prototype]]
委托意味著它可以沿著鏈接到Task
來尋找setID()
,這樣當(dāng)然就找到了。另外,由于調(diào)用點(diǎn)的隱含this
綁定規(guī)則(見第二章),當(dāng)setID()
運(yùn)行時,即便方法是在Task
上找到的,這個函數(shù)調(diào)用的this
綁定依然是我們期望和想要的XYZ
。我們在代碼稍后的this.outputID()
中也看到了同樣的事情。
換句話說,我們可以使用存在于Task
上的泛化工具與XYZ
互動,因?yàn)?code>XYZ可以委托至Task
。
行為委托 意味著:在某個對象(XYZ
)的屬性或方法沒能在這個對象(XYZ
)上找到時,讓這個對象(XYZ
)為屬性或方法引用提供一個委托(Task
)。
這是一個 極其強(qiáng)大 的設(shè)計(jì)模式,與父類和子類,繼承,多態(tài)等有很大的不同。與其在你的思維中縱向地,從上面父類到下面子類地組織對象,你應(yīng)帶并列地,對等地考慮對象,而且對象間擁有方向性的委托鏈接。
注意: 委托更適于作為內(nèi)部實(shí)現(xiàn)的細(xì)節(jié),而不是直接暴露在API接口的設(shè)計(jì)中。在上面的例子中,我們的API設(shè)計(jì)沒必要有意地讓開發(fā)者調(diào)用XYZ.setID()
(當(dāng)然我們可以!)。我們以某種隱藏的方式將委托作為我們API的內(nèi)部細(xì)節(jié),即XYZ.prepareTask(..)
委托到Task.setID(..)
。詳細(xì)的內(nèi)容,參照第五章的“鏈接作為候補(bǔ)?”中的討論。
相互委托(不允許)
你不能在兩個或多個對象間相互地委托(雙向地)對方來創(chuàng)建一個 循環(huán) 。如果你使B
鏈接到A
,然后試著讓A
鏈接到B
,那么你將得到一個錯誤。
這樣的事情不被允許有些可惜(不是非常令人驚訝,但稍稍有些惱人)。如果你制造一個在任意一方都不存在的屬性/方法引用,你就會在[[Prototype]]
上得到一個無限遞歸的循環(huán)。但如果所有的引用都嚴(yán)格存在,那么B
就可以委托至A
,或相反,而且它可以工作。這意味著你可以為了多種任務(wù)用這兩個對象互相委托至對方。有一些情況這可能會有用。
但它不被允許是因?yàn)橐娴膶?shí)現(xiàn)者發(fā)現(xiàn),在設(shè)置時檢查(并拒絕!)無限循環(huán)引用一次,要比每次你在一個對象上查詢屬性時都做相同檢查的性能要高。
調(diào)試
我們將簡單地討論一個可能困擾開發(fā)者的微妙的細(xì)節(jié)。一般來說,JS語言規(guī)范不會控制瀏覽器開發(fā)者工具如何向開發(fā)者表示指定的值/結(jié)構(gòu),所以每種瀏覽器/引擎都自由地按需要解釋這個事情。因此,瀏覽器/工具 不總是意見統(tǒng)一。特別地,我們現(xiàn)在要考察的行為就是當(dāng)前僅在Chrome的開發(fā)者工具中觀察到的。
考慮這段傳統(tǒng)的“類構(gòu)造器”風(fēng)格的JS代碼,正如它將在Chrome開發(fā)者工具 控制臺 中出現(xiàn)的:
function Foo() {}
var a1 = new Foo();
a1; // Foo {}
讓我們看一下這個代碼段的最后一行:對表達(dá)式a1
進(jìn)行求值的輸出,打印Foo {}
。如果你在FireFox中試用同樣的代碼,你很可能會看到Object {}
。為什么會有不同?這些輸出意味著什么?
Chrome實(shí)質(zhì)上在說“{}是一個由名為‘Foo’的函數(shù)創(chuàng)建的空對象”。Firefox在說“{}是一個由Object普通構(gòu)建的空對象”。這種微妙的區(qū)別是因?yàn)镃hrome在像一個 內(nèi)部屬性 一樣,動態(tài)跟蹤執(zhí)行創(chuàng)建的實(shí)際方法的名稱,而其他瀏覽器不會跟蹤這樣的附加信息。
試圖用JavaScript機(jī)制來解釋它很吸引人:
function Foo() {}
var a1 = new Foo();
a1.constructor; // Foo(){}
a1.constructor.name; // "Foo"
那么,Chrome就是通過簡單地查看對象的.Constructor.name
來輸出“Foo”的?令人費(fèi)解的是,答案既是“是”也是“不”。
考慮下面的代碼:
function Foo() {}
var a1 = new Foo();
Foo.prototype.constructor = function Gotcha(){};
a1.constructor; // Gotcha(){}
a1.constructor.name; // "Gotcha"
a1; // Foo {}
即便我們將a1.constructor.name
合法地改變?yōu)槠渌臇|西(“Gotcha”),Chrome控制臺依舊使用名稱“Foo”。
那么,說明前面問題(它使用.constructor.name
嗎?)的答案是 不,他一定在內(nèi)部追蹤其他的什么東西。
但是,且慢!讓我們看看這種行為如何與OLOO風(fēng)格的代碼一起工作:
var Foo = {};
var a1 = Object.create( Foo );
a1; // Object {}
Object.defineProperty( Foo, "constructor", {
enumerable: false,
value: function Gotcha(){}
});
a1; // Gotcha {}
啊哈!Gotcha,Chrome的控制臺 確實(shí) 尋找并且使用了.constructor.name
。實(shí)際上,就在寫這本書的時候,正是這個行為被認(rèn)定為是Chrome的一個Bug,而且就在你讀到這里的時候,它可能已經(jīng)被修復(fù)了。所以你可能已經(jīng)看到了被修改過的 a1; // Object{}
。
這個bug暫且不論,Chrome執(zhí)行的(剛剛在代碼段中展示的)“構(gòu)造器名稱”內(nèi)部追蹤(目前僅用于調(diào)試輸出的目的),是一個僅在Chrome內(nèi)部存在的擴(kuò)張行為,它已經(jīng)超出了JS語言規(guī)范要求的范圍。
如果你不使用“構(gòu)造器”來制造你的對象,就像我們在本章的OLOO風(fēng)格代碼中不鼓勵的那樣,那么你將會得到一個Chrome不會為其追蹤內(nèi)部“構(gòu)造器名稱”的對象,所以這樣的對象將正確地僅僅被輸出“Object {}”,意味著“從Object()構(gòu)建生成的對象”。
不要認(rèn)為 這代表一個OLOO風(fēng)格代碼的缺點(diǎn)。當(dāng)你用OLOO編碼而且用行為代理作為你的設(shè)計(jì)模式時,誰 “創(chuàng)建了”(也就是,哪個函數(shù) 被和new
一起調(diào)用了?)一些對象是一個無關(guān)的細(xì)節(jié)。Chrome特殊的內(nèi)部“構(gòu)造器名稱”追蹤僅僅在你完全接受“類風(fēng)格”編碼時才有用,而在你接受OLOO委托時是沒有意義的。
思維模型比較
現(xiàn)在你至少在理論上可以看到“類”和“委托”設(shè)計(jì)模式的不同了,讓我們看看這些設(shè)計(jì)模式在我們用來推導(dǎo)我們代碼的思維模型上的含義。
我們將查看一些更加理論上的(“Foo”,“Bar”)代碼,然后比較兩種方法(OO vs. OLOO)的代碼實(shí)現(xiàn)。第一段代碼使用經(jīng)典的(“原型的”)OO風(fēng)格:
function Foo(who) {
this.me = who;
}
Foo.prototype.identify = function() {
return "I am " + this.me;
};
function Bar(who) {
Foo.call( this, who );
}
Bar.prototype = Object.create( Foo.prototype );
Bar.prototype.speak = function() {
alert( "Hello, " + this.identify() + "." );
};
var b1 = new Bar( "b1" );
var b2 = new Bar( "b2" );
b1.speak();
b2.speak();
父類Foo
,被子類Bar
繼承,之后Bar
被初始化兩次:b1
和b2
。我們得到的是b1
委托至Bar.prototype
,Bar.prototype
委托至Foo.prototype
。這對你來說應(yīng)當(dāng)看起來十分熟悉。沒有太具開拓性的東西發(fā)生。
現(xiàn)在,讓我們使用 OLOO 風(fēng)格的代碼 實(shí)現(xiàn)完全相同的功能:
var Foo = {
init: function(who) {
this.me = who;
},
identify: function() {
return "I am " + this.me;
}
};
var Bar = Object.create( Foo );
Bar.speak = function() {
alert( "Hello, " + this.identify() + "." );
};
var b1 = Object.create( Bar );
b1.init( "b1" );
var b2 = Object.create( Bar );
b2.init( "b2" );
b1.speak();
b2.speak();
我們利用了完全相同的從Bar
到Foo
的[[Prototype]]
委托,正如我們在前一個代碼段中b1
,Bar.prototype
,和Foo.prototype
之間那樣。我們?nèi)匀挥?個對象鏈接在一起。
但重要的是,我們極大地簡化了發(fā)生的 所有其他事項(xiàng),因?yàn)槲覀儸F(xiàn)在僅僅建立了相互鏈接的 對象,而不需要所有其他討厭且困惑的看起來像類(但動起來不像)的東西,還有構(gòu)造器,原型和new
調(diào)用。
問問你自己:如果我能用OLOO風(fēng)格代碼得到我用“類”風(fēng)格代碼得到的一樣的東西,但OLOO更簡單而且需要考慮的事情更少,OLOO不是更好嗎?
讓我們講解一下這兩個代碼段間涉及的思維模型。
首先,類風(fēng)給的代碼段意味著這樣的實(shí)體與它們的關(guān)系的思維模型:
[圖片上傳失敗...(image-75b1c6-1515410940434)]
實(shí)際上,這有點(diǎn)兒不公平/誤導(dǎo),因?yàn)樗故玖嗽S多額外的,你在 技術(shù)上 一直不需要知道(雖然你 需要 理解它)的細(xì)節(jié)。一個關(guān)鍵是,它是一系列十分復(fù)雜的關(guān)系。但另一個關(guān)鍵是:如果你花時間來沿著這些關(guān)系的箭頭走,在JS的機(jī)制中 有數(shù)量驚人的內(nèi)部統(tǒng)一性。
例如,JS函數(shù)可以訪問call(..)
,apply(..)
和bind(..)
(見第二章)的能力是因?yàn)楹瘮?shù)本身是對象,而函數(shù)對象還擁有一個[[Prototype]]
鏈接,鏈到Function.prototype
對象,它定義了那些任何函數(shù)對象都可以委托到的默認(rèn)方法。JS可以做這些事情,你也能!
好了,現(xiàn)在讓我們看一個這張圖的 稍稍 簡化的版本,用它來進(jìn)行比較稍微“公平”一點(diǎn)——它僅展示了 相關(guān) 的實(shí)體與關(guān)系。
[圖片上傳失敗...(image-f28224-1515410940434)]
任然非常復(fù)雜,對吧?虛線描繪了當(dāng)你在Foo.prototype
和Bar.prototype
間建立“繼承”時的隱含關(guān)系,而且還沒有 修復(fù) 丟失的 .constructor
屬性引用(見第五章“終極構(gòu)造器”)。即便將虛線去掉,每次你與對象鏈接打交道時,這個思維模型依然要變很多可怕的戲法。
現(xiàn)在,然我們看看OLOO風(fēng)格代碼的思維模型:
[圖片上傳失敗...(image-30e581-1515410940434)]
正如你所比較它們得到的,十分明顯,OLOO風(fēng)格的代碼 需要關(guān)心的東西少太多了,因?yàn)镺LOO風(fēng)格代碼接受了 事實(shí):我們唯一需要真正關(guān)心的事情是 鏈接到其他對象的對象。
所有其他“類”的爛設(shè)計(jì)用一種令人費(fèi)解而且復(fù)雜的方式得到相同的結(jié)果。去掉那些東西,事情就變得簡單得多(還不會失去任何功能)。
Classes vs. Objects
我們已經(jīng)看到了各種理論的探索和“類”與“行為委托”的思維模型的比較。現(xiàn)在讓我們來看看更具體的代碼場景,來展示你如何實(shí)際應(yīng)用這些想法。
我們將首先講解一種在前端網(wǎng)頁開發(fā)中的典型場景:建造UI部件(按鈕,下拉列表等等)。
Widget“類”
因?yàn)槟憧赡苓€是如此地習(xí)慣于OO設(shè)計(jì)模式,你很可能會立即這樣考慮這個問題:一個父類(也許稱為Wedget
)擁有所有共通的基本部件行為,然后衍生的子類擁有具體的部件類型(比如Button
)。
注意: 為了DOM和CSS的操作,我們將在這里使用JQuery,這僅僅是因?yàn)閷τ谖覀儸F(xiàn)在的討論,它不是一個我們真正關(guān)心的細(xì)節(jié)。這些代碼中不關(guān)心你用哪個JS框架(JQuery,Dojo,YUI等等)來解決如此無趣的問題。
讓我們來看看,在沒有任何“類”幫助庫或語法的情況下,我們?nèi)绾斡媒?jīng)典風(fēng)格的純JS來實(shí)現(xiàn)“類”設(shè)計(jì):
// 父類
function Widget(width,height) {
this.width = width || 50;
this.height = height || 50;
this.$elem = null;
}
Widget.prototype.render = function($where){
if (this.$elem) {
this.$elem.css( {
width: this.width + "px",
height: this.height + "px"
} ).appendTo( $where );
}
};
// 子類
function Button(width,height,label) {
// "super"構(gòu)造器調(diào)用
Widget.call( this, width, height );
this.label = label || "Default";
this.$elem = $( "<button>" ).text( this.label );
}
// 使`Button` “繼承” `Widget`
Button.prototype = Object.create( Widget.prototype );
// 覆蓋“繼承來的” `render(..)`
Button.prototype.render = function($where) {
// "super"調(diào)用
Widget.prototype.render.call( this, $where );
this.$elem.click( this.onClick.bind( this ) );
};
Button.prototype.onClick = function(evt) {
console.log( "Button '" + this.label + "' clicked!" );
};
$( document ).ready( function(){
var $body = $( document.body );
var btn1 = new Button( 125, 30, "Hello" );
var btn2 = new Button( 150, 40, "World" );
btn1.render( $body );
btn2.render( $body );
} );
OO設(shè)計(jì)模式告訴我們要在父類中聲明一個基礎(chǔ)render(..)
,之后在我們的子類中覆蓋它,但不是完全替代它,而是用按鈕特定的行為增強(qiáng)這個基礎(chǔ)功能。
注意 顯示假想多態(tài) 的丑態(tài),Widget.call
和Widget.prototype.render.call
引用是為了偽裝從子“類”方法得到“父類”基礎(chǔ)方法支持的“super”調(diào)用。呃。
ES6 class
語法糖
我們會在附錄A中講解ES6的class
語法糖,但是讓我們演示一下我們?nèi)绾斡?code>class來實(shí)現(xiàn)相同的代碼。
class Widget {
constructor(width,height) {
this.width = width || 50;
this.height = height || 50;
this.$elem = null;
}
render($where){
if (this.$elem) {
this.$elem.css( {
width: this.width + "px",
height: this.height + "px"
} ).appendTo( $where );
}
}
}
class Button extends Widget {
constructor(width,height,label) {
super( width, height );
this.label = label || "Default";
this.$elem = $( "<button>" ).text( this.label );
}
render($where) {
super.render( $where );
this.$elem.click( this.onClick.bind( this ) );
}
onClick(evt) {
console.log( "Button '" + this.label + "' clicked!" );
}
}
$( document ).ready( function(){
var $body = $( document.body );
var btn1 = new Button( 125, 30, "Hello" );
var btn2 = new Button( 150, 40, "World" );
btn1.render( $body );
btn2.render( $body );
} );
毋庸置疑,通過使用ES6的class
,許多前面經(jīng)典方法中語法的丑態(tài)被改善了。super(..)
的存在看起來非常適宜(但當(dāng)你深入挖掘它時,不全是好事!)。
除了語法上的改進(jìn),這些都不是 真正的 類,因?yàn)樗麄內(nèi)匀还ぷ髟?code>[[Prototype]]機(jī)制之上。它們依然會受到思維模型不匹配的拖累,就像我們在第四,五章中,和直到現(xiàn)在探索的那樣。附錄A將會詳細(xì)講解ES6class
語法和他的含義。我們將會看到為什么解決語法上的小問題不會實(shí)質(zhì)上解決我們在JS中的類的困惑,雖然它做出了勇敢的努力假裝解決了問題!
無論你是使用經(jīng)典的原型語法還是新的ES6語法糖,你依然選擇了使用“類”來對問題(UI部件)進(jìn)行建模。正如我們前面幾章試著展示的,在JavaScript中做這個選擇會帶給你額外的頭疼和思維上的彎路。
委托部件對象
這是我們更簡單的Widget
/Button
例子,使用了 OLOO風(fēng)格委托:
var Widget = {
init: function(width,height){
this.width = width || 50;
this.height = height || 50;
this.$elem = null;
},
insert: function($where){
if (this.$elem) {
this.$elem.css( {
width: this.width + "px",
height: this.height + "px"
} ).appendTo( $where );
}
}
};
var Button = Object.create( Widget );
Button.setup = function(width,height,label){
// delegated call
this.init( width, height );
this.label = label || "Default";
this.$elem = $( "<button>" ).text( this.label );
};
Button.build = function($where) {
// delegated call
this.insert( $where );
this.$elem.click( this.onClick.bind( this ) );
};
Button.onClick = function(evt) {
console.log( "Button '" + this.label + "' clicked!" );
};
$( document ).ready( function(){
var $body = $( document.body );
var btn1 = Object.create( Button );
btn1.setup( 125, 30, "Hello" );
var btn2 = Object.create( Button );
btn2.setup( 150, 40, "World" );
btn1.build( $body );
btn2.build( $body );
} );
使用這種OLOO風(fēng)格的方法,我們不認(rèn)為Widget
是一個父類而Button
是一個子類,Wedget
只是一個對象 和某種具體類型的部件也許想要代理到的工具的集合,而且Button
也只是一個獨(dú)立的對象(當(dāng)然,帶有委托至Wedget
的鏈接!)。
從設(shè)計(jì)模式的角度來看,我們 沒有 像類的方法建議的那樣,在兩個對象中共享相同的render(..)
方法名稱,而是選擇了更能描述每個特定任務(wù)的不同的名稱。同樣的原因,初始化 方法被分別稱為init(..)
和setup(..)
。
不僅委托設(shè)計(jì)模式建議使用不同而且更具描述性的名稱,而且在OLOO中這樣做會避免難看的顯式假想多態(tài)調(diào)用,正如你可以通過簡單,相對的this.init(..)
和this.insert(..)
委托調(diào)用看到的。
語法上,我們也沒有任何構(gòu)造器,.prototype
或者new
出現(xiàn),它們事實(shí)上是不必要的設(shè)計(jì)。
現(xiàn)在,如果你再細(xì)心考察一下,你可能會注意到之前僅有一個調(diào)用(var btn1 = new Button(..)
),而現(xiàn)在有了兩個(var btn1 = Object.create(Button)
和btn1.setup(..)
)。這猛地看起來像是一個缺點(diǎn)(代碼變多了)。
然而,即便是這樣的事情,和經(jīng)典原型風(fēng)格比起來也是 OLOO風(fēng)格代碼的優(yōu)點(diǎn)。為什么?
用類的構(gòu)造器,你“強(qiáng)制”(不完全是這樣,但是被強(qiáng)烈建議)構(gòu)建和初始化在同一個步驟中進(jìn)行。然而,有許多種情況,能夠?qū)⑦@兩步分開做(就像你在OLOO中做的)更靈活。
舉個例子,我們假定你在程序的最開始,在一個池中創(chuàng)建所有的實(shí)例,但你等到在它們被從池中找出并使用之前再用指定的設(shè)置初始化它們。我們的例子中,這兩個調(diào)用緊挨在一起,當(dāng)然它們也可以按需要發(fā)生在非常不同的時間和代碼中非常不同的部分。
OLOO 對關(guān)注點(diǎn)分離原則有 更好 的支持,也就是創(chuàng)建和初始化沒有必要合并在同一個操作中。
更簡單的設(shè)計(jì)
OLOO除了提供表面上更簡單(而且更靈活!)的代碼之外,行為委托作為一個模式實(shí)際上會帶來更簡單的代碼架構(gòu)。讓我們講解最后一個例子來說明OLOO是如何簡化你的整體設(shè)計(jì)的。
這個場景中我們將講解兩個控制器對象,一個用來處理網(wǎng)頁的登錄form(表單),另一個實(shí)際處理服務(wù)器的認(rèn)證(通信)。
我們需要幫助工具來進(jìn)行與服務(wù)器的Ajax通信。我們將使用JQuery(雖然其他的框架都可以),因?yàn)樗粌H為我們處理Ajax,而且還返回一個類似Promise的應(yīng)答,這樣我們就可以在代碼中使用.then(..)
來監(jiān)聽這個應(yīng)答。
注意: 我們不會再這里講到Promise,但我們會在以后的 你不懂JS 系列中講到。
根據(jù)典型的類的設(shè)計(jì)模式,我們在一個叫做Controller
的類中將任務(wù)分解為基本功能,之后我們會衍生出兩個子類,LoginController
和AuthController
,它們都繼承自Controller
而且特化某些基本行為。
// 父類
function Controller() {
this.errors = [];
}
Controller.prototype.showDialog = function(title,msg) {
// 在對話框中給用戶顯示標(biāo)題和消息
};
Controller.prototype.success = function(msg) {
this.showDialog( "Success", msg );
};
Controller.prototype.failure = function(err) {
this.errors.push( err );
this.showDialog( "Error", err );
};
// 子類
function LoginController() {
Controller.call( this );
}
// 將子類鏈接到父類
LoginController.prototype = Object.create( Controller.prototype );
LoginController.prototype.getUser = function() {
return document.getElementById( "login_username" ).value;
};
LoginController.prototype.getPassword = function() {
return document.getElementById( "login_password" ).value;
};
LoginController.prototype.validateEntry = function(user,pw) {
user = user || this.getUser();
pw = pw || this.getPassword();
if (!(user && pw)) {
return this.failure( "Please enter a username & password!" );
}
else if (pw.length < 5) {
return this.failure( "Password must be 5+ characters!" );
}
// 到這里了?輸入合法!
return true;
};
// 覆蓋來擴(kuò)展基本的`failure()`
LoginController.prototype.failure = function(err) {
// "super"調(diào)用
Controller.prototype.failure.call( this, "Login invalid: " + err );
};
// 子類
function AuthController(login) {
Controller.call( this );
// 除了繼承外,我們還需要合成
this.login = login;
}
// 將子類鏈接到父類
AuthController.prototype = Object.create( Controller.prototype );
AuthController.prototype.server = function(url,data) {
return $.ajax( {
url: url,
data: data
} );
};
AuthController.prototype.checkAuth = function() {
var user = this.login.getUser();
var pw = this.login.getPassword();
if (this.login.validateEntry( user, pw )) {
this.server( "/check-auth",{
user: user,
pw: pw
} )
.then( this.success.bind( this ) )
.fail( this.failure.bind( this ) );
}
};
// 覆蓋以擴(kuò)展基本的`success()`
AuthController.prototype.success = function() {
// "super"調(diào)用
Controller.prototype.success.call( this, "Authenticated!" );
};
// 覆蓋以擴(kuò)展基本的`failure()`
AuthController.prototype.failure = function(err) {
// "super"調(diào)用
Controller.prototype.failure.call( this, "Auth Failed: " + err );
};
var auth = new AuthController(
// 除了繼承,我們還需要合成
new LoginController()
);
auth.checkAuth();
我們有所有控制器分享的基本行為,它們是success(..)
,failure(..)
和showDialog(..)
。我們的子類LoginController
和AuthController
覆蓋了failure(..)
和success(..)
來增強(qiáng)基本類的行為。還要注意的是,AuthController
需要一個LoginController
實(shí)例來與登錄form互動,所以它變成了一個數(shù)據(jù)屬性成員。
另外一件要提的事情是,我們選擇一些 合成 散布在繼承的頂端。AuthController
需要知道LoginController
,所以我們初始化它(new LoginController()
),使它一個成為this.login
的類屬性成員來引用它,這樣AuthController
才可以調(diào)用LoginController
上的行為。
注意: 這里可能會存在一絲沖動,就是使AuthController
繼承LoginController
,或者反過來,這樣的話我們就會通過繼承鏈得到 虛擬合成。但是這是一個非常清晰地例子,表明對這個問題來講,將類繼承作為模型有什么問題,因?yàn)?code>AuthController和LoginController
都不特化對方的行為,所以它們之間的繼承沒有太大的意義,除非類是你唯一的設(shè)計(jì)模式。與此相反的是,我們在一些簡單的合成中分層,然后它們就可以合作了,同時他倆都享有繼承自父類Controller
的好處。
如果你熟悉面向類(OO)的設(shè)計(jì),這都聽該看起來十分熟悉和自然。
去類化
但是,我們真的需要用一個父類,兩個子類,和一些合成來對這個問題建立模型嗎?有辦法利用OLOO風(fēng)格的行為委托得到 簡單得多 的設(shè)計(jì)嗎?有的!
var LoginController = {
errors: [],
getUser: function() {
return document.getElementById( "login_username" ).value;
},
getPassword: function() {
return document.getElementById( "login_password" ).value;
},
validateEntry: function(user,pw) {
user = user || this.getUser();
pw = pw || this.getPassword();
if (!(user && pw)) {
return this.failure( "Please enter a username & password!" );
}
else if (pw.length < 5) {
return this.failure( "Password must be 5+ characters!" );
}
// 到這里了?輸入合法!
return true;
},
showDialog: function(title,msg) {
// 在對話框中向用于展示成功消息
},
failure: function(err) {
this.errors.push( err );
this.showDialog( "Error", "Login invalid: " + err );
}
};
// 鏈接`AuthController`委托到`LoginController`
var AuthController = Object.create( LoginController );
AuthController.errors = [];
AuthController.checkAuth = function() {
var user = this.getUser();
var pw = this.getPassword();
if (this.validateEntry( user, pw )) {
this.server( "/check-auth",{
user: user,
pw: pw
} )
.then( this.accepted.bind( this ) )
.fail( this.rejected.bind( this ) );
}
};
AuthController.server = function(url,data) {
return $.ajax( {
url: url,
data: data
} );
};
AuthController.accepted = function() {
this.showDialog( "Success", "Authenticated!" )
};
AuthController.rejected = function(err) {
this.failure( "Auth Failed: " + err );
};
因?yàn)?code>AuthController只是一個對象(LoginController
也是),我們不需要初始化(比如new AuthController()
)就能執(zhí)行我們的任務(wù)。所有我們要做的是:
AuthController.checkAuth();
當(dāng)然,通過OLOO,如果你確實(shí)需要在委托鏈上創(chuàng)建一個或多個附加的對象時也很容易,而且仍然不需要任何像類實(shí)例化那樣的東西:
var controller1 = Object.create( AuthController );
var controller2 = Object.create( AuthController );
使用行為委托,AuthController
和LoginController
僅僅是對象,互相是 水平 對等的,而且沒有被安排或關(guān)聯(lián)成面向類中的父與子。我們有些隨意地選擇讓AuthController
委托至LoginController
—— 相反方向的委托也同樣是有效的。
第二個代碼段的主要要點(diǎn)是,我們只擁有兩個實(shí)體(LoginController
and AuthController
),而 不是之前的三個。
我們不需要一個基本的Controller
類來在兩個子類間“分享”行為,因?yàn)槲惺且环N可以給我們所需功能的,足夠強(qiáng)大的機(jī)制。同時,就像之前注意的,我們也不需要實(shí)例化我們的對象來使它們工作,因?yàn)檫@里沒有類,只有對象自身。 另外,這里不需要 合成 作為委托來給兩個對象 差異化 地合作的能力。
最后,由于沒有讓名稱success(..)
和failure(..)
在兩個對象上相同,我們避開了面向類的設(shè)計(jì)的多態(tài)陷阱:它將會需要難看的顯式假想多態(tài)。相反,我們在AuthController
上稱它們?yōu)?code>accepted()和rejected(..)
—— 對于他們的具體任務(wù)來說,稍稍更具描述性的名稱。
底線: 我們最終得到了相同的結(jié)果,但是用了(顯著的)更簡單的設(shè)計(jì)。這就是OLOO風(fēng)格代碼和 行為委托 設(shè)計(jì)模式的力量。
更好的語法
一個使ES6class
看似如此誘人的更好的東西是(見附錄A來了解為什么要避免它!),聲明類方法的速記語法:
class Foo {
methodName() { /* .. */ }
}
我們從聲明中扔掉了單詞function
,這使所有的JS開發(fā)者歡呼!
你可能已經(jīng)注意到,而且為此感到沮喪:上面推薦的OLOO語法出現(xiàn)了許多function
,這看起來像對OLOO簡化目標(biāo)的詆毀。但它不必是!
在ES6中,我們可以在任何字面對象中使用 簡約方法聲明,所以一個OLOO風(fēng)格的對象可以用這種方式聲明(與class
語法中相同的語法糖):
var LoginController = {
errors: [],
getUser() { // 看,沒有`function`!
// ...
},
getPassword() {
// ...
}
// ...
};
唯一的區(qū)別是字面對象的元素間依然需要,
逗號分隔符,而class
語法不必如此。這是在整件事情上很小的讓步。
還有,在ES6中,一個你使用的更笨重的語法(比如AuthController
的定義中):你一個一個地給屬性賦值而不使用字面對象,可以改寫為使用字面對象(于是你可以使用簡約方法),而且你可以使用Object.setPrototypeOf(..)
來修改對象的[[Prototype]]
,像這樣:
// 使用更好的字面對象語法 w/ 簡約方法!
var AuthController = {
errors: [],
checkAuth() {
// ...
},
server(url,data) {
// ...
}
// ...
};
// 現(xiàn)在, 鏈接`AuthController`委托至`LoginController`
Object.setPrototypeOf( AuthController, LoginController );
ES6中的OLOO風(fēng)格,與簡明方法一起,變得比它以前 友好得多(即使在以前,它也比經(jīng)典的原型風(fēng)格代碼簡單好看的多)。 你不必非得選用類(復(fù)雜性)來得到干凈漂亮的對象語法!
沒有詞法
簡約方法確實(shí)有一個缺點(diǎn),一個重要的細(xì)節(jié)。考慮這段代碼:
var Foo = {
bar() { /*..*/ },
baz: function baz() { /*..*/ }
};
這是去掉語法糖后,這段代碼將如何工作:
var Foo = {
bar: function() { /*..*/ },
baz: function baz() { /*..*/ }
};
看到區(qū)別了?bar()
的速記法變成了一個附著在bar
屬性上的 匿名函數(shù)表達(dá)式(function()..
),因?yàn)楹瘮?shù)對象本身沒有名稱標(biāo)識符。和擁有詞法名稱標(biāo)識符baz
,附著在.baz
屬性上的手動指定的 命名函數(shù)表達(dá)式(function baz()..
)做個比較。
那又怎么樣?在 “你不懂JS” 系列的 “作用域與閉包” 這本書中,我們詳細(xì)講解了 匿名函數(shù)表達(dá)式 的三個主要缺點(diǎn)。我們簡單地重復(fù)一下它們,以便于我們和簡明方法相比較。
一個匿名函數(shù)缺少name
標(biāo)識符:
- 使調(diào)試時的棧追蹤變得困難
- 使自引用(遞歸,事件綁定等)變得困難
- 使代碼(稍稍)變得難于理解
第一和第三條不適用于簡明方法。
雖然去掉語法糖使用 匿名函數(shù)表達(dá)式 一般會使棧追蹤中沒有name
。簡明方法在語言規(guī)范中被要求去設(shè)置相應(yīng)的函數(shù)對象內(nèi)部的name
屬性,所以棧追蹤應(yīng)當(dāng)可以使用它(這是依賴于具體實(shí)現(xiàn)的,所以不能保證)。
不幸的是,第二條 仍然是簡明方法的一個缺陷。 它們不會有詞法標(biāo)識符用來自引用。考慮:
var Foo = {
bar: function(x) {
if (x < 10) {
return Foo.bar( x * 2 );
}
return x;
},
baz: function baz(x) {
if (x < 10) {
return baz( x * 2 );
}
return x;
}
};
在這個例子中上面的手動Foo.bar(x*2)
引用就足夠了,但是在許多情況下,一個函數(shù)沒必要能夠這樣做,比如使用this
綁定,函數(shù)在委托中被分享到不同的對象,等等。你將會想要使用一個真正的自引用,而函數(shù)對象的name
標(biāo)識符是實(shí)現(xiàn)的最佳方式。
只要小心簡明方法的這個注意點(diǎn),而且如果當(dāng)你陷入缺少自引用的問題時,僅僅為這個聲明 放棄簡明方法語法,取代以手動的 命名函數(shù)表達(dá)式 聲明形式:baz: function baz(){..}
。
自省
如果你花了很長時間在面向類的編程方式(不管是JS還是其他的語言),你可能會對 類型自省 很熟悉:自省一個實(shí)例來找出它是什么 種類 的對象。在類的實(shí)例上進(jìn)行 類型自省 的主要目的是根據(jù) 對象是如何創(chuàng)建的 來推斷它的結(jié)構(gòu)/能力。
考慮這段代碼,它使用instanceof
(見第五章)來自省一個對象a1
來推斷它的能力:
function Foo() {
// ...
}
Foo.prototype.something = function(){
// ...
}
var a1 = new Foo();
// 稍后
if (a1 instanceof Foo) {
a1.something();
}
因?yàn)?code>Foo.prototype(不是Foo
!)在a1
的[[Prototype]]
鏈上(見第五章),instanceof
操作符(使人困惑地)假裝告訴我們a1
是一個Foo
“類”的實(shí)例。有了這個知識,我們假定a1
有Foo
“類”中描述的能力。
當(dāng)然,這里沒有Foo
類,只有一個普通的函數(shù)Foo
,它恰好擁有一個引用指向一個隨意的對象(Foo.prototype
),而a1
恰好委托鏈接至這個對象。通過它的語法,instanceof
假裝檢查了a1
和Foo
之間的關(guān)系,但它實(shí)際上告訴我們的是a1
和Foo.prototype
(這個隨意被引用的對象)是否有關(guān)聯(lián)。
instanceof
在語義上的混亂(和間接)意味著,要使用以instanceof
為基礎(chǔ)的自省來查詢對象a1
是否與討論中的對象有關(guān)聯(lián),你 不得不 擁有一個持有對這個對象引用的函數(shù) —— 你不能直接查詢這兩個對象是否有關(guān)聯(lián)。
回想本章前面的抽象Foo
/ Bar
/ b1
例子,我們在這里縮寫一下:
function Foo() { /* .. */ }
Foo.prototype...
function Bar() { /* .. */ }
Bar.prototype = Object.create( Foo.prototype );
var b1 = new Bar( "b1" );
為了在這個例子中的實(shí)體上進(jìn)行 類型自省, 使用instanceof
和.prototype
語義,這里有各種你可能需要實(shí)施的檢查:
// 的`Foo`和`Bar`互相聯(lián)系
Bar.prototype instanceof Foo; // true
Object.getPrototypeOf( Bar.prototype ) === Foo.prototype; // true
Foo.prototype.isPrototypeOf( Bar.prototype ); // true
// `b1`與`Foo`和`Bar`的聯(lián)系
b1 instanceof Foo; // true
b1 instanceof Bar; // true
Object.getPrototypeOf( b1 ) === Bar.prototype; // true
Foo.prototype.isPrototypeOf( b1 ); // true
Bar.prototype.isPrototypeOf( b1 ); // true
可以說,其中有些爛透了。舉個例子,直覺上(用類)你可能想說這樣的東西Bar instanceof Foo
(因?yàn)楹苋菀谆煜皩?shí)例”的意義認(rèn)為它包含“繼承”),但在JS中這不是一個合理的比較。你不得不說Bar.prototype instanceof Foo
。
另一個常見,但也許健壯性更差的 類型自省 模式叫“duck typing(鴨子類型)”,比起instanceof
來許多開發(fā)者都傾向于它。這個術(shù)語源自一則諺語,“如果它看起來像鴨子,叫起來像鴨子,那么它一定是一只鴨子”。
例如:
if (a1.something) {
a1.something();
}
與其檢查a1
和一個持有可委托的something()
函數(shù)的對象的關(guān)系,我們假設(shè)a1.something
測試通過意味著a1
有能力調(diào)用.something()
(不管是直接在a1
上直接找到方法,還是委托至其他對象)。就其本身而言,這種假設(shè)沒什么風(fēng)險。
但是“鴨子類型”常常被擴(kuò)展用于 除了被測試關(guān)于對象能力以外的其他假設(shè),這當(dāng)然會在測試中引入更多風(fēng)險(比如脆弱的設(shè)計(jì))。
“鴨子類型”的一個值得注意的例子來自于ES6的Promises(就是我們前面解釋過,將不再本書內(nèi)涵蓋的內(nèi)容)。
由于種種原因,需要判定任意一個對象引用是否 是一個Promise,但測試是通過檢查對象是否恰好有then()
函數(shù)出現(xiàn)在它上面來完成的。換句話說,如果任何對象 恰好有一個then()
方法,ES6的Promises將會無條件地假設(shè)這個對象 是“thenable” 的,而且因此會期望它按照所有的Promises標(biāo)準(zhǔn)行為那樣一致地動作。
如果你有任何非Promise對象,而卻不管因?yàn)槭裁此『脫碛?code>then()方法,你會被強(qiáng)烈建議使它遠(yuǎn)離ES6的Promise機(jī)制,來避免破壞這種假設(shè)。
這個例子清楚地展現(xiàn)了“鴨子類型”的風(fēng)險。你應(yīng)當(dāng)僅在可控的條件下,保守地使用這種方式。
再次將我們的注意力轉(zhuǎn)向本章中出現(xiàn)的OLOO風(fēng)格的代碼,類型自省 變得清晰多了。讓我們回想(并縮寫)本章的Foo
/ Bar
/ b1
的OLOO示例:
var Foo = { /* .. */ };
var Bar = Object.create( Foo );
Bar...
var b1 = Object.create( Bar );
使用這種OLOO方式,我們所擁有的一切都是通過[[Prototype]]
委托關(guān)聯(lián)起來的普通對象,這是我們可能會用到的大幅簡化后的 類型自省:
// `Foo`和`Bar`互相的聯(lián)系
Foo.isPrototypeOf( Bar ); // true
Object.getPrototypeOf( Bar ) === Foo; // true
// `b1`與`Foo`和`Bar`的聯(lián)系
Foo.isPrototypeOf( b1 ); // true
Bar.isPrototypeOf( b1 ); // true
Object.getPrototypeOf( b1 ) === Bar; // true
我們不再使用instanceof
,因?yàn)樗钊嗣曰蟮丶傺b與類有關(guān)系。現(xiàn)在,我們只需要(非正式地)問這個問題,“你是我的 一個 原型嗎?”。不再需要用Foo.prototype
或者痛苦冗長的Foo.prototype.isPrototypeOf(..)
來間接地查詢了。
我想可以說這些檢查比起前面一組自省檢查,極大地減少了復(fù)雜性/混亂。又一次,我們看到了在JavaScript中OLOO要比類風(fēng)格的編碼簡單(但有著相同的力量)。
復(fù)習(xí)
在你的軟件體系結(jié)構(gòu)中,類和繼承是你可以 選用 或 不選用 的設(shè)計(jì)模式。多數(shù)開發(fā)者理所當(dāng)然地認(rèn)為類是組織代碼的唯一(正確的)方法,但我們在這里看到了另一種不太常被提到的,但實(shí)際上十分強(qiáng)大的設(shè)計(jì)模式:行為委托。
行為委托意味著對象彼此是對等的,在它們自己當(dāng)中相互委托,而不是父類與子類的關(guān)系。JavaScript的[[Prototype]]
機(jī)制的設(shè)計(jì)本質(zhì),就是行為委托機(jī)制。這意味著我們可以選擇掙扎著在JS上實(shí)現(xiàn)類機(jī)制,也可以欣然接受[[Prototype]]
作為委托機(jī)制的本性。
當(dāng)你僅用對象設(shè)計(jì)代碼時,它不僅能簡化你使用的語法,而且它還能實(shí)際上引領(lǐng)更簡單的代碼結(jié)構(gòu)設(shè)計(jì)。
OLOO(鏈接到其他對象的對像)是一種沒有類的抽象,而直接創(chuàng)建和關(guān)聯(lián)對象的代碼風(fēng)格。OLOO十分自然地實(shí)現(xiàn)了基于[[Prototype]]
的行為委托。