摘要:是完全的面向對象語言,它們通過類的形式組織函數和變量,使之不能脫離對象存在。而在基于原型的面向對象方式中,對象則是依靠構造器利用原型構造出來的。
JavaScript 函數式腳本語言特性以及其看似隨意的編寫風格,導致長期以來人們對這一門語言的誤解,即認為 JavaScript 不是一門面向對象的語言,或者只是部分具備一些面向對象的特征。本文將回歸面向對象本意,從對語言感悟的角度闡述為什么 JavaScript 是一門徹底的面向對象的語言,以及如何正確地使用這一特性。
前言當今 JavaScript 大行其道,各種應用對其依賴日深。web 程序員已逐漸習慣使用各種優秀的 JavaScript 框架快速開發 Web 應用,從而忽略了對原生 JavaScript 的學習和深入理解。所以,經常出現的情況是,很多做了多年 JS 開發的程序員對閉包、函數式編程、原型總是說不清道不明,即使使用了框架,其代碼組織也非常糟糕。這都是對原生 JavaScript 語言特性理解不夠的表現。要掌握好 JavaScript,首先一點是必須摒棄一些其他高級語言如 Java、C# 等類式面向對象思維的干擾,全面地從函數式語言的角度理解 JavaScript 原型式面向對象的特點。把握好這一點之后,才有可能進一步使用好這門語言。本文適合群體:使用過 JS 框架但對 JS 語言本質缺乏理解的程序員,具有 Java、C++ 等語言開發經驗,準備學習并使用 JavaScript 的程序員,以及一直對 JavaScript 是否面向對象模棱兩可,但希望知道真相的 JS 愛好者。
重新認識面向對象為了說明 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 指針的變化。正是這種特點更貼近 “萬物皆有所屬,宇宙乃萬物生存之根本”的自然觀點。在 程序清單 1中 window 便類似與宇宙的概念。
清單 1. 對象的上下文依賴
在接受了面向對象存在一種叫做基于原型實現的方式的事實之后,下面我們就可以來深入探討 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 規定,當要給某個對象的屬性賦值時,解釋器會查找該對象原型鏈中第一個含有該屬性的對象(注:原型本身就是一個對象,那么原型鏈即為一組對象的鏈。對象的原型鏈中的第一個對象是該對象本身)進行賦值。反之,如果要獲取某個對象屬性的值,解釋器自然是返回該對象原型鏈中首先具有該屬性的對象屬性值。
理解了原型鏈,那么將非常容易理解 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 是如何做到這一點,進而實現了由原型繼承方式到類式繼承方式的轉換的。
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 項目中發揮她的威力。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/86267.html
摘要:很多情況下,通常一個人類,即創建了一個具體的對象。對象就是數據,對象本身不包含方法。類是相似對象的描述,稱為類的定義,是該類對象的藍圖或原型。在中,對象通過對類的實體化形成的對象。一類的對象抽取出來。注意中,對象一定是通過類的實例化來的。 showImg(https://segmentfault.com/img/bVTJ3H?w=900&h=385); 馬上就要到七夕了,離年底老媽老爸...
摘要:很多情況下,通常一個人類,即創建了一個具體的對象。對象就是數據,對象本身不包含方法。類是相似對象的描述,稱為類的定義,是該類對象的藍圖或原型。在中,對象通過對類的實體化形成的對象。一類的對象抽取出來。注意中,對象一定是通過類的實例化來的。 showImg(https://segmentfault.com/img/bVTJ3H?w=900&h=385); 馬上就要到七夕了,離年底老媽老爸...
摘要:很多情況下,通常一個人類,即創建了一個具體的對象。對象就是數據,對象本身不包含方法。類是相似對象的描述,稱為類的定義,是該類對象的藍圖或原型。在中,對象通過對類的實體化形成的對象。一類的對象抽取出來。注意中,對象一定是通過類的實例化來的。 showImg(https://segmentfault.com/img/bVTJ3H?w=900&h=385); 馬上就要到七夕了,離年底老媽老爸...
摘要:用代碼可以這樣描述安全到達國外面向過程既然說了面向對象,那么與之對應的就是面向過程。小結在這篇文章中,介紹了什么是面向對象和面向過程,以及中對象的含義。 這是 javascript 面向對象版塊的第一篇文章,主要講解對面向對象思想的一個理解。先說說什么是對象,其實這個還真的不好說。我們可以把自己當成一個對象,或者過年的時候相親,找對象,那么你未來的老婆也是一個對象。我們就要一些屬性,比...
摘要:之前,本質上不能算是一門面向對象的編程語言,因為它對于封裝繼承多態這些面向對象語言的特點并沒有在語言層面上提供原生的支持。所以在中出現了等關鍵字,解決了面向對象中出現了問題。 ES6之前,javascript本質上不能算是一門面向對象的編程語言,因為它對于封裝、繼承、多態這些面向對象語言的特點并沒有在語言層面上提供原生的支持。但是,它引入了原型(prototype)的概念,可以讓我們以...
閱讀 3400·2021-10-08 10:15
閱讀 5440·2021-09-23 11:56
閱讀 1467·2019-08-30 15:55
閱讀 445·2019-08-29 16:05
閱讀 2725·2019-08-29 12:34
閱讀 2036·2019-08-29 12:18
閱讀 914·2019-08-26 12:02
閱讀 1650·2019-08-26 12:00