對象的失敗可以不由它們的層次和所屬哪個類來決定,而是由當前特性來決定不過,還有一個模型,我們也應該考慮。
基於動態類我們認為,在上面例子裡展示的區別“類VS原型 ”在這個基於動態類的模型中不是那麼重要,(尤其是如果原型鏈是不變的,為更準確區分,還是有必要考慮一個靜態類)。 作為例子,它也可以使用Python或Ruby(或其他類似的語言)。 這些語言都使用基於動態類的範式。 然而,在某些方面,我們是可以看到基於原型實現的某些功能。
在下面例子中,我們可以看到僅僅是基於委託的原型,我們可以放大一個類(原型),從而影響到所有與這個類相關的對象,我們也可以在運行時動態地改變這個對象的類(為委託提供一個新對象)等等。
# Python class A(object): def __init__(self, a): self.a = a def square(self): return self.a * self.a a = A(10) # 建立執行個體print(a.a) # 10 A.b = 20 # 為類提供一個新屬性print(a.b) # 20 – 可以在a執行個體裡訪問到 a.b = 30 # 建立a自身的屬性print(a.b) # 30 del a.b # 刪除自身的屬性print(a.b) # 20 - 再次從類裡擷取(原型) # 就像基於原型的模型# 可以在運行時改變對象的原型 class B(object): # 空類B pass b = B() # B的執行個體 b.__class__ = A # 動態改變類(原型) b.a = 10 # 建立新屬性print(b.square()) # 100 - A類的方法這時候可用 # 可以顯示刪除類上的引用del Adel B # 但對象依然有隱式的引用,並且這些方法依然可用print(b.square()) # 100 # 但這時候不能再改變類了# 這是實現的特性b.__class__ = dict # error
Ruby中的實現也是類似的:也使用了完全動態類(順便說一下在目前的版本的Python中,與Ruby和ECMAScript的對比,放大類(原型)不行的),我們可以徹底改變對象(或類)的特性(在類上添加方法/屬性,而這些變化會影響已經存在的對象),但是,它不能的動態改變一個對象的類。
但是,這篇文章不是專門針對Python和Ruby的,因此我們不多說了,我們來繼續討論ECMAScript本身。
但在此之前,我們還得再看一下在一些OOP裡有的“文法糖”,因為很多之前關於JavaScript的文章往往會文這些問題。
本節唯一需要注意的錯誤句子是:“JavaScript不是類,它有原型,可以代替類”。 非常有必要知道並非所有基於類的實現都是完全不一樣的,即便我們可能會說“JavaScript是不同的”,但也有必要考慮(除了“類”的概念)還有其他相關的特性呢。
各種OOP實現的其它特性本節我們簡要介紹一下其它特性和各種OOP實現中關於代碼重用的方式,也包括ECMAScript中的OOP實現。 原因是,之前出現的關於JavaScript中關於OOP的實現是有一些習慣性的思維限制,唯一主要的要求是,應該在技術上和思想上加以證明。不能說沒發現和其它OOP實現裡的文法糖功能,就草率認為JavaScript不是不是純粹的OOP語言,這是不對滴。
多態在ECMAScript中對象有幾種含義的多態性。
例如,一個函數可以應用於不同的對象,就像原生對象的特性(因為這個值在進入執行內容時確定的):
function test() { alert([this.a, this.b]);} test.call({a: 10, b: 20}); // 10, 20test.call({a: 100, b: 200}); // 100, 200 var a = 1;var b = 2; test(); // 1, 2不過,也有例外:Date.prototype.getTime()方法,根據標準這個值總是應該有一個日期對象,否則就會拋出異常。
alert(Date.prototype.getTime.call(new Date())); // timealert(Date.prototype.getTime.call(new String(''))); // TypeError所謂函數定義時的多重參數變形性也就等價於所有資料類型,只不過接受多態性參數(例如數組的.sort排序方法和它的參數——多態的排序功能)。順便說一下,上面的例子也可以被視為是一種多重參數變形性。
原型裡方法可以被定義為空白,所有建立的對象應重新定義(實現)該方法(即“一個介面(簽名),多個實現”)。
多態性和我們上面提到的Duck類型是有關的:即對象的類型和在階層中的位置不是那麼重要,但如果它有所有必要的特徵,它可以很容易地接受(即通用介面很重要,實現則可以多種多樣)。
封裝關於封裝,往往會有錯誤的看法。本節我們討論一下一些OOP實現裡的文法糖——也就是眾所周知的修飾符:在這種情況下,我們將討論一些OOP實現便捷的“糖” -眾所周知的修飾符:private,protected和public(或者稱為對象的存取層級或存取修飾詞)。
在這裡我要提醒一下封裝的主要目的:封裝是一個抽象的增加,而不是選拔個直接往你的類裡寫入一些東西的隱藏“惡意駭客”。
這是一個很大的錯誤:為了隱藏使用隱藏。
存取層級(private,protected和public),為了方便編程在很多物件導向裡都已經實現了(真的是非常方便的文法糖),更抽象地描述和構建系統。
這些可以在一些實現裡看出(如已經提到的Python和Ruby)。一方面(在Python中),這些__private _protected屬性(通過底線這個命名規範),從外部不可訪問。 另一方面,Python可以通過特殊的規則從外部存取(_ClassName__field_name)。
class A(object): def __init__(self): self.public = 10 self.__private = 20 def get_private(self): return self.__private # outside: a = A() # A的執行個體 print(a.public) # OK, 30print(a.get_private()) # OK, 20print(a.__private) # 失敗,因為只能在A裡可用 # 但在Python裡,可以通過特殊規則來訪問 print(a._A__private) # OK, 20
在Ruby裡:一方面有能力來定義private和protected的特性,另一方面,也有特殊的方法( 例如instance_variable_get,instance_variable_set,send等)擷取封裝的資料。
class A def initialize @a = 10 end def public_method private_method(20) end private def private_method(b) return @a + b end end a = A.new # 新執行個體 a.public_method # OK, 30 a.a # 失敗, @a - 是私人的執行個體變數 # private_method是私人的,只能在A類裡訪問 a.private_method # 錯誤 # 但是有特殊的中繼資料方法名,可以擷取到資料 a.send(:private_method, 20) # OK, 30a.instance_variable_get(:@a) # OK, 10
最主要的原因是,程式員自己想要獲得的封裝(請注意,我特別不使用“隱藏”)的資料。 如果這些資料會以某種方式不正確地更改或有任何錯誤,則全部責任都是程式員,但不是簡單的“拼字錯誤”或“隨便改變某些欄位”。 但如果這種情況很頻繁,那就是很不好的編程習慣和風格 ,因為通常值用公用的API來和對象“交談”。
重複一下,封裝的基本目的是一個從輔助資料的使用者中抽象出來,而不是一個防止駭客隱藏資料。 更嚴重的,封裝不是用private修飾資料而達到軟體安全的目的。
封裝輔助對象(局部),我們用最小的代價、本地化和預測性變化來問為公用介面的行為變化提供可行性,這也正是封裝的目的。
另外setter方法??的重要目的是抽象複雜的計算。 例如,element.innerHTML這個setter——抽象的語句——“現在這個元素內的HTML是如下內容”,而在 innerHTML 屬性的setter函數將難以計算和檢查。 在這種情況下,問題大多涉及到抽象 ,但封裝也會發生。
封裝的概念不僅僅只與OOP相關。 例如,它可以是一個簡單的功能,只封裝了各種計算,使得其抽象(沒有必要讓使用者知道,例如函數Math.round(... ...)是如何?的,使用者只是簡單地調用它)。 它是一種封裝,注意,我沒有說他是“private, protected和public”。
ECMAScript規範的目前的版本,沒有定義private, protected和public修飾符。
然而,在實踐中是有可能看到有些東西被命名為“模仿JS封裝”。 一般該內容相關的目的是(作為一個規則,建構函式本身)使用。 不幸的是,經常實施這種“模仿”,程式員可以產生偽絕對非抽象的實體設定“getter / setter方法”(我再說一遍,它是錯誤的):
function A() { var _a; // private a this.getA = function _getA() { return _a; }; this.setA = function _setA(a) { _a = a; }; } var a = new A(); a.setA(10);alert(a._a); // undefined, privatealert(a.getA()); // 10因此,每個人都明白,對於每個建立的對象,對於的getA/setA方法也建立了,這也是導致記憶體增加的原因(和原型定義相比)。 雖然,理論上第一種情況下可以對對象進行最佳化。
另外,一些JavaScript的文章經常提到“私人方法”的概念,注意:ECMA-262-3標準裡沒有定義任何關於“私人方法”的概念。
但是,某些情況下它可以在建構函式中建立,因為JS是意識形態的語言——對象是完全可變的並且有獨特的特性(在建構函式裡某些條件下,有些對象可以得到額外的方法,而其他則不行)。
此外,在JavaScript裡,如果還是把封裝曲解成為了不讓惡意駭客在某些自動寫入某些值的一種理解來代替使用setter方法,那所謂的“隱藏(hidden)”和“私人(private)”其實沒有很“隱藏”,,有些實現可以通過調用上下文到eval函數(可以在SpiderMonkey1.7上測試)在相關的範圍鏈(以及相應的所有變數對象)上擷取值)。
eval('_a = 100', a.getA); // 或者a.setA,因為_a兩個方法的[[Scope]]上a.getA(); // 100或者,在實現中允許直接進入使用中的物件(例如Rhino),通過訪問該對象的相應屬性可以改變內部變數的值:
// Rhinovar foo = (function () { var x = 10; // private return function () { print(x); };})();foo(); // 10foo.__parent__.x = 20;foo(); // 20有時,在JavaScript裡通過在變數前加底線來達到“private”和“protected”資料的目的(但與Python相比,這裡只是命名規範):
var _myPrivateData = 'testString';
對於括弧括住執行內容是經常使用,但對於真正的輔助資料,則和對象沒有直接關聯,只是方便從外部的API抽象出來:
(function () { // 初始化上下文 })();多重繼承多繼承是代碼重用改進的一個很方便的文法糖(如果我們一次能繼承一個類,為什麼不能一次繼承10個?)。 然而由於多重繼承有一些不足,才導致在實現中沒有流行起來。
ECMAScript不支援多繼承(即只有一個對象,可以用來作為一個直接原型),雖然其祖先自程式設計語言有這樣的能力。 但在某些實現中(如SpiderMonkey)使用__noSuchMethod__可以用於管理調度和委託來替代原型鏈。
MixinsMixins是代碼重用的一種便捷方式。 Mixins已建議作為多重繼承的替代品。 這些獨立的元素都可以與任何對象進行混合來擴充它們的功能(因此對象也可以混合多個Mixins)。 ECMA-262-3規範沒有定義“Mixins”的概念,但根據Mixins定義以及ECMAScript擁有動態可變對象,所以使用Mixins簡單地擴充特性是沒有障礙的。
典型的例子:
// helper for augmentationObject.extend = function (destination, source) { for (property in source) if (source.hasOwnProperty(property)) { destination[property] = source[property]; } return destination;}; var X = {a: 10, b: 20};var Y = {c: 30, d: 40}; Object.extend(X, Y); // mix Y into Xalert([X.a, X.b, X.c, X.d]); 10, 20, 30, 40請注意,我採取在ECMA-262-3中被提及過的引號中的這些定義(“mixin”,“mix”),在規範裡並沒有這樣的概念,而且不是mix而是常用的通過新特性去擴充項物件。(Ruby中mixins的概念是官方定義的,mixin建立了一個包含模組的一個引用來代替簡單複製該模組的所有屬性到另外一個模組上——事實上是:為委託建立一個額外的對象(原型))。
TraitsTraits和mixins的概念相似,但它有很多功能(根據定義,因為可以應用mixins所以不能包含狀態,因為它有可能導致命名衝突)。 根據ECMAScript說明Traits和mixins遵循同樣的原則,所以該規範沒有定義“Traits”的概念。
介面在一些OOP中實現的介面和mixins及traits類似。然而,與mixins及traits相比,介面強制實作類別必須實現其方法簽名的行為。
介面完全可以被視為抽象類別。不過與抽象類別相比(抽象類別裡的方法可以只實現一部分,另外一部分依然定義為簽名),繼承只能是單繼承基類,但可以繼承多個介面,節約這個原因,可以介面(多個混合)可以看做是多繼承的替代方案。
ECMA-262-3標準既沒有定義“介面”的概念,也沒有定義“抽象類別”的概念。 然而,作為模仿,它是可以由“空”的方法(或空方法中拋出異常,告訴開發人員這個方法需要被實現)的對象來實現。
對象組合對象組合也是一個動態代碼重用技術之一。 對象組合不同於高靈活性的繼承,它實現了一個動態可變的委託。而這,也是基於委託原型的基本。 除了動態可變原型,該對象可以為委託彙總對象(建立一個組合作為結果——彙總 ),並進一步發送訊息到對象上,委託到該委託上。這可以兩個以上的委託,因為它的動態特性決定著它可以在運行時改變。
已經提到的__noSuchMethod__例子是這樣,但也讓我們展示了如何明確地使用委託:
例如:
var _delegate = { foo: function () { alert('_delegate.foo'); }}; var agregate = { delegate: _delegate, foo: function () { return this.delegate.foo.call(this); } }; agregate.foo(); // delegate.foo agregate.delegate = { foo: function () { alert('foo from new delegate'); }}; agregate.foo(); // foo from new delegate這種對象關係稱為“has-a”,而整合是“is-a“的關係。
由於顯示組合的缺乏(與繼承相比的靈活性),增加中間代碼也是可以的。
AOP特性作為面向方面的一個功能,可以使用function decorators。ECMA-262-3規格沒有明確定義的“function decorators”的概念(和Python相對,這個詞是在Python官方定義了)。 不過,擁有函數式參數的函數在某些方面是可以裝飾和啟用的(通過應用所謂的建議):
最簡單的裝飾者例子:
function checkDecorator(originalFunction) { return function () { if (fooBar != 'test') { alert('wrong parameter'); return false; } return originalFunction(); };} function test() { alert('test function');} var testWithCheck = checkDecorator(test);var fooBar = false; test(); // 'test function'testWithCheck(); // 'wrong parameter' fooBar = 'test';test(); // 'test function'testWithCheck(); // 'test function'結論在這篇文章,我們理清了OOP的概論(我希望這些資料已經對你有用了),下一章節我們將繼續物件導向編程之ECMAScript的實現 。