爲了說明 JavaScript 是一門完全的面向對象的語言,首先有必要從面向對象的概念着手 , 探討一下面向對象中的幾個概念:程序員
以 這三點作爲依據,C++ 是半面向對象半面向過程語言,由於,雖然他實現了類的封裝、繼承和多態,但存在非對象性質的全局函數和變量。Java、C# 是徹底的面嚮對象語言,它們經過類的形式組織函數和變量,使之不能脫離對象存在。但這裏函數自己是一個過程,只是依附在某個類上。web
然而,面 向對象僅僅是一個概念或者編程思想而已,它不該該依賴於某個語言存在。好比 Java 採用面向對象思想構造其語言,它實現了類、繼承、派生、多態、接口等機制。可是這些機制,只是實現面向對象編程的一種手段,而非必須。換言之,一門語言可 以根據其自身特性選擇合適的方式來實現面向對象。因此,因爲大多數程序員首先學習或者使用的是相似 Java、C++ 等高級編譯型語言(Java 雖然是半編譯半解釋,但通常作爲編譯型來說解),於是先入爲主地接受了「類」這個面向對象實現方式,從而在學習腳本語言的時候,習慣性地用類式面向對象語 言中的概念來判斷該語言是不是面嚮對象語言,或者是否具有面向對象特性。這也是阻礙程序員深刻學習並掌握 JavaScript 的重要緣由之一。編程
實際上,JavaScript 語言是經過一種叫作 原型(prototype)的方式來實現面向對象編程的。下面就來討論 基於類的(class-based)面向對象和 基於原型的 (prototype-based) 面向對象這兩種方式在構造客觀世界的方式上的差異。數據結構
在基於類的面向對象方式中,對象(object)依靠 類(class)來產生。而在基於原型的面向對象方式中,對象(object)則是依靠 構造器(constructor)利用 原型(prototype)構造出來的。舉個客觀世界的例子來講明二種方式認知的差別。例如工廠造一輛車,一方面,工人必須參照一張工程圖紙,設計規定這輛車應該如何製造。這裏的工程圖紙就比如是語言中的 類 (class),而車就是按照這個 類(class)製造出來的;另外一方面,工人和機器 ( 至關於 constructor) 利用各類零部件如發動機,輪胎,方向盤 ( 至關於 prototype 的各個屬性 ) 將汽車構造出來。閉包
事實上關於這兩種方式誰更爲完全地表達了面向對象的思想,目前尚有爭論。但筆者認爲原型式面向對象是一種更爲完全的面向對象方式,理由以下:架構
首先,客觀世界中的對象的產生都是其它實物對象構造的結果,而抽象的「圖紙」是不能產生「汽車」的,也就是說,類是一個抽象概念而並不是實體,而對象的產生是一個實體的產生;app
其次,按照一切事物皆對象這個最基本的面向對象的法則來看,類 (class) 自己並非一個對象,然而原型方式中的構造器 (constructor) 和原型 (prototype) 自己也是其餘對象經過原型方式構造出來的對象。框架
再 次,在類式面嚮對象語言中,對象的狀態 (state) 由對象實例 (instance) 所持有,對象的行爲方法 (method) 則由聲明該對象的類所持有,而且只有對象的結構和方法可以被繼承;而在原型式面嚮對象語言中,對象的行爲、狀態都屬於對象自己,而且可以一塊兒被繼承(參考資源),這也更貼近客觀實際。編程語言
最 後,類式面嚮對象語言好比 Java,爲了彌補沒法使用面向過程語言中全局函數和變量的不便,容許在類中聲明靜態 (static) 屬性和靜態方法。而實際上,客觀世界不存在所謂靜態概念,由於一切事物皆對象!而在原型式面嚮對象語言中,除內建對象 (build-in object) 外,不容許全局對象、方法或者屬性的存在,也沒有靜態概念。全部語言元素 (primitive) 必須依賴對象存在。但因爲函數式語言的特色,語言元素所依賴的對象是隨着運行時 (runtime) 上下文 (context) 變化而變化的,具體體如今 this 指針的變化。正是這種特色更貼近 「萬物皆有所屬,宇宙乃萬物生存之根本」的天然觀點。在 程序清單 1中 window 便相似與宇宙的概念。函數式編程
<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。
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:
// 構造器 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 表達式來用調用構造器建立對象。
在 ECMAScript 中,每一個由構造器建立的對象擁有一個指向構造器 prototype 屬性值的 隱式引用(implicit reference),這個引用稱之爲 原型(prototype)。進一步,每一個原型能夠擁有指向本身原型的 隱式引用(即該原型的原型),如此下去,這就是所謂的 原型鏈(prototype chain) (參考資源)。在具體的語言實現中,每一個對象都有一個 __proto__ 屬性來實現對原型的 隱式引用。程序清單 4說明了這一點。
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 中,object1->prototype1->prototype2 構成了 對象 object1 的原型鏈,根據上述屬性隱藏機制,能夠清楚地看到 prototype1 對象中的 property4 屬性和 prototype2 對象中的 property3 屬性皆被隱藏。理解了原型鏈,那麼將很是容易理解 JS 中基於原型的繼承實現原理,程序清單 5 是利用原型鏈實現繼承的簡單例子。
// 聲明 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 代碼最後兩行的等式中自行領悟。
從 代碼清單 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實現類式繼承的最好說明。
// 聲明 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 的源碼及其附帶註釋。爲了方便理解,用中文對代碼逐行補充說明。
拋開代碼第二部分,總體連貫地考察第一和第三部分會發現,extend 函數的根本目的就是要構造一個具備新原型屬性的新構造器。咱們不由感嘆 John Resig的大師手筆及其對 JS 語言本質把握的細膩程度。至於 John Resig是如何想到這樣精妙的實現方法,感興趣的讀者能夠閱讀本文 (參考資源),其中有詳細介紹關於最初設計 Simple Inheritance 的思惟過程。
到 此爲止,若是您任然對 JavaScript 面向對象持懷疑態度,那麼這個懷疑必定是,JavaScript 沒有實現面向對象中的信息隱藏,即私有和公有。與其餘類式面向對象那樣顯式地聲明私有公有成員的方式不一樣,JavaScript 的信息隱藏就是靠閉包實現的。見 程序清單 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 項目中發揮她的威力。