介紹
在本篇文章,我們考慮在ECMAScript中的物件導向程式設計的各個方面(雖然以前在許多文章中已經討論過這個主題)。我們將更多地從理論方面看這些問題。 特別是,我們會考慮物件的創建演算法,物件(包括基本關係 - 繼承)之間的關係是如何,也可以在討論中使用(我希望將消除之前對於JavaScript中OOP的一些概念歧義)。
英文原文:http://dmitrysoshnikov.com/ecmascript/chapter-7-1-oop-general-theory/
概論、典範與思想
在進行ECMAScript中的OOP技術分析之前,我們有必要掌握一些OOP基本的特徵,並澄清概論中的主要概念。
ECMAScript支援包括結構化、物件導向、函數式、命令式等多種編程方式,某些情況下還支援面向方面編程;但本文是討論物件導向編程,所以來給出ECMAScript中物件導向編程的定義:
ECMAScript是基於原型實現的物件導向程式語言。
基於原型的OOP和基於靜態類別的方式直接有許多差異。 讓我們一起來看看他們直接詳細的差異。
基於類別特性和基於原型
注意,在前面一句很重要的一點已經指出的那樣-完全基於靜態類別。 隨著“靜態”一詞,我們了解靜態物件和靜態類,強類型(雖然不是必需的)。
關於這種情況,許多論壇上的文檔都有強調這是他們反對將在JavaScript裡將“類與原型”進行比較的主要原因,儘管他們在實現上的有所不同(例如基於動態類的Python和Ruby)不是太反對的重點(某些條件寫,儘管思想上有一定不同,但JavaScript沒有變得那麼另類),但他們反對的重點是靜態類和動態原型(statics classes vs. dynamics prototypes ),確切地說,一個靜態類別(例如:C ,JAVA)和他的屬下及方法定義的機制可以讓我們看到它和基於原型實現的準確區別。
但是,讓我們來一個一個列舉一下。 讓我們考慮一般則和這些範式的主要概念。
基於靜態類別
在基於類別的模型中,有個關於類別和實例的概念。 類別的實例也常被命名為物件或範例 。
類別與物件
類別代表了一個實例(也就是物件)的抽象。在這方面有點像數學,但我們一把稱之為類型(type)或分類(classification)。
例如(這裡和下面的例子都是偽代碼):
層次繼承
為了提高程式碼重用,類別可以從一個擴展為另一個,在加上額外的資訊。 這種機制稱為(分層)繼承 。
在類別的實例上呼叫方的時候,通常會現在原生類本書就查找該方法,如果沒找到就到直接父類去查找,如果還沒找到,就到父類的父類去查找(例如嚴格的繼承鏈上),如果查到繼承的頂部還沒查到,那結果就是:該物件沒有類似的行為,也沒辦法取得結果。
基於類別的關鍵概念
因此,我們有以下關鍵概念:
1.在建立一個物件之前,必須聲明類,首先有必要界定其類
2.因此,該物件將由抽象成自身「象形和相似性」(結構和行為)的類別裡創建
3.方法是通過了嚴格的,直接的,一成不變的繼承鏈來處理
4.子類別包含了繼承鏈中所有的屬性(即使其中的某些屬性是子類別不需要的);
5.建立類別實例,類別不能(因為靜態模型)來改變其實例的特徵(屬性或方法);
6.實例(因為嚴格的靜態模型)除了有該實例所對應類別裡聲明的行為和屬性以外,是不能額外的行為或屬性的。
讓我們來看看在JavaScript裡如何取代OOP模型,也就是我們所建議的基於原型的OOP。
基於原型
這裡的基本概念是動態可變物件。轉換(完整轉換,不僅包括值,還包括特性)和動態語言有直接關係。下面這樣的物件可以獨立儲存他們所有的特性(屬性,方法)而不需要的類別。
此外,由於動態的,他們可以很容易地改變(添加,刪除,修改)自己的特性:
也就是說,在賦值的時候,如果某些特性不存在,則創建它並且將賦值與它進行初始化,如果它存在,就只是更新。
在這種情況下,程式碼重用不是透過擴展類別來實現的,(請注意,我們沒有說類別沒辦法改變,因為這裡根本沒有類別的概念),而是透過原型來實現的。
原型是一個對象,它是用來作為其他對象的原始copy,或者如果一些對象沒有自己的必要特性,原型可以作為這些對象的一個委託而當成輔助對象。
基於委託
任何物件都可以用來作為另一個物件的原型對象,因為物件可以輕鬆地在運行時改變它的原型動態。
注意,目前我們正在考慮的是概論而不是具體實現,當我們在ECMAScript中討論具體實現時,我們將看到他們自身的一些特點。
例(偽代碼):
這個例子展示了原型作為輔助物件屬性的重要功能和機制,就像是要自己的屬性一下,和自身屬性相比,這些屬性是委託屬性。這個機制被稱為委託,並且基於它的原型模型是一個委託的原型(或基於委託的原型 ) 。引用的機制在這裡稱為發送訊息到物件上,如果這個物件得不到回應就會委託給原型來尋找(要求它嘗試回應訊息)。
在這種情況下的程式碼重用稱為基於委託的繼承或基於原型的繼承。由於任何物件可以當成原型,也就是說原型也可以有自己的原型。 這些原型連結在一起形成一個所謂的原型鏈。 鏈也像靜態類別中分層次的,但是它可以很容易地重新排列,改變層次和結構。
如果一個物件和它的原型鏈不能回應訊息發送,該物件可以啟動相應的系統訊號,可能是由原型鏈上其它的委託進行處理。
該系統訊號,在許多實作裡都是可用的,包括基於括動態類別的系統:Smalltalk中的#doesNotUnderstand,Ruby中的method_missing;Python中的__getattr__,PHP中的__call;和ECMAScript中的__noSuchMethod__實現,等等。
例(SpiderMonkey的ECMAScript的實作):
也就是說,基於靜態類別的實現,在不能回應訊息的情況下,得出的結論是:目前的物件不具有所要求的特性,但是如果嘗試從原型鏈裡獲取,依然可能得到結果,或該物件經過一系列變化以後擁有該特性。
關於ECMAScript,具體的實作就是:使用基於委託的原型。 然而,正如我們將從規範和實現中看到的,他們也有自身的特性。
Concatenative模型
老實說,有必要在說句話關於另外一種情況(盡快在ECMASCript沒有用到):當原型從其它對象複雜原來代替原生對象這種情況。這種情況代碼重用是在物件創建階段對一個物件的真正複製(克隆)而不是委託。這種原型被稱為concatenative原型。複製物件所有原型的特性,可以進一步完全改變其屬性和方法,同樣作為原型可以改變自己(在基於委託的模型中,這個改變不會改變現有存在的物件行為,而是改變它的原型特性) 。 這種方法的優點是可以減少調度和委託的時間,而缺點是記憶體使用率搞。
Duck型
回來動態弱型別變化的對象,與基於靜態類別的模型相比,檢驗它是否可以做這些事和對像有什麼類型(類別)無關,而是是否能夠相應訊息有關(即在檢查以後是否有能力做它是必須的) 。
例如:
這就是所謂的Dock型別 。 也就是說,物體在check的時候可以透過自己的特性來識別,而不是物件在層次結構中的位置或他們屬於任何具體類型。
基於原型的關鍵概念
讓我們來看看這種方式的主要特點:
1.基本概念是物件
2.物件是完全動態可變的(理論上完全可以從一個型別轉換到另一個型別)
3.對象沒有描述自己的結構和行為的嚴格類,對像不需要類
4.物件沒有類別但可以有原型,他們如果不能回應訊息的話可以委託給原型
5.運作時隨時可以改變物件的原型;
6.在基於委託的模型中,改變原型的特點,將影響與該原型相關的所有物件;
7.在concatenative原型模型中,原型是從其他物件克隆的原始副本,並進一步成為完全獨立的副本原件,原型特性的變換不會影響從它克隆的對象
8.如果不能回應訊息,它的呼叫者可以採取額外的措施(例如,改變調度)
9.物件的失敗可以不是由它們的層次和所屬哪個類別來決定,而是由當前特性來決定
不過,還有一個模型,我們也應該考慮。
基於動態類別
我們認為,在上面例子裡展示的區別「類別VS原型」在這個基於動態類別的模型中不是那麼重要,(尤其是如果原型鍊是不變的,為更準確區分,還是有必要考慮一個靜態類別)。 作為例子,它也可以使用Python或Ruby(或其他類似的語言)。 這些語言都使用基於動態類別的範式。 然而,在某些方面,我們是可以看到基於原型實現的某些功能。
在下面例子中,我們可以看到僅僅是基於委託的原型,我們可以放大一個類別(原型),從而影響到所有與這個類別相關的對象,我們也可以在運行時動態地改變這個對象的類別(為委託提供一個新物件)等等。
Ruby中的實作也是類似的:也使用了完全動態的類別(順便說一下在目前版本的Python中,與Ruby和ECMAScript的對比,放大類別(原型)不行的),我們可以徹底改變對象(或類別)的特性(在類別上新增方法/屬性,而這些變化會影響已經存在的物件),但是,它不能的動態改變一個物件的類別。
但是,這篇文章不是專門針對Python和Ruby的,因此我們不多說了,我們來繼續討論ECMAScript本身。
但在此之前,我們還得再看一下在一些OOP裡有的“語法糖”,因為很多之前關於JavaScript的文章往往會文這些問題。
本節唯一需要注意的錯誤句子是:「JavaScript不是類,它有原型,可以取代類別」。 非常有必要知道並非所有基於類別的實作都是完全不一樣的,即便我們可能會說“JavaScript是不同的”,但也有必要考慮(除了“類別”的概念)還有其他相關的特性呢。
各種OOP實現的其它特性
本節我們簡單介紹一下其它特性和各種OOP實作中關於程式碼重用的方式,也包括ECMAScript中的OOP實作。 原因是,先前出現的關於JavaScript中關於OOP的實作是有一些習慣性的思考限制,唯一主要的要求是,應該在技術上和思想上加以證明。不能說沒發現和其它OOP實作裡的語法糖功能,就草率認為JavaScript不是不是純粹的OOP語言,這是不對滴。
多型
在ECMAScript中物件有幾種意義的多態性。
例如,一個函數可以應用於不同的對象,就像原生對象的特性(因為這個值在進入執行上下文時決定的):
所謂函數定義時的參數多型性也就等價於所有資料型別,只不過接受多態性參數(例如陣列的.sort排序法和它的參數-多態的排序功能)。順便說一下,上面的例子也可以被視為是一種參數多態性。
原型裡方法可以被定義為空,所有建立的物件應重新定義(實作)該方法(即「一個介面(簽章),多個實作」)。
多態性與我們上面提到的Duck類型是有關的:即物件的類型和在層次結構中的位置不是那麼重要,但如果它有所有必要的特徵,它可以很容易地接受(即通用介面很重要,實作則可以多種多樣)。
封裝
關於封裝,往往會有錯誤的看法。本節我們討論一些OOP實現裡的語法糖——也就是眾所周知的修飾符:在這種情況下,我們將討論一些OOP實現便捷的“糖” -眾所周知的修飾符:private,protected和public(或稱為物件的存取等級或存取修飾符)。
在這裡我要提醒一下封裝的主要目的:封裝是一個抽象的增加,而不是選拔個直接往你的類別裡寫入一些東西的隱藏「惡意駭客」。
這是一個很大的錯誤:為了隱藏使用隱藏。
存取等級(private,protected和public),為了方便程式設計在許多物件導向裡都已經實現了(真的是很方便的語法糖),更抽像地描述和建構系統。
這些可以在一些實作裡看出(如已經提到的Python和Ruby)。一方面(在Python中),這些__private _protected屬性(透過底線這個命名規範),從外部無法存取。 另一方面,Python可以透過特殊的規則從外部存取(_ClassName__field_name)。
在Ruby裡:一方面有能力來定義private和protected的特性,另一方面,也有特殊的方法( 例如instance_variable_get,instance_variable_set,send等)取得封裝的資料。
最主要的原因是,程式設計師自己想要取得的封裝(請注意,我特別不使用「隱藏」)的資料。 如果這些資料會以某種方式不正確地更改或有任何錯誤,則全部責任都是程式設計師,但不是簡單的「拼字錯誤」或「隨便改變某些欄位」。 但如果這種情況很頻繁,那就是很不好的程式設計習慣和風格 ,因為通常值用公共的API來和物件「交談」。
重複一下,封裝的基本目的是一個從輔助資料的使用者中抽象化出來,而不是一個防止駭客隱藏資料。 更嚴重的,封裝不是用private修飾資料而達到軟體安全的目的。
封裝輔助物件(局部),我們用最小的代價、在地化和預測性變化來問為公共介面的行為變化提供可行性,這也正是封裝的目的。
另外setter方法的重要目的是抽象複雜的計算。 例如,element.innerHTML這個setter——抽象的語句——“現在這個元素內的HTML是如下內容”,而在 innerHTML屬性的setter函數將難以計算和檢查。 在這種情況下,問題大多涉及抽象 ,但封裝也會發生。
封裝的概念不只與OOP相關。 例如,它可以是一個簡單的功能,只封裝了各種計算,使得其抽象(沒有必要讓用戶知道,例如函數Math.round(... ...)是如何實現的,用戶只是簡單地調用它)。 它是一種封裝,注意,我沒有說他是「private, protected和public」。
ECMAScript規範的目前版本,沒有定義private, protected和public修飾符。
然而,在實踐中是有可能看到有些東西被命名為「模仿JS封裝」。 一般該上下文的目的是(作為一個規則,構造函數本身)使用。 不幸的是,經常實施這種“模仿”,程式設計師可以產生偽絕對非抽象的實體設定“getter / setter方法”(我再說一遍,它是錯誤的):
因此,每個人都明白,對於每個創建的對象,對於的getA/setA方法也創建了,這也是導致內存增加的原因(和原型定義相比)。 雖然,理論上第一種情況下可以對物件進行最佳化。
另外,一些JavaScript的文章經常提到「私有方法」的概念,注意:ECMA-262-3標準裡沒有定義任何關於「私有方法」的概念。
但是,某些情況下它可以在建構函式中創建,因為JS是意識形態的語言-物件是完全可變的並且有獨特的特性(在建構函式裡某些條件下,有些物件可以得到額外的方法,而其他則不行)。
此外,在JavaScript裡,如果還是把封裝曲解成為了不讓惡意黑客在某些自動寫入某些值的一種理解來代替使用setter方法,那所謂的“隱藏(hidden)”和“私有(private)”其實沒有很“隱藏”,,有些實作可以透過呼叫上下文到eval函數(可以在SpiderMonkey1.7上測試)在相關的作用域鏈(以及對應的所有變數物件)上取得值)。
或者,在實作中允許直接進入活動物件(例如Rhino),透過存取該物件的對應屬性可以改變內部變數的值:
var _myPrivateData = 'testString';
對於括號括住執行上下文是經常使用,但對於真正的輔助數據,則和物件沒有直接關聯,只是方便從外部的API抽象化:
多重繼承
多重繼承是代碼重用改進的一個很方便的語法糖(如果我們一次能繼承一個類,為什麼不能一次繼承10個?)。 然而由於多重繼承有一些不足,才導致在實作中沒有流行起來。
ECMAScript不支援多重繼承(即只有一個對象,可以用來作為一個直接原型),雖然其祖先自程式語言有這樣的能力。 但在某些實作中(如SpiderMonkey)使用__noSuchMethod__可以用來管理調度和委託來取代原型鏈。
Mixins
Mixins是程式碼重用的一種便捷方式。 Mixins已建議作為多重繼承的替代品。 這些獨立的元素都可以與任何物件進行混合以擴展它們的功能(因此物件也可以混合多個Mixins)。 ECMA-262-3規範沒有定義「Mixins」的概念,但根據Mixins定義以及ECMAScript擁有動態可變對象,所以使用Mixins簡單地擴充特性是沒有障礙的。
典型的例子:
請注意,我採取在ECMA-262-3中被提及過的引號中的這些定義(“mixin”,“mix”),在規範裡並沒有這樣的概念,而且不是mix而是常用的透過新特性去擴展物件。 (Ruby中mixins的概念是官方定義的,mixin創建了一個包含模組的一個引用來代替簡單複製該模組的所有屬性到另一個模組上——事實上是:為委託創建一個額外的物件(原型) )。
Traits
Traits和mixins的概念相似,但它有很多功能(根據定義,因為可以應用mixins所以不能包含狀態,因為它有可能導致命名衝突)。 根據ECMAScript說明Traits和mixins遵循相同的原則,所以這個規範並沒有定義「Traits」的概念。
介面
在一些OOP中實作的介面和mixins及traits類似。然而,與mixins及traits相比,介面強制實作類別必須實作其方法簽章的行為。
介面完全可以被視為抽象類別。不過與抽象類別相比(抽象類別裡的方法可以只實現一部分,另外一部分依然定義為簽名),繼承只能是單繼承基類,但可以繼承多個接口,節約這個原因,可以接口(多個混合)可以看做是多繼承的替代方案。
ECMA-262-3標準既沒有定義「介面」的概念,也沒有定義「抽象類別」的概念。 然而,作為模仿,它是可以由「空」的方法(或空方法中拋出異常,告訴開發人員這個方法需要被實現)的物件來實現。
物件組合
物件組合也是一個動態程式碼重用技術之一。 物件組合不同於高靈活性的繼承,它實現了一個動態可變的委託。而這,也是基於委託原型的基本。 除了動態可變原型,該物件可以為委託聚合物件(建立一個組合作為結果-聚合 ),並進一步傳送訊息到物件上,委託到該委託上。這可以兩個以上的委託,因為它的動態特性決定它可以在運作時改變。
已經提到的__noSuchMethod__例子是這樣,但也讓我們展示瞭如何明確地使用委託:
例如:
這種物件關係稱為“has-a”,而整合是“is-a“的關係。
由於顯示組合的缺乏(與繼承相比的靈活性),增加中間代碼也是可以的。
AOP特性
作為面向方面的功能,可以使用function decorators。 ECMA-262-3規格沒有明確定義的「function decorators」的概念(和Python相對,這個字是在Python官方定義了)。 不過,擁有函數式參數的函數在某些方面是可以裝飾和激活的(透過應用所謂的建議):
最簡單的裝飾者範例:
結論
在這篇文章,我們理清了OOP的概論(我希望這些資料已經對你有用了),下一章節我們將繼續面向對象編程之ECMAScript的實現 。