當(dāng)今 JavaScript 大行其道,各種應(yīng)用對其依賴日深。web 程序員已逐漸習(xí)慣使用各種優(yōu)秀的 JavaScript 框架快速開發(fā) Web 應(yīng)用,從而忽略了對原生 JavaScript 的學(xué)習(xí)和深入理解。所以,經(jīng)常出現(xiàn)的情況是,很多做了多年 JS 開發(fā)的程序員對閉包、函數(shù)式編程、原型總是說不清道不明,即使使用了框架,其代碼組織也非常糟糕。這都是對原生 JavaScript 語言特性理解不夠的表現(xiàn)。要掌握好 JavaScript,首先一點(diǎn)是必須摒棄一些其他高級語言如 Java、C# 等類式面向?qū)ο笏季S的干擾,全面地從函數(shù)式語言的角度理解 JavaScript 原型式面向?qū)ο蟮奶攸c(diǎn)。把握好這一點(diǎn)之后,才有可能進(jìn)一步使用好這門語言。本文適合群體:使用過 JS 框架但對 JS 語言本質(zhì)缺乏理解的程序員,具有 Java、C++ 等語言開發(fā)經(jīng)驗(yàn),準(zhǔn)備學(xué)習(xí)并使用 JavaScript 的程序員,以及一直對 JavaScript 是否面向?qū)ο竽@鈨煽桑M勒嫦嗟?JS 愛好者。
重新認(rèn)識面向?qū)ο?/STRONG>
為了說明 JavaScript 是一門徹底的面向?qū)ο蟮恼Z言,首先有必要從面向?qū)ο蟮母拍钪?, 探討一下面向?qū)ο笾械膸讉€概念:
一切事物皆對象
對象具有封裝和繼承特性
對象與對象之間使用消息通信,各自存在信息隱藏
以這三點(diǎn)做為依據(jù),C++ 是半面向?qū)ο蟀朊嫦蜻^程語言,因?yàn)椋m然他實(shí)現(xiàn)了類的封裝、繼承和多態(tài),但存在非對象性質(zhì)的全局函數(shù)和變量。Java、C# 是完全的面向?qū)ο笳Z言,它們通過類的形式組織函數(shù)和變量,使之不能脫離對象存在。但這里函數(shù)本身是一個過程,只是依附在某個類上。
然而,面向?qū)ο髢H僅是一個概念或者編程思想而已,它不應(yīng)該依賴于某個語言存在。比如 Java 采用面向?qū)ο笏枷霕?gòu)造其語言,它實(shí)現(xiàn)了類、繼承、派生、多態(tài)、接口等機(jī)制。但是這些機(jī)制,只是實(shí)現(xiàn)面向?qū)ο缶幊痰囊环N手段,而非必須。換言之,一門語言可以根據(jù)其自身特性選擇合適的方式來實(shí)現(xiàn)面向?qū)ο蟆K裕捎诖蠖鄶?shù)程序員首先學(xué)習(xí)或者使用的是類似 Java、C++ 等高級編譯型語言(Java 雖然是半編譯半解釋,但一般做為編譯型來講解),因而先入為主地接受了“類”這個面向?qū)ο髮?shí)現(xiàn)方式,從而在學(xué)習(xí)腳本語言的時候,習(xí)慣性地用類式面向?qū)ο笳Z言中的概念來判斷該語言是否是面向?qū)ο笳Z言,或者是否具備面向?qū)ο筇匦浴_@也是阻礙程序員深入學(xué)習(xí)并掌握 JavaScript 的重要原因之一。
實(shí)際上,JavaScript 語言是通過一種叫做 原型(prototype)的方式來實(shí)現(xiàn)面向?qū)ο缶幊痰摹O旅婢蛠碛懻?基于類的(class-based)面向?qū)ο蠛?基于原型的 (prototype-based) 面向?qū)ο筮@兩種方式在構(gòu)造客觀世界的方式上的差別。
基于類的面向?qū)ο蠛突谠偷拿嫦驅(qū)ο蠓绞奖容^
在基于類的面向?qū)ο蠓绞街校?STRONG>對象(object)依靠 類(class)來產(chǎn)生。而在基于原型的面向?qū)ο蠓绞街校?STRONG>對象(object)則是依靠 構(gòu)造器(constructor)利用 原型(prototype)構(gòu)造出來的。舉個客觀世界的例子來說明二種方式認(rèn)知的差異。例如工廠造一輛車,一方面,工人必須參照一張工程圖紙,設(shè)計規(guī)定這輛車應(yīng)該如何制造。這里的工程圖紙就好比是語言中的 類 (class),而車就是按照這個 類(class)制造出來的;另一方面,工人和機(jī)器 ( 相當(dāng)于 constructor) 利用各種零部件如發(fā)動機(jī),輪胎,方向盤 ( 相當(dāng)于 prototype 的各個屬性 ) 將汽車構(gòu)造出來。
事實(shí)上關(guān)于這兩種方式誰更為徹底地表達(dá)了面向?qū)ο蟮乃枷耄壳吧杏袪幷摗5P者認(rèn)為原型式面向?qū)ο笫且环N更為徹底的面向?qū)ο蠓绞剑碛扇缦拢?/P>
首先,客觀世界中的對象的產(chǎn)生都是其它實(shí)物對象構(gòu)造的結(jié)果,而抽象的“圖紙”是不能產(chǎn)生“汽車”的,也就是說,類是一個抽象概念而并非實(shí)體,而對象的產(chǎn)生是一個實(shí)體的產(chǎn)生;
其次,按照一切事物皆對象這個最基本的面向?qū)ο蟮姆▌t來看,類 (class) 本身并不是一個對象,然而原型方式中的構(gòu)造器 (constructor) 和原型 (prototype) 本身也是其他對象通過原型方式構(gòu)造出來的對象。
再次,在類式面向?qū)ο笳Z言中,對象的狀態(tài) (state) 由對象實(shí)例 (instance) 所持有,對象的行為方法 (method) 則由聲明該對象的類所持有,并且只有對象的結(jié)構(gòu)和方法能夠被繼承;而在原型式面向?qū)ο笳Z言中,對象的行為、狀態(tài)都屬于對象本身,并且能夠一起被繼承(參考資源),這也更貼近客觀實(shí)際。
最后,類式面向?qū)ο笳Z言比如 Java,為了彌補(bǔ)無法使用面向過程語言中全局函數(shù)和變量的不便,允許在類中聲明靜態(tài) (static) 屬性和靜態(tài)方法。而實(shí)際上,客觀世界不存在所謂靜態(tài)概念,因?yàn)橐磺惺挛锝詫ο螅《谠褪矫嫦驅(qū)ο笳Z言中,除內(nèi)建對象 (build-in object) 外,不允許全局對象、方法或者屬性的存在,也沒有靜態(tài)概念。所有語言元素 (primitive) 必須依賴對象存在。但由于函數(shù)式語言的特點(diǎn),語言元素所依賴的對象是隨著運(yùn)行時 (runtime) 上下文 (context) 變化而變化的,具體體現(xiàn)在 this 指針的變化。正是這種特點(diǎn)更貼近 “萬物皆有所屬,宇宙乃萬物生存之根本”的自然觀點(diǎn)。在 程序清單 1中 window 便類似與宇宙的概念。
清單 1. 對象的上下文依賴
obj.fun = fun;
console.log( this === window ); // 打印 true
console.log( window.str === str ); // 打印 true
console.log( window.obj === obj ); // 打印 true
console.log( window.fun === fun ); // 打印 true
fun(); // 打印 我是一個 Function 對象!誰調(diào)用我,我屬于誰:window
obj.fun(); // 打印 我是一個 Function 對象!誰調(diào)用我,我屬于誰:obj
fun.apply(str); // 打印 我是一個 Function 對象!誰調(diào)用我,我屬于誰:str
</script>
在接受了面向?qū)ο蟠嬖谝环N叫做基于原型實(shí)現(xiàn)的方式的事實(shí)之后,下面我們就可以來深入探討 ECMAScript 是如何依據(jù)這一方式構(gòu)造自己的語言的。
最基本的面向?qū)ο?/STRONG>
ECMAScript 是一門徹底的面向?qū)ο蟮木幊陶Z言(參考資源),JavaScript 是其中的一個變種 (variant)。它提供了 6 種基本數(shù)據(jù)類型,即 Boolean、Number、String、Null、Undefined、Object。為了實(shí)現(xiàn)面向?qū)ο螅?EM>ECMAScript設(shè)計出了一種非常成功的數(shù)據(jù)結(jié)構(gòu) - JSON(JavaScript Object Notation), 這一經(jīng)典結(jié)構(gòu)已經(jīng)可以脫離語言而成為一種廣泛應(yīng)用的數(shù)據(jù)交互格式 (參考資源)。
應(yīng)該說,具有基本數(shù)據(jù)類型和 JSON 構(gòu)造語法的 ECMAScript 已經(jīng)基本可以實(shí)現(xiàn)面向?qū)ο蟮木幊塘恕i_發(fā)者可以隨意地用 字面式聲明(literal notation)方式來構(gòu)造一個對象,并對其不存在的屬性直接賦值,或者用 delete 將屬性刪除 ( 注:JS 中的 delete 關(guān)鍵字用于刪除對象屬性,經(jīng)常被誤作為 C++ 中的 delete,而后者是用于釋放不再使用的對象 ),如 程序清單 2。
清單 2. 字面式 (literal notation) 對象聲明
在實(shí)際開發(fā)過程中,大部分初學(xué)者或者對 JS 應(yīng)用沒有太高要求的開發(fā)者也基本上只用到 ECMAScript 定義的這一部分內(nèi)容,就能滿足基本的開發(fā)需求。然而,這樣的代碼復(fù)用性非常弱,與其他實(shí)現(xiàn)了繼承、派生、多態(tài)等等的類式面向?qū)ο蟮膹?qiáng)類型語言比較起來顯得有些干癟,不能滿足復(fù)雜的 JS 應(yīng)用開發(fā)。所以 ECMAScript 引入原型來解決對象繼承問題。
除了 字面式聲明(literal notation)方式之外,ECMAScript 允許通過 構(gòu)造器(constructor)創(chuàng)建對象。每個構(gòu)造器實(shí)際上是一個 函數(shù)(function) 對象, 該函數(shù)對象含有一個“prototype”屬性用于實(shí)現(xiàn) 基于原型的繼承(prototype-based inheritance)和 共享屬性(shared properties)。對象可以由“new 關(guān)鍵字 + 構(gòu)造器調(diào)用”的方式來創(chuàng)建,如 程序清單 3:
清單 3. 使用構(gòu)造器 (constructor) 創(chuàng)建對象
由于早期 JavaScript 的發(fā)明者為了使這門語言與大名鼎鼎的 Java 拉上關(guān)系 ( 雖然現(xiàn)在大家知道二者是雷鋒和雷鋒塔的關(guān)系 ),使用了new 關(guān)鍵字來限定構(gòu)造器調(diào)用并創(chuàng)建對象,以使其在語法上跟 Java 創(chuàng)建對象的方式看上去類似。但需要指出的是,這兩門語言的new含義毫無關(guān)系,因?yàn)槠鋵ο髽?gòu)造的機(jī)理完全不同。也正是因?yàn)檫@里語法上的類似,眾多習(xí)慣了類式面向?qū)ο笳Z言中對象創(chuàng)建方式的程序員,難以透徹理解 JS 對象原型構(gòu)造的方式,因?yàn)樗麄兛偸遣幻靼自?JS 語言中,為什么“函數(shù)名可以作為類名”的現(xiàn)象。而實(shí)質(zhì)上,JS 這里僅僅是借用了關(guān)鍵字 new,僅此而已;換句話說,ECMAScript 完全可以用其它 非 new 表達(dá)式來用調(diào)用構(gòu)造器創(chuàng)建對象。
在 ECMAScript 中,每個由構(gòu)造器創(chuàng)建的對象擁有一個指向構(gòu)造器 prototype 屬性值的 隱式引用(implicit reference),這個引用稱之為 原型(prototype)。進(jìn)一步,每個原型可以擁有指向自己原型的 隱式引用(即該原型的原型),如此下去,這就是所謂的原型鏈(prototype chain) (參考資源)。在具體的語言實(shí)現(xiàn)中,每個對象都有一個 __proto__ 屬性來實(shí)現(xiàn)對原型的 隱式引用。程序清單 4說明了這一點(diǎn)。
清單 4. 對象的 __proto__ 屬性和隱式引用
// 原型本身是一個 Object 對象,所以他的隱式引用指向了
// Object 構(gòu)造器的 prototype 屬性 , 故而打印 true
console.log( Person.prototype.__proto__ === Object.prototype );
// 構(gòu)造器 Person 本身是一個函數(shù)對象,所以此處打印 true
console.log( Person.__proto__ === Function.prototype );
有了 原型鏈,便可以定義一種所謂的 屬性隱藏機(jī)制,并通過這種機(jī)制實(shí)現(xiàn)繼承。ECMAScript 規(guī)定,當(dāng)要給某個對象的屬性賦值時,解釋器會查找該對象原型鏈中第一個含有該屬性的對象(注:原型本身就是一個對象,那么原型鏈即為一組對象的鏈。對象的原型鏈中的第一個對象是該對象本身)進(jìn)行賦值。反之,如果要獲取某個對象屬性的值,解釋器自然是返回該對象原型鏈中首先具有該屬性的對象屬性值。圖 1說名了這中隱藏機(jī)制:
在圖 1 中,object1->prototype1->prototype2 構(gòu)成了 對象 object1 的原型鏈,根據(jù)上述屬性隱藏機(jī)制,可以清楚地看到 prototype1 對象中的 property4 屬性和 prototype2 對象中的 property3 屬性皆被隱藏。理解了原型鏈,那么將非常容易理解 JS 中基于原型的繼承實(shí)現(xiàn)原理,程序清單 5 是利用原型鏈實(shí)現(xiàn)繼承的簡單例子。
清單 5. 利用原型鏈 Horse->Mammal->Animal 實(shí)現(xiàn)繼承
// 聲明 Horse 對象構(gòu)造器
function Horse( height, weight ) {
this.name = "horse";
this.height = height;
this.weight = weight;
}
// 將 Horse 對象的原型指定為一個 Mamal 對象,繼續(xù)構(gòu)建 Horse 與 Mammal 之間的原型鏈
Horse.prototype = new Mammal();
// 重新指定 eat 方法 , 此方法將覆蓋從 Animal 原型繼承過來的 eat 方法
Horse.prototype.eat = function() {
alert( "Horse is eating grass!" );
}
// 驗(yàn)證并理解原型鏈
var horse = new Horse( 100, 300 );
console.log( horse.__proto__ === Horse.prototype );
console.log( Horse.prototype.__proto__ === Mammal.prototype );
console.log( Mammal.prototype.__proto__ === Animal.prototype );
理解清單 5 中對象原型繼承邏輯實(shí)現(xiàn)的關(guān)鍵在于 Horse.prototype = new Mammal() 和 Mammal.prototype = new Animal() 這兩句代碼。首先,等式右邊的結(jié)果是構(gòu)造出一個臨時對象,然后將這個對象賦值給等式左邊對象的 prototype 屬性。也就是說將右邊新建的對象作為左邊對象的原型。讀者可以將這兩個等式替換到相應(yīng)的程序清單 5 代碼最后兩行的等式中自行領(lǐng)悟。
JavaScript 類式繼承的實(shí)現(xiàn)方法
從代碼清單 5 可以看出,基于原型的繼承方式,雖然實(shí)現(xiàn)了代碼復(fù)用,但其行文松散且不夠流暢,可閱讀性差,不利于實(shí)現(xiàn)擴(kuò)展和對源代碼進(jìn)行有效地組織管理。不得不承認(rèn),類式繼承方式在語言實(shí)現(xiàn)上更具健壯性,且在構(gòu)建可復(fù)用代碼和組織架構(gòu)程序方面具有明顯的優(yōu)勢。這使得程序員們希望尋找到一種能夠在 JavaScript 中以類式繼承風(fēng)格進(jìn)行編碼的方法途徑。從抽象的角度來講,既然類式繼承和原型繼承都是為實(shí)現(xiàn)面向?qū)ο蠖O(shè)計的,并且他們各自實(shí)現(xiàn)的載體語言在計算能力上是等價的 ( 因?yàn)閳D靈機(jī)的計算能力與 Lambda 演算的計算能力是等價的 ),那么能不能找到一種變換,使得原型式繼承語言通過該變換實(shí)現(xiàn)具有類式繼承編碼的風(fēng)格呢?
目前一些主流的 JS 框架都提供了這種轉(zhuǎn)換機(jī)制,也即類式聲明方法,比如 Dojo.declare()、Ext.entend() 等等。用戶使用這些框架,可以輕易而友好地組織自己的 JS 代碼。其實(shí),在眾多框架出現(xiàn)之前,JavaScript 大師 Douglas Crockford 最早利用三個函數(shù)對 Function 對象進(jìn)行擴(kuò)展,實(shí)現(xiàn)了這種變換,關(guān)于它的實(shí)現(xiàn)細(xì)節(jié)可以(參考資源)。此外還有由 Dean Edwards實(shí)現(xiàn)的著名的 Base.js(參考資源)。值得一提的是,jQuery 之父 John Resig 在搏眾家之長之后,用不到 30 行代碼便實(shí)現(xiàn)了自己的 Simple Inheritance。使用其提供的 extend 方法聲明類非常簡單。程序清單 6是使用了 Simple Inheritance庫實(shí)現(xiàn)類的聲明的例子。其中最后一句打印輸出語句是對 Simple Inheritance實(shí)現(xiàn)類式繼承的最好說明。
清單 6. 使用 Simple Inheritance 實(shí)現(xiàn)類式繼承
如果您已對原型、函數(shù)構(gòu)造器、閉包和基于上下文的 this 有了充分的理解,那么理解 Simple Inheritance 的實(shí)現(xiàn)原理也并非相當(dāng)困難。從本質(zhì)上講,var Person = Class.extend(...)該語句中,左邊的 Person 實(shí)際上是獲得了由 Class 調(diào)用 extend 方法返回的一個構(gòu)造器,也即一個 function 對象的引用。順著這個思路,我們繼續(xù)介紹 Simple Inheritance 是如何做到這一點(diǎn),進(jìn)而實(shí)現(xiàn)了由原型繼承方式到類式繼承方式的轉(zhuǎn)換的。圖 2 是 Simple Inheritance 的源碼及其附帶注釋。為了方便理解,用中文對代碼逐行補(bǔ)充說明。
圖 2.Simple Inheritance 源碼解析
拋開代碼第二部分,整體連貫地考察第一和第三部分會發(fā)現(xiàn),extend 函數(shù)的根本目的就是要構(gòu)造一個具有新原型屬性的新構(gòu)造器。我們不禁感嘆 John Resig的大師手筆及其對 JS 語言本質(zhì)把握的細(xì)膩程度。至于 John Resig是如何想到這樣精妙的實(shí)現(xiàn)方法,感興趣的讀者可以閱讀本文 (參考資源),其中有詳細(xì)介紹關(guān)于最初設(shè)計 Simple Inheritance 的思維過程。
到此為止,如果您任然對 JavaScript 面向?qū)ο蟪謶岩蓱B(tài)度,那么這個懷疑一定是,JavaScript 沒有實(shí)現(xiàn)面向?qū)ο笾械男畔㈦[藏,即私有和公有。與其他類式面向?qū)ο竽菢语@式地聲明私有公有成員的方式不同,JavaScript 的信息隱藏就是靠閉包實(shí)現(xiàn)的。見 程序清單 7:
清單 7. 使用閉包實(shí)現(xiàn)信息隱藏
JavaScript 必須依賴閉包實(shí)現(xiàn)信息隱藏,是由其函數(shù)式語言特性所決定的。本文不會對函數(shù)式語言和閉包這兩個話題展開討論,正如上文默認(rèn)您理解 JavaScript 中基于上下文的 this 一樣。關(guān)于 JavaScript 中實(shí)現(xiàn)信息隱藏,Douglas Crockford在《 Private members in JavaScript 》(參考資源)一文中有更權(quán)威和詳細(xì)的介紹。
JavaScript 被認(rèn)為是世界上最受誤解的編程語言,因?yàn)樗砼?c 語言家族的外衣,表現(xiàn)的卻是 LISP 風(fēng)格的函數(shù)式語言特性;沒有類,卻實(shí)也徹底實(shí)現(xiàn)了面向?qū)ο蟆R獙@門語言有透徹的理解,就必須扒開其 c 語言的外衣,從新回到函數(shù)式編程的角度,同時摒棄原有類的面向?qū)ο蟾拍钊W(xué)習(xí)領(lǐng)悟它。隨著近些年來 Web 應(yīng)用的普及和 JS 語言自身的長足發(fā)展,特別是后臺 JS 引擎的出現(xiàn) ( 如基于 V8 的 NodeJS 等 ),可以預(yù)見,原來只是作為玩具編寫頁面效果的 JS 將獲得更廣闊發(fā)展天地。這樣的發(fā)展趨勢,也對 JS 程序員提出了更高要求。只有徹底領(lǐng)悟了這門語言,才有可能在大型的 JS 項(xiàng)目中發(fā)揮她的威力。
新聞熱點(diǎn)
疑難解答