全面理解面向?qū)ο蟮?JavaScript(來自ibm)
當今 JavaScript 大行其道,各種應用對其依賴日深。web 程序員已逐漸習慣使用各種優(yōu)秀的 JavaScript 框架快速開發(fā) Web 應用,從而忽略了對原生 JavaScript 的學習和深入理解。所以,經(jīng)常出現(xiàn)的情況是,很多做了多年 JS 開發(fā)的程序員對閉包、函數(shù)式編程、原型總是說不清道不明,即使使用了框架,其代碼組織也非常糟糕。這都是對原生 JavaScript 語言特性理解不夠的表現(xiàn)。要掌握好 JavaScript,首先一點是必須摒棄一些其他高級語言如 Java、C# 等類式面向?qū)ο笏季S的干擾,全面地從函數(shù)式語言的角度理解 JavaScript 原型式面向?qū)ο蟮奶攸c。把握好這一點之后,才有可能進一步使用好這門語言。本文適合群體:使用過 JS 框架但對 JS 語言本質(zhì)缺乏理解的程序員,具有 Java、C++ 等語言開發(fā)經(jīng)驗,準備學習并使用 JavaScript 的程序員,以及一直對 JavaScript 是否面向?qū)ο竽@鈨煽?,但希望知道JS愛好者。
重新認識面向?qū)ο?/strong>
為了說明 JavaScript 是一門徹底的面向?qū)ο蟮恼Z言,首先有必要從面向?qū)ο蟮母拍钪?, 探討一下面向?qū)ο笾械膸讉€概念:
一切事物皆對象
對象具有封裝和繼承特性
對象與對象之間使用消息通信,各自存在信息隱藏
以這三點做為依據(jù),C++ 是半面向?qū)ο蟀朊嫦蜻^程語言,因為,雖然他實現(xiàn)了類的封裝、繼承和多態(tài),但存在非對象性質(zhì)的全局函數(shù)和變量。Java、C# 是完全的面向?qū)ο笳Z言,它們通過類的形式組織函數(shù)和變量,使之不能脫離對象存在。但這里函數(shù)本身是一個過程,只是依附在某個類上。
然而,面向?qū)ο髢H僅是一個概念或者編程思想而已,它不應該依賴于某個語言存在。比如 Java 采用面向?qū)ο笏枷霕嬙炱湔Z言,它實現(xiàn)了類、繼承、派生、多態(tài)、接口等機制。但是這些機制,只是實現(xiàn)面向?qū)ο缶幊痰囊环N手段,而非必須。換言之,一門語言可以根據(jù)其自身特性選擇合適的方式來實現(xiàn)面向?qū)ο?。所以,由于大多?shù)程序員首先學習或者使用的是類似 Java、C++ 等高級編譯型語言(Java 雖然是半編譯半解釋,但一般做為編譯型來講解),因而先入為主地接受了“類”這個面向?qū)ο髮崿F(xiàn)方式,從而在學習腳本語言的時候,習慣性地用類式面向?qū)ο笳Z言中的概念來判斷該語言是否是面向?qū)ο笳Z言,或者是否具備面向?qū)ο筇匦?。這也是阻礙程序員深入學習并掌握 JavaScript 的重要原因之一。
實際上,JavaScript 語言是通過一種叫做 原型(prototype)的方式來實現(xiàn)面向?qū)ο缶幊痰?。下面就來討?基于類的(class-based)面向?qū)ο蠛?基于原型的 (prototype-based) 面向?qū)ο筮@兩種方式在構造客觀世界的方式上的差別。
基于類的面向?qū)ο蠛突谠偷拿嫦驅(qū)ο蠓绞奖容^
事實上關于這兩種方式誰更為徹底地表達了面向?qū)ο蟮乃枷?,目前尚有爭論。但筆者認為原型式面向?qū)ο笫且环N更為徹底的面向?qū)ο蠓绞剑碛扇缦拢?/p>
首先,客觀世界中的對象的產(chǎn)生都是其它實物對象構造的結(jié)果,而抽象的“圖紙”是不能產(chǎn)生“汽車”的,也就是說,類是一個抽象概念而并非實體,而對象的產(chǎn)生是一個實體的產(chǎn)生;
其次,按照一切事物皆對象這個最基本的面向?qū)ο蟮姆▌t來看,類 (class) 本身并不是一個對象,然而原型方式中的構造器 (constructor) 和原型 (prototype) 本身也是其他對象通過原型方式構造出來的對象。
再次,在類式面向?qū)ο笳Z言中,對象的狀態(tài) (state) 由對象實例 (instance) 所持有,對象的行為方法 (method) 則由聲明該對象的類所持有,并且只有對象的結(jié)構和方法能夠被繼承;而在原型式面向?qū)ο笳Z言中,對象的行為、狀態(tài)都屬于對象本身,并且能夠一起被繼承,這也更貼近客觀實際。
最后,類式面向?qū)ο笳Z言比如 Java,為了彌補無法使用面向過程語言中全局函數(shù)和變量的不便,允許在類中聲明靜態(tài) (static) 屬性和靜態(tài)方法。而實際上,客觀世界不存在所謂靜態(tài)概念,因為一切事物皆對象!而在原型式面向?qū)ο笳Z言中,除內(nèi)建對象 (build-in object) 外,不允許全局對象、方法或者屬性的存在,也沒有靜態(tài)概念。所有語言元素 (primitive) 必須依賴對象存在。但由于函數(shù)式語言的特點,語言元素所依賴的對象是隨著運行時 (runtime) 上下文 (context) 變化而變化的,具體體現(xiàn)在 this 指針的變化。正是這種特點更貼近 “萬物皆有所屬,宇宙乃萬物生存之根本”的自然觀點。在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>
[/code]
在接受了面向?qū)ο蟠嬖谝环N叫做基于原型實現(xiàn)的方式的事實之后,下面我們就可以來深入探討 ECMAScript 是如何依據(jù)這一方式構造自己的語言的。
最基本的面向?qū)ο?/strong>
ECMAScript 是一門徹底的面向?qū)ο蟮木幊陶Z言(參考資源),JavaScript 是其中的一個變種 (variant)。它提供了 6 種基本數(shù)據(jù)類型,即 Boolean、Number、String、Null、Undefined、Object。為了實現(xiàn)面向?qū)ο?,ECMAScript設計出了一種非常成功的數(shù)據(jù)結(jié)構 - JSON(JavaScript Object Notation), 這一經(jīng)典結(jié)構已經(jīng)可以脫離語言而成為一種廣泛應用的數(shù)據(jù)交互格式 (參考資源)。
應該說,具有基本數(shù)據(jù)類型和 JSON 構造語法的 ECMAScript 已經(jīng)基本可以實現(xiàn)面向?qū)ο蟮木幊塘?。開發(fā)者可以隨意地用 字面式聲明(literal notation)方式來構造一個對象,并對其不存在的屬性直接賦值,或者用 delete 將屬性刪除 ( 注:JS 中的 delete 關鍵字用于刪除對象屬性,經(jīng)常被誤作為 C++ 中的 delete,而后者是用于釋放不再使用的對象 ),如 程序清單 2。
清單 2. 字面式 (literal notation) 對象聲明
var person = {
name: “張三”,
age: 26,
gender: “男”,
eat: function( stuff ) {
alert( “我在吃” + stuff );
}
};
person.height = 176;
delete person[ “age” ];
在實際開發(fā)過程中,大部分初學者或者對 JS 應用沒有太高要求的開發(fā)者也基本上只用到 ECMAScript 定義的這一部分內(nèi)容,就能滿足基本的開發(fā)需求。然而,這樣的代碼復用性非常弱,與其他實現(xiàn)了繼承、派生、多態(tài)等等的類式面向?qū)ο蟮膹婎愋驼Z言比較起來顯得有些干癟,不能滿足復雜的 JS 應用開發(fā)。所以 ECMAScript 引入原型來解決對象繼承問題。
除了 字面式聲明(literal notation)方式之外,ECMAScript 允許通過 構造器(constructor)創(chuàng)建對象。每個構造器實際上是一個 函數(shù)(function) 對象, 該函數(shù)對象含有一個“prototype”屬性用于實現(xiàn) 基于原型的繼承(prototype-based inheritance)和 共享屬性(shared properties)。對象可以由“new 關鍵字 + 構造器調(diào)用”的方式來創(chuàng)建,如 程序清單 3:
清單 3. 使用構造器 (constructor) 創(chuàng)建對象
// 構造器 Person 本身是一個函數(shù)對象
function Person() {
// 此處可做一些初始化工作
}
// 它有一個名叫 prototype 的屬性
Person.prototype = {
name: “張三”,
age: 26,
gender: “男”,
eat: function( stuff ) {
alert( “我在吃” + stuff );
}
}
// 使用 new 關鍵字構造對象
var p = new Person();
由于早期 JavaScript 的發(fā)明者為了使這門語言與大名鼎鼎的 Java 拉上關系 ( 雖然現(xiàn)在大家知道二者是雷鋒和雷鋒塔的關系 ),使用了new 關鍵字來限定構造器調(diào)用并創(chuàng)建對象,以使其在語法上跟 Java 創(chuàng)建對象的方式看上去類似。但需要指出的是,這兩門語言的new含義毫無關系,因為其對象構造的機理完全不同。也正是因為這里語法上的類似,眾多習慣了類式面向?qū)ο笳Z言中對象創(chuàng)建方式的程序員,難以透徹理解 JS 對象原型構造的方式,因為他們總是不明白在 JS 語言中,為什么“函數(shù)名可以作為類名”的現(xiàn)象。而實質(zhì)上,JS 這里僅僅是借用了關鍵字 new,僅此而已;換句話說,ECMAScript 完全可以用其它 非 new 表達式來用調(diào)用構造器創(chuàng)建對象。
在 ECMAScript 中,每個由構造器創(chuàng)建的對象擁有一個指向構造器 prototype 屬性值的 隱式引用(implicit reference),這個引用稱之為 原型(prototype)。進一步,每個原型可以擁有指向自己原型的 隱式引用(即該原型的原型),如此下去,這就是所謂的原型鏈(prototype chain) (參考資源)。在具體的語言實現(xiàn)中,每個對象都有一個 __proto__ 屬性來實現(xiàn)對原型的 隱式引用。程序清單 4說明了這一點。
清單 4. 對象的 __proto__ 屬性和隱式引用
function Person( name ) {
this.name = name;
}
var p = new Person();
// 對象的隱式引用指向了構造器的 prototype 屬性,所以此處打印 true
console.log( p.__proto__ === Person.prototype );
// 原型本身是一個 Object 對象,所以他的隱式引用指向了
// Object 構造器的 prototype 屬性 , 故而打印 true
console.log( Person.prototype.__proto__ === Object.prototype );
// 構造器 Person 本身是一個函數(shù)對象,所以此處打印 true
console.log( Person.__proto__ === Function.prototype );
有了 原型鏈,便可以定義一種所謂的 屬性隱藏機制,并通過這種機制實現(xiàn)繼承。ECMAScript 規(guī)定,當要給某個對象的屬性賦值時,解釋器會查找該對象原型鏈中第一個含有該屬性的對象(注:原型本身就是一個對象,那么原型鏈即為一組對象的鏈。對象的原型鏈中的第一個對象是該對象本身)進行賦值。反之,如果要獲取某個對象屬性的值,解釋器自然是返回該對象原型鏈中首先具有該屬性的對象屬性值。圖 1說名了這中隱藏機制:
在圖 1 中,object1->prototype1->prototype2 構成了 對象 object1 的原型鏈,根據(jù)上述屬性隱藏機制,可以清楚地看到 prototype1 對象中的 property4 屬性和 prototype2 對象中的 property3 屬性皆被隱藏。理解了原型鏈,那么將非常容易理解 JS 中基于原型的繼承實現(xiàn)原理,程序清單 5 是利用原型鏈實現(xiàn)繼承的簡單例子。
清單 5. 利用原型鏈 Horse->Mammal->Animal 實現(xiàn)繼承
// 聲明 Animal 對象構造器
function Animal() {
}
// 將 Animal 的 prototype 屬性指向一個對象,
// 亦可直接理解為指定 Animal 對象的原型
Animal.prototype = {
name: animal",
weight: 0,
eat: function() {
alert( "Animal is eating!" );
}
}
// 聲明 Mammal 對象構造器
function Mammal() {
this.name = "mammal";
}
// 指定 Mammal 對象的原型為一個 Animal 對象。
// 實際上此處便是在創(chuàng)建 Mammal 對象和 Animal 對象之間的原型鏈
Mammal.prototype = new Animal();
// 聲明 Horse 對象構造器
function Horse( height, weight ) {
this.name = "horse";
this.height = height;
this.weight = weight;
}
// 將 Horse 對象的原型指定為一個 Mamal 對象,繼續(xù)構建 Horse 與 Mammal 之間的原型鏈
Horse.prototype = new Mammal();
// 重新指定 eat 方法 , 此方法將覆蓋從 Animal 原型繼承過來的 eat 方法
Horse.prototype.eat = function() {
alert( "Horse is eating grass!" );
}
// 驗證并理解原型鏈
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 中對象原型繼承邏輯實現(xiàn)的關鍵在于 Horse.prototype = new Mammal() 和 Mammal.prototype = new Animal() 這兩句代碼。首先,等式右邊的結(jié)果是構造出一個臨時對象,然后將這個對象賦值給等式左邊對象的 prototype 屬性。也就是說將右邊新建的對象作為左邊對象的原型。讀者可以將這兩個等式替換到相應的程序清單 5 代碼最后兩行的等式中自行領悟。
從代碼清單 5 可以看出,基于原型的繼承方式,雖然實現(xiàn)了代碼復用,但其行文松散且不夠流暢,可閱讀性差,不利于實現(xiàn)擴展和對源代碼進行有效地組織管理。不得不承認,類式繼承方式在語言實現(xiàn)上更具健壯性,且在構建可復用代碼和組織架構程序方面具有明顯的優(yōu)勢。這使得程序員們希望尋找到一種能夠在 JavaScript 中以類式繼承風格進行編碼的方法途徑。從抽象的角度來講,既然類式繼承和原型繼承都是為實現(xiàn)面向?qū)ο蠖O計的,并且他們各自實現(xiàn)的載體語言在計算能力上是等價的 ( 因為圖靈機的計算能力與 Lambda 演算的計算能力是等價的 ),那么能不能找到一種變換,使得原型式繼承語言通過該變換實現(xiàn)具有類式繼承編碼的風格呢?
目前一些主流的 JS 框架都提供了這種轉(zhuǎn)換機制,也即類式聲明方法,比如 Dojo.declare()、Ext.entend() 等等。用戶使用這些框架,可以輕易而友好地組織自己的 JS 代碼。其實,在眾多框架出現(xiàn)之前,JavaScript 大師 Douglas Crockford 最早利用三個函數(shù)對 Function 對象進行擴展,實現(xiàn)了這種變換,關于它的實現(xiàn)細節(jié)可以(參考資源)。此外還有由 Dean Edwards實現(xiàn)的著名的 Base.js(參考資源)。值得一提的是,jQuery 之父 John Resig 在搏眾家之長之后,用不到 30 行代碼便實現(xiàn)了自己的 Simple Inheritance。使用其提供的 extend 方法聲明類非常簡單。程序清單 6是使用了 Simple Inheritance庫實現(xiàn)類的聲明的例子。其中最后一句打印輸出語句是對 Simple Inheritance實現(xiàn)類式繼承的最好說明。
清單 6. 使用 Simple Inheritance 實現(xiàn)類式繼承
// 聲明 Person 類
var Person = Class.extend( {
_issleeping: true,
init: function( name ) {
this._name = name;
},
isSleeping: function() {
return this._issleeping;
}
} );
// 聲明 Programmer 類,并繼承 Person
var Programmer = Person.extend( {
init: function( name, issleeping ) {
// 調(diào)用父類構造函數(shù)
this._super( name );
// 設置自己的狀態(tài)
this._issleeping = issleeping;
}
} );
var person = new Person( "張三" );
var diors = new Programmer( "張江男", false );
// 打印 true
console.log( person.isSleeping() );
// 打印 false
console.log( diors.isSleeping() );
// 此處全為 true,故打印 true
console.log( person instanceof Person && person instanceof Class
&& diors instanceof Programmer &&
diors instanceof Person && diors instanceof Class );
如果您已對原型、函數(shù)構造器、閉包和基于上下文的 this 有了充分的理解,那么理解 Simple Inheritance 的實現(xiàn)原理也并非相當困難。從本質(zhì)上講,var Person = Class.extend(...)該語句中,左邊的 Person 實際上是獲得了由 Class 調(diào)用 extend 方法返回的一個構造器,也即一個 function 對象的引用。順著這個思路,我們繼續(xù)介紹 Simple Inheritance 是如何做到這一點,進而實現(xiàn)了由原型繼承方式到類式繼承方式的轉(zhuǎn)換的。圖 2 是 Simple Inheritance 的源碼及其附帶注釋。為了方便理解,用中文對代碼逐行補充說明。
圖 2.Simple Inheritance 源碼解析
拋開代碼第二部分,整體連貫地考察第一和第三部分會發(fā)現(xiàn),extend 函數(shù)的根本目的就是要構造一個具有新原型屬性的新構造器。我們不禁感嘆 John Resig的大師手筆及其對 JS 語言本質(zhì)把握的細膩程度。至于 John Resig是如何想到這樣精妙的實現(xiàn)方法,感興趣的讀者可以閱讀本文 (參考資源),其中有詳細介紹關于最初設計 Simple Inheritance 的思維過程。
到此為止,如果您任然對 JavaScript 面向?qū)ο蟪謶岩蓱B(tài)度,那么這個懷疑一定是,JavaScript 沒有實現(xiàn)面向?qū)ο笾械男畔㈦[藏,即私有和公有。與其他類式面向?qū)ο竽菢语@式地聲明私有公有成員的方式不同,JavaScript 的信息隱藏就是靠閉包實現(xiàn)的。見 程序清單 7:
清單 7. 使用閉包實現(xiàn)信息隱藏
// 聲明 User 構造器
function User( pwd ) {
// 定義私有屬性
var password = pwd;
// 定義私有方法
function getPassword() {
// 返回了閉包中的 password
return password;
}
// 特權函數(shù)聲明,用于該對象其他公有方法能通過該特權方法訪問到私有成員
this.passwordService = function() {
return getPassword();
}
}
// 公有成員聲明
User.prototype.checkPassword = function( pwd ) {
return this.passwordService() === pwd;
};
// 驗證隱藏性
var u = new User( "123456" );
// 打印 true
console.log( u.checkPassword( "123456" ) );
// 打印 undefined
console.log( u.password );
// 打印 true
console.log( typeof u.gePassword === "undefined" );
JavaScript 必須依賴閉包實現(xiàn)信息隱藏,是由其函數(shù)式語言特性所決定的。本文不會對函數(shù)式語言和閉包這兩個話題展開討論,正如上文默認您理解 JavaScript 中基于上下文的 this 一樣。關于 JavaScript 中實現(xiàn)信息隱藏,Douglas Crockford在《 Private members in JavaScript 》(參考資源)一文中有更權威和詳細的介紹。
JavaScript 被認為是世界上最受誤解的編程語言,因為它身披 c 語言家族的外衣,表現(xiàn)的卻是 LISP 風格的函數(shù)式語言特性;沒有類,卻實也徹底實現(xiàn)了面向?qū)ο?。要對這門語言有透徹的理解,就必須扒開其 c 語言的外衣,從新回到函數(shù)式編程的角度,同時摒棄原有類的面向?qū)ο蟾拍钊W習領悟它。隨著近些年來 Web 應用的普及和 JS 語言自身的長足發(fā)展,特別是后臺 JS 引擎的出現(xiàn) ( 如基于 V8 的 NodeJS 等 ),可以預見,原來只是作為玩具編寫頁面效果的 JS 將獲得更廣闊發(fā)展天地。這樣的發(fā)展趨勢,也對 JS 程序員提出了更高要求。只有徹底領悟了這門語言,才有可能在大型的 JS 項目中發(fā)揮她的威力。
相關文章
JS+CSS實現(xiàn)超漂亮的動態(tài)翻書效果(思路詳解)
我們平常沖浪時是不是看過一些學校高級的錄取通知書,翻開通知書就能看見里面的內(nèi)容,呈現(xiàn)出逼真的3D效果,本文帶領大家基于JS+CSS實現(xiàn)超漂亮的動態(tài)翻書效果,需要的朋友可以參考下2023-05-05關于 byval 與 byref 的區(qū)別分析總結(jié)
關于 byval 與 byref 的區(qū)別分析總結(jié)...2007-10-10JavaScript 數(shù)組循環(huán)引起的思考
Google array.js 程序中建議改進的代碼。思考為什么i-- 到了 i為0時while循環(huán)結(jié)束了?2010-01-01js+數(shù)組實現(xiàn)網(wǎng)頁上顯示時間/星期幾的實用方法
在網(wǎng)頁上顯示時間(年月日/時分秒),很多新手朋友都想實現(xiàn)這樣的功能,本文整理了一些實用技巧,殺出來與大家分享,感興趣的朋友可以了解下2013-01-01bootstrap時間插件daterangepicker使用詳解
這篇文章主要為大家詳細介紹了bootstrap時間插件daterangepicker使用,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-10-10JavaScript輸入分鐘、秒倒計時技巧總結(jié)(附代碼)
這篇文章主要介紹了JavaScript輸入分鐘、秒倒計時的代碼實現(xiàn),通過css和js代碼展示了邏輯過程,具體操作步驟大家可查看下文的詳細講解,感興趣的小伙伴們可以參考一下。2017-08-08