全面理解面向對象的 JavaScript (share)

 

 如下分享自:javascript

 http://www.ibm.com/developerworks/cn/web/1304_zengyz_jsoo/

 

 

簡介: JavaScript 函數式腳本語言特性以及其看似隨意的編寫風格,致使長期以來人們對這一門語言的誤解,即認爲 JavaScript 不是一門面向對象的語言,或者只是部分具有一些面向對象的特徵。本文將回歸面向對象本意,從對語言感悟的角度闡述爲何 JavaScript 是一門完全的面向對象的語言,以及如何正確地使用這一特性。html

 

前言

當今 JavaScript 大行其道,各類應用對其依賴日深。web 程序員已逐漸習慣使用各類優秀的 JavaScript 框架快速開發 Web 應用,從而忽略了對原生 JavaScript 的學習和深刻理解。因此,常常出現的狀況是,不少作了多年 JS 開發的程序員對閉包、函數式編程、原型老是說不清道不明,即便使用了框架,其代碼組織也很是糟糕。這都是對原生 JavaScript 語言特性理解不夠的表現。要掌握好 JavaScript,首先一點是必須摒棄一些其餘高級語言如 Java、C# 等類式面向對象思惟的干擾,全面地從函數式語言的角度理解 JavaScript 原型式面向對象的特色。把握好這一點以後,纔有可能進一步使用好這門語言。本文適合羣體:使用過 JS 框架但對 JS 語言本質缺少理解的程序員,具備 Java、C++ 等語言開發經驗,準備學習並使用 JavaScript 的程序員,以及一直對 JavaScript 是否面向對象模棱兩可,但但願知道真相的 JS 愛好者。html5

 

從新認識面向對象

爲了說明 JavaScript 是一門完全的面向對象的語言,首先有必要從面向對象的概念着手 , 探討一下面向對象中的幾個概念:java

  • 一切事物皆對象
  • 對象具備封裝和繼承特性
  • 對象與對象之間使用消息通訊,各自存在信息隱藏

以這三點作爲依據,C++ 是半面向對象半面向過程語言,由於,雖然他實現了類的封裝、繼承和多態,但存在非對象性質的全局函數和變量。Java、C# 是徹底的面嚮對象語言,它們經過類的形式組織函數和變量,使之不能脫離對象存在。但這裏函數自己是一個過程,只是依附在某個類上。程序員

然而,面向對象僅僅是一個概念或者編程思想而已,它不該該依賴於某個語言存在。好比 Java 採用面向對象思想構造其語言,它實現了類、繼承、派生、多態、接口等機制。可是這些機制,只是實現面向對象編程的一種手段,而非必須。換言之,一門語言能夠根據其自身特性選擇合適的方式來實現面向對象。因此,因爲大多數程序員首先學習或者使用的是相似 Java、C++ 等高級編譯型語言(Java 雖然是半編譯半解釋,但通常作爲編譯型來說解),於是先入爲主地接受了「類」這個面向對象實現方式,從而在學習腳本語言的時候,習慣性地用類式面嚮對象語言中的概念來判斷該語言是不是面嚮對象語言,或者是否具有面向對象特性。這也是阻礙程序員深刻學習並掌握 JavaScript 的重要緣由之一。web

