JavaScript 函數式腳本語言特性以及其看似隨意的編寫風格,致使長期以來人們對這一門語言的誤解,即認爲 JavaScript 不是一門面向對象的語言,或者只是部分具有一些面向對象的特徵。本文將回歸面向對象本意,從對語言感悟的角度闡述爲何 JavaScript 是一門完全的面向對象的語言,以及如何正確地使用這一特性。程序員
當今 JavaScript 大行其道,各類應用對其依賴日深。web 程序員已逐漸習慣使用各類優秀的 JavaScript 框架快速開發 Web 應用,從而忽略了對原生 JavaScript 的學習和深刻理解。因此,常常出現的狀況是,不少作了多年 JS 開發的程序員對閉包、函數式編程、原型老是說不清道不明,即便使用了框架,其代碼組織也很是糟糕。這都是對原生 JavaScript 語言特性理解不夠的表現。要掌握好 JavaScript,首先一點是必須摒棄一些其餘高級語言如 Java、C# 等類式面向對象思惟的干擾,全面地從函數式語言的角度理解 JavaScript 原型式面向對象的特色。把握好這一點以後,纔有可能進一步使用好這門語言。本文適合羣體:使用過 JS 框架但對 JS 語言本質缺少理解的程序員,具備 Java、C++ 等語言開發經驗,準備學習並使用 JavaScript 的程序員,以及一直對 JavaScript 是否面向對象模棱兩可,但但願知道真相的 JS 愛好者。web
爲了說明 JavaScript 是一門完全的面向對象的語言,首先有必要從面向對象的概念着手 , 探討一下面向對象中的幾個概念:編程
以這三點作爲依據,C++ 是半面向對象半面向過程語言,由於,雖然他實現了類的封裝、繼承和多態,但存在非對象性質的全局函數和變量。Java、C# 是徹底的面嚮對象語言,它們經過類的形式組織函數和變量,使之不能脫離對象存在。但這裏函數自己是一個過程,只是依附在某個類上。數據結構
然而,面向對象僅僅是一個概念或者編程思想而已,它不該該依賴於某個語言存在。好比 Java 採用面向對象思想構造其語言,它實現了類、繼承、派生、多態、接口等機制。可是這些機制,只是實現面向對象編程的一種手段,而非必須。換言之,一門語言能夠根據其自身特性選擇合適的方式來實現面向對象。因此,因爲大多數程序員首先學習或者使用的是相似 Java、C++ 等高級編譯型語言(Java 雖然是半編譯半解釋,但通常作爲編譯型來說解),於是先入爲主地接受了「類」這個面向對象實現方式,從而在學習腳本語言的時候,習慣性地用類式面嚮對象語言中的概念來判斷該語言是不是面嚮對象語言,或者是否具有面向對象特性。這也是阻礙程序員深刻學習並掌握 JavaScript 的重要緣由之一。閉包
實際上,JavaScript 語言是經過一種叫作 原型(prototype)的方式來實現面向對象編程的。下面就來討論 基於類的(class-based)面向對象和 基於原型的 (prototype-based) 面向對象這兩種方式在構造客觀世界的方式上的差異。框架
在基於類的面向對象方式中,對象(object)依靠 類(class)來產生。而在基於原型的面向對象方式中,對象(object)則是依靠 構造器(constructor)利用 原型(prototype)構造出來的。舉個客觀世界的例子來講明二種方式認知的差別。例如工廠造一輛車,一方面,工人必須參照一張工程圖紙,設計規定這輛車應該如何製造。這裏的工程圖紙就比如是語言中的 類 (class),而車就是按照這個 類(class)製造出來的;另外一方面,工人和機器 ( 至關於 constructor) 利用各類零部件如發動機,輪胎,方向盤 ( 至關於 prototype 的各個屬性 ) 將汽車構造出來。編程語言
事實上關於這兩種方式誰更爲完全地表達了面向對象的思想,目前尚有爭論。但筆者認爲原型式面向對象是一種更爲完全的面向對象方式,理由以下:函數式編程
首先,客觀世界中的對象的產生都是其它實物對象構造的結果,而抽象的「圖紙」是不能產生「汽車」的,也就是說,類是一個抽象概念而並不是實體,而對象的產生是一個實體的產生;函數
其次,按照一切事物皆對象這個最基本的面向對象的法則來看,類 (class) 自己並非一個對象,然而原型方式中的構造器 (constructor) 和原型 (prototype) 自己也是其餘對象經過原型方式構造出來的對象。學習
再次,在類式面嚮對象語言中,對象的狀態 (state) 由對象實例 (instance) 所持有,對象的行爲方法 (method) 則由聲明該對象的類所持有,而且只有對象的結構和方法可以被繼承;而在原型式面嚮對象語言中,對象的行爲、狀態都屬於對象自己,而且可以一塊兒被繼承(參考資源),這也更貼近客觀實際。
最後,類式面嚮對象語言好比 Java,爲了彌補沒法使用面向過程語言中全局函數和變量的不便,容許在類中聲明靜態 (static) 屬性和靜態方法。而實際上,客觀世界不存在所謂靜態概念,由於一切事物皆對象!而在原型式面嚮對象語言中,除內建對象 (build-in object) 外,不容許全局對象、方法或者屬性的存在,也沒有靜態概念。全部語言元素 (primitive) 必須依賴對象存在。但因爲函數式語言的特色,語言元素所依賴的對象是隨着運行時 (runtime) 上下文 (context) 變化而變化的,具體體如今 this 指針的變化。正是這種特色更貼近 「萬物皆有所屬,宇宙乃萬物生存之根本」的天然觀點。
ECMAScript 是一門完全的面向對象的編程語言(參考資源),JavaScript 是其中的一個變種 (variant)。它提供了 6 種基本數據類型, Boolean、Number、String、Null、Undefined、Object。爲了實現面向對象,ECMAScript設計出了一種很是成功的數據結構 - JSON(JavaScript Object Notation), 這一經典結構已經能夠脫離語言而成爲一種普遍應用的數據交互格式 (參考資源)。
應該說,具備基本數據類型和 JSON 構造語法的 ECMAScript 已經基本能夠實現面向對象的編程了。開發者能夠隨意地用 字面式聲明(literal notation)方式來構造一個對象,並對其不存在的屬性直接賦值,或者用 delete 將屬性刪除 ( 注:JS 中的 delete 關鍵字用於刪除對象屬性,常常被誤做爲 C++ 中的 delete,然後者是用於釋放再也不使用的對象 )。
在實際開發過程當中,大部分初學者或者對 JS 應用沒有過高要求的開發者也基本上只用到 ECMAScript 定義的這一部份內容,就能知足基本的開發需求。然而,這樣的代碼複用性很是弱,與其餘實現了繼承、派生、多態等等的類式面向對象的強類型語言比較起來顯得有些乾癟,不能知足複雜的 JS 應用開發。因此 ECMAScript 引入原型來解決對象繼承問題。
使用函數構造器構造對象
除了 字面式聲明(literal notation)方式以外,ECMAScript 容許經過 構造器(constructor)建立對象。每一個構造器其實是一個 函數(function) 對象, 該函數對象含有一個「prototype」屬性用於實現 基於原型的繼承(prototype-based inheritance)和 共享屬性(shared properties)。對象能夠由「new 關鍵字 + 構造器調用」的方式來建立。
因爲早期 JavaScript 的發明者爲了使這門語言與大名鼎鼎的 Java 拉上關係 ( 雖然如今你們知道兩者是雷鋒和雷鋒塔的關係 ),使用了 new 關鍵字來限定構造器調用並建立對象,以使其在語法上跟 Java 建立對象的方式看上去相似。但須要指出的是,這兩門語言的 new含義毫無關係,由於其對象構造的機理徹底不一樣。也正是由於這裏語法上的相似,衆多習慣了類式面嚮對象語言中對象建立方式的程序員,難以透徹理解 JS 對象原型構造的方式,由於他們老是不明白在 JS 語言中,爲何「函數名能夠做爲類名」的現象。而實質上,JS 這裏僅僅是借用了關鍵字 new,僅此而已;換句話說,ECMAScript 徹底能夠用其它 非new 表達式來用調用構造器建立對象。
在 ECMAScript 中,每一個由構造器建立的對象擁有一個指向構造器 prototype 屬性值的 隱式引用(implicit reference),這個引用稱之爲 原型(prototype)。進一步,每一個原型能夠擁有指向本身原型的 隱式引用(即該原型的原型),如此下去,這就是所謂的 原型鏈(prototype chain) (參考資源)。在具體的語言實現中,每一個對象都有一個 __proto__ 屬性來實現對原型的 隱式引用。
有了 原型鏈,即可以定義一種所謂的 屬性隱藏機制,並經過這種機制實現繼承。ECMAScript 規定,當要給某個對象的屬性賦值時,解釋器會查找該對象原型鏈中第一個含有該屬性的對象(注:原型自己就是一個對象,那麼原型鏈即爲一組對象的鏈。對象的原型鏈中的第一個對象是該對象自己)進行賦值。反之,若是要獲取某個對象屬性的值,解釋器天然是返回該對象原型鏈中首先具備該屬性的對象屬性值。
若是您已對原型、函數構造器、閉包和基於上下文的 this 有了充分的理解,那麼理解 Simple Inheritance 的實現原理也並不是至關困難。從本質上講,var Person = Class.extend(...)該語句中,左邊的 Person 其實是得到了由 Class 調用 extend 方法返回的一個構造器,也即一個 function 對象的引用。順着這個思路,咱們繼續介紹 Simple Inheritance 是如何作到這一點,進而實現了由原型繼承方式到類式繼承方式的轉換的。
到此爲止,若是您任然對 JavaScript 面向對象持懷疑態度,那麼這個懷疑必定是,JavaScript 沒有實現面向對象中的信息隱藏,即私有和公有。與其餘類式面向對象那樣顯式地聲明私有公有成員的方式不一樣,JavaScript 的信息隱藏就是靠閉包實現的。
JavaScript 必須依賴閉包實現信息隱藏,是由其函數式語言特性所決定的。本文不會對函數式語言和閉包這兩個話題展開討論,正如上文默認您理解 JavaScript 中基於上下文的 this 同樣。關於 JavaScript 中實現信息隱藏,Douglas Crockford在《 Private members in JavaScript 》(參考資源)一文中有更權威和詳細的介紹。
JavaScript 被認爲是世界上最受誤解的編程語言,由於它身披 c 語言家族的外衣,表現的倒是 LISP 風格的函數式語言特性;沒有類,卻實也完全實現了面向對象。要對這門語言有透徹的理解,就必須扒開其 c 語言的外衣,重新回到函數式編程的角度,同時摒棄原有類的面向對象概念去學習領悟它。隨着近些年來 Web 應用的普及和 JS 語言自身的長足發展,特別是後臺 JS 引擎的出現 ( 如基於 V8 的 NodeJS 等 ),能夠預見,原來只是做爲玩具編寫頁面效果的 JS 將得到更廣闊發展天地。這樣的發展趨勢,也對 JS 程序員提出了更高要求。只有完全領悟了這門語言,纔有可能在大型的 JS 項目中發揮她的威力。