實際上,JavaScript 語言是經過一種叫作 原型(prototype的方式來實現面向對象編程的。下面就來討論 基於類的(class-based)面向對象和 基於原型的 (prototype-based) 面向對象這兩種方式在構造客觀世界的方式上的差異。ajax

 

基於類的面向對象和基於原型的面向對象方式比較

在基於類的面向對象方式中,對象(object依靠 類(class來產生。而在基於原型的面向對象方式中,對象(object則是依靠 構造器(constructor利用 原型(prototype構造出來的。舉個客觀世界的例子來講明二種方式認知的差別。例如工廠造一輛車,一方面,工人必須參照一張工程圖紙,設計規定這輛車應該如何製造。這裏的工程圖紙就比如是語言中的 類 (class),而車就是按照這個 類(class製造出來的;另外一方面,工人和機器 ( 至關於 constructor) 利用各類零部件如發動機,輪胎,方向盤 ( 至關於 prototype 的各個屬性 ) 將汽車構造出來。編程

事實上關於這兩種方式誰更爲完全地表達了面向對象的思想,目前尚有爭論。但筆者認爲原型式面向對象是一種更爲完全的面向對象方式,理由以下:json

首先,客觀世界中的對象的產生都是其它實物對象構造的結果,而抽象的「圖紙」是不能產生「汽車」的,也就是說,類是一個抽象概念而並不是實體,而對象的產生是一個實體的產生;數據結構

其次,按照一切事物皆對象這個最基本的面向對象的法則來看,類 (class) 自己並非一個對象,然而原型方式中的構造器 (constructor) 和原型 (prototype) 自己也是其餘對象經過原型方式構造出來的對象。

再次,在類式面嚮對象語言中,對象的狀態 (state) 由對象實例 (instance) 所持有,對象的行爲方法 (method) 則由聲明該對象的類所持有,而且只有對象的結構和方法可以被繼承;而在原型式面嚮對象語言中,對象的行爲、狀態都屬於對象自己,而且可以一塊兒被繼承(參考資源),這也更貼近客觀實際。

最後,類式面嚮對象語言好比 Java,爲了彌補沒法使用面向過程語言中全局函數和變量的不便,容許在類中聲明靜態 (static) 屬性和靜態方法。而實際上,客觀世界不存在所謂靜態概念,由於一切事物皆對象!而在原型式面嚮對象語言中,除內建對象 (build-in object) 外,不容許全局對象、方法或者屬性的存在,也沒有靜態概念。全部語言元素 (primitive) 必須依賴對象存在。但因爲函數式語言的特色,語言元素所依賴的對象是隨着運行時 (runtime) 上下文 (context) 變化而變化的,具體體如今 this 指針的變化。正是這種特色更貼近 「萬物皆有所屬,宇宙乃萬物生存之根本」的天然觀點。在 程序清單 1中 window 便相似與宇宙的概念。


清單 1. 對象的上下文依賴
				 
 <script> 
 var str = "我是一個 String 對象 , 我聲明在這裏 , 但我不是獨立存在的!"
 var obj = { des: "我是一個 Object 對象 , 我聲明在這裏,我也不是獨立存在的。" }; 
 var fun = function() { 
    console.log( "我是一個 Function 對象!誰調用我,我屬於誰:", this ); 
 }; 

 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 對象!誰調用我,我屬於誰:window 
 obj.fun();                          // 打印 我是一個 Function 對象!誰調用我,我屬於誰:obj 
 fun.apply(str);                   // 打印 我是一個 Function 對象!誰調用我,我屬於誰:str 
 </script> 

在接受了面向對象存在一種叫作基於原型實現的方式的事實以後,下面咱們就能夠來深刻探討 ECMAScript 是如何依據這一方式構造本身的語言的。

 

最基本的面向對象

ECMAScript 是一門完全的面向對象的編程語言(參考資源),JavaScript 是其中的一個變種 (variant)。它提供了 6 種基本數據類型,即 Boolean、Number、String、Null、Undefined、Object。爲了實現面向對象,ECMAScript設計出了一種很是成功的數據結構 - JSON(JavaScript Object Notation), 這一經典結構已經能夠脫離語言而成爲一種普遍應用的數據交互格式 (參考資源)。

應該說,具備基本數據類型和 JSON 構造語法的 ECMAScript 已經基本能夠實現面向對象的編程了。開發者能夠隨意地用 字面式聲明(literal notation方式來構造一個對象,並對其不存在的屬性直接賦值,或者用 delete 將屬性刪除 ( 注:JS 中的 delete 關鍵字用於刪除對象屬性,常常被誤做爲 C++ 中的 delete,然後者是用於釋放再也不使用的對象 ),如 程序清單 2


清單 2. 字面式 (literal notation) 對象聲明
				 
 var person = { 
    name: 「張三」, 
    age: 26, 
    gender: 「男」, 
    eat: function( stuff ) { 
        alert( 「我在吃」 + stuff ); 
    } 
 }; 
 person.height = 176; 
 delete person[ 「age」 ]; 

在實際開發過程當中,大部分初學者或者對 JS 應用沒有過高要求的開發者也基本上只用到 ECMAScript 定義的這一部份內容,就能知足基本的開發需求。然而,這樣的代碼複用性很是弱,與其餘實現了繼承、派生、多態等等的類式面向對象的強類型語言比較起來顯得有些乾癟,不能知足複雜的 JS 應用開發。因此 ECMAScript 引入原型來解決對象繼承問題。

 

使用函數構造器構造對象

除了 字面式聲明(literal notation方式以外,ECMAScript 容許經過 構造器(constructor)建立對象。每一個構造器其實是一個 函數(function) 對象, 該函數對象含有一個「prototype」屬性用於實現 基於原型的繼承prototype-based inheritance和 共享屬性(shared properties對象能夠由「new 關鍵字 + 構造器調用」的方式來建立,如 程序清單 3


清單 3. 使用構造器 (constructor) 建立對象
				 
 // 構造器 Person 自己是一個函數對象
 function Person() { 
	 // 此處可作一些初始化工做
 } 
 // 它有一個名叫 prototype 的屬性
 Person.prototype = { 
    name: 「張三」, 
    age: 26, 
    gender: 「男」, 
    eat: function( stuff ) { 
        alert( 「我在吃」 + stuff ); 
    } 
 } 
 // 使用 new 關鍵字構造對象
 var p = new Person(); 

因爲早期 JavaScript 的發明者爲了使這門語言與大名鼎鼎的 Java 拉上關係 ( 雖然如今你們知道兩者是雷鋒和雷鋒塔的關係 ),使用了 new 關鍵字來限定構造器調用並建立對象,以使其在語法上跟 Java 建立對象的方式看上去相似。但須要指出的是,這兩門語言的new含義毫無關係,由於其對象構造的機理徹底不一樣。也正是由於這裏語法上的相似,衆多習慣了類式面嚮對象語言中對象建立方式的程序員,難以透徹理解 JS 對象原型構造的方式,由於他們老是不明白在 JS 語言中,爲何「函數名能夠做爲類名」的現象。而實質上,JS 這裏僅僅是借用了關鍵字 new,僅此而已;換句話說,ECMAScript 徹底能夠用其它  new 表達式來用調用構造器建立對象。

 

完全理解原型鏈 (prototype chain)

在 ECMAScript 中,每一個由構造器建立的對象擁有一個指向構造器 prototype 屬性值的 隱式引用(implicit reference,這個引用稱之爲 原型(prototype。進一步,每一個原型能夠擁有指向本身原型的 隱式引用(即該原型的原型),如此下去,這就是所謂的原型鏈(prototype chain (參考資源)。在具體的語言實現中,每一個對象都有一個 __proto__ 屬性來實現對原型的 隱式引用程序清單 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 自己是一個函數對象,因此此處打印 true 
 console.log( Person.__proto__ === Function.prototype ); 

有了 原型鏈,即可以定義一種所謂的 屬性隱藏機制,並經過這種機制實現繼承。ECMAScript 規定,當要給某個對象的屬性賦值時,解釋器會查找該對象原型鏈中第一個含有該屬性的對象(注:原型自己就是一個對象,那麼原型鏈即爲一組對象的鏈。對象的原型鏈中的第一個對象是該對象自己)進行賦值。反之,若是要獲取某個對象屬性的值,解釋器天然是返回該對象原型鏈中首先具備該屬性的對象屬性值。圖 1說名了這中隱藏機制:


圖 1. 原型鏈中的屬性隱藏機制
 

在圖 1 中,object1->prototype1->prototype2 構成了 對象 object1 的原型鏈,根據上述屬性隱藏機制,能夠清楚地看到 prototype1 對象中的 property4 屬性和 prototype2 對象中的 property3 屬性皆被隱藏。理解了原型鏈,那麼將很是容易理解 JS 中基於原型的繼承實現原理,程序清單 5 是利用原型鏈實現繼承的簡單例子。


清單 5. 利用原型鏈 Horse->Mammal->Animal 實現繼承
				 
 // 聲明 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 對象。
 // 實際上此處即是在建立 Mammal 對象和 Animal 對象之間的原型鏈
 Mammal.prototype = new Animal(); 

 // 聲明 Horse 對象構造器
 function Horse( height, weight ) { 
    this.name = "horse"; 
    this.height = height; 
    this.weight = weight; 
 } 
 // 將 Horse 對象的原型指定爲一個 Mamal 對象,繼續構建 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 中對象原型繼承邏輯實現的關鍵在於 Horse.prototype = new Mammal() 和 Mammal.prototype = new Animal() 這兩句代碼。首先,等式右邊的結果是構造出一個臨時對象,而後將這個對象賦值給等式左邊對象的 prototype 屬性。也就是說將右邊新建的對象做爲左邊對象的原型。讀者能夠將這兩個等式替換到相應的程序清單 5 代碼最後兩行的等式中自行領悟。

 

JavaScript 類式繼承的實現方法

從代碼清單 5 能夠看出,基於原型的繼承方式,雖然實現了代碼複用,但其行文鬆散且不夠流暢,可閱讀性差,不利於實現擴展和對源代碼進行有效地組織管理。不得不認可,類式繼承方式在語言實現上更具健壯性,且在構建可複用代碼和組織架構程序方面具備明顯的優點。這使得程序員們但願尋找到一種可以在 JavaScript 中以類式繼承風格進行編碼的方法途徑。從抽象的角度來說,既然類式繼承和原型繼承都是爲實現面向對象而設計的,而且他們各自實現的載體語言在計算能力上是等價的 ( 由於圖靈機的計算能力與 Lambda 演算的計算能力是等價的 ),那麼能不能找到一種變換,使得原型式繼承語言經過該變換實現具備類式繼承編碼的風格呢?

目前一些主流的 JS 框架都提供了這種轉換機制,也即類式聲明方法,好比 Dojo.declare()、Ext.entend() 等等。用戶使用這些框架,能夠輕易而友好地組織本身的 JS 代碼。其實,在衆多框架出現以前,JavaScript 大師 Douglas Crockford 最先利用三個函數對 Function 對象進行擴展,實現了這種變換,關於它的實現細節能夠(參考資源)。此外還有由 Dean Edwards實現的著名的 Base.js(參考資源)。值得一提的是,jQuery 之父 John Resig 在搏衆家之長以後,用不到 30 行代碼便實現了本身的 Simple Inheritance。使用其提供的 extend 方法聲明類很是簡單。程序清單 6是使用了 Simple Inheritance庫實現類的聲明的例子。其中最後一句打印輸出語句是對 Simple Inheritance實現類式繼承的最好說明。


清單 6. 使用 Simple Inheritance 實現類式繼承
				 
 // 聲明 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 ) { 
        // 調用父類構造函數
        this._super( name ); 
        // 設置本身的狀態
        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 ); 

若是您已對原型、函數構造器、閉包和基於上下文的 this 有了充分的理解,那麼理解 Simple Inheritance 的實現原理也並不是至關困難。從本質上講,var Person = Class.extend(...)該語句中,左邊的 Person 其實是得到了由 Class 調用 extend 方法返回的一個構造器,也即一個 function 對象的引用。順着這個思路,咱們繼續介紹 Simple Inheritance 是如何作到這一點,進而實現了由原型繼承方式到類式繼承方式的轉換的。圖 2 是 Simple Inheritance 的源碼及其附帶註釋。爲了方便理解,用中文對代碼逐行補充說明。


圖 2.Simple Inheritance 源碼解析
 

拋開代碼第二部分,總體連貫地考察第一和第三部分會發現,extend 函數的根本目的就是要構造一個具備新原型屬性的新構造器。咱們不由感嘆 John Resig的大師手筆及其對 JS 語言本質把握的細膩程度。至於 John Resig是如何想到這樣精妙的實現方法,感興趣的讀者能夠閱讀本文 (參考資源),其中有詳細介紹關於最初設計 Simple Inheritance 的思惟過程。

 

JavaScript 私有成員實現

到此爲止,若是您任然對 JavaScript 面向對象持懷疑態度,那麼這個懷疑必定是,JavaScript 沒有實現面向對象中的信息隱藏,即私有和公有。與其餘類式面向對象那樣顯式地聲明私有公有成員的方式不一樣,JavaScript 的信息隱藏就是靠閉包實現的。見 程序清單 7:


清單 7. 使用閉包實現信息隱藏
				 
 // 聲明 User 構造器
 function User( pwd ) { 
    // 定義私有屬性
    var password = pwd; 
    // 定義私有方法 
    function getPassword() { 
        // 返回了閉包中的 password 
        return password; 
    } 
    // 特權函數聲明,用於該對象其餘公有方法能經過該特權方法訪問到私有成員
    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 必須依賴閉包實現信息隱藏,是由其函數式語言特性所決定的。本文不會對函數式語言和閉包這兩個話題展開討論,正如上文默認您理解 JavaScript 中基於上下文的 this 同樣。關於 JavaScript 中實現信息隱藏,Douglas Crockford在《 Private members in JavaScript 》(參考資源)一文中有更權威和詳細的介紹。

 

結束語

JavaScript 被認爲是世界上最受誤解的編程語言,由於它身披 c 語言家族的外衣,表現的倒是 LISP 風格的函數式語言特性;沒有類,卻實也完全實現了面向對象。要對這門語言有透徹的理解,就必須扒開其 c 語言的外衣,重新回到函數式編程的角度,同時摒棄原有類的面向對象概念去學習領悟它。隨着近些年來 Web 應用的普及和 JS 語言自身的長足發展,特別是後臺 JS 引擎的出現 ( 如基於 V8 的 NodeJS 等 ),能夠預見,原來只是做爲玩具編寫頁面效果的 JS 將得到更廣闊發展天地。這樣的發展趨勢,也對 JS 程序員提出了更高要求。只有完全領悟了這門語言,纔有可能在大型的 JS 項目中發揮她的威力。

 

參考資料

學習

相關文章
相關標籤/搜索