深刻理解JavaScript系列(17):面向對象編程之概論

介紹

在本篇文章,咱們考慮在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)。

例如(這裏和下面的例子都是僞代碼):

C = Class {a, b, c} // 類C, 包括特性a, b, c

實例的特色是:屬性(對象描述 )和方法(對象活動)。特性自己也可視爲對象:即屬性是否可寫的,可配置,可設置的(getter/setter)等。所以,對象存儲了狀態 (即在一個類中描述的全部屬性的具體值),類爲他們的實例定義了嚴格不變的結構(屬性)和嚴格不變的行爲(方法)。

C = Class {a, b, c, method1, method2}
 
c1 = {a: 10, b: 20, c: 30} // 類C是實例:對象с1
c2 = {a: 50, b: 60, c: 70} // 類C是實例:對象с2,擁有本身的狀態(也就是屬性值)

層次繼承

爲了提升代碼重用,類能夠從一個擴展爲另外一個,在加上額外的信息。 這種機制被稱爲(分層)繼承 。

D = Class extends C = {d, e} // {a, b, c, d, e}
d1 = {a: 10, b: 20, c: 30, d: 40, e: 50}

在類的實例上調用方的時候,一般會如今原生類本書就查找該方法,若是沒找到就到直接父類去查找,若是還沒找到,就到父類的父類去查找(例如嚴格的繼承鏈上),若是查到繼承的頂部還沒查到,那結果就是:該對象沒有相似的行爲,也沒辦法獲取結果。

d1.method1() // D.method1 (no) -> C.method1 (yes)
d1.method5() // D.method5 (no) -> C.method5 (no) -> no result

與在繼承裏方法不復制到一個子類相比,屬性老是被複雜到子類裏的。 咱們能夠看到子類D繼承自父類C類:屬性a,b,c是複製過去了,D的結構是{a, b, c, d, e} } 。然而,方法{method1, method2}是沒有複製過去,而是繼承過去的。 所以,也就是說若是一個很深層次的類有一些對象根本不須要的屬性的話,那子類也有擁有這些屬性。

基於類的關鍵概念

所以,咱們有以下關鍵概念:
建立一個對象以前,必須聲明類,首先有必要界定其類
所以,該對象將由抽象成自身「象形和類似性」(結構和行爲)的類裏建立
方法是經過了嚴格的,直接的,一成不變的繼承鏈來處理
子類包含了繼承鏈中全部的屬性(即便其中的某些屬性是子類不須要的);
建立類實例,類不能(由於靜態模型)來改變其實例的特徵(屬性或方法);
實例(由於嚴格的靜態模型)除了有該實例所對應類裏聲明的行爲和屬性之外,是不能額外的行爲或屬性的。

讓咱們看看在JavaScript裏如何替代OOP模型,也就是咱們所建議的基於原型的OOP。

基於原型

這裏的基本概念是動態可變對象。轉換(完整轉換,不只包括值,還包括特性)和動態語言有直接關係。下面這樣的對象能夠獨立存儲他們全部的特性(屬性,方法)而不須要的類。

object = {a: 10, b: 20, c: 30, method: fn};
object.a; // 10
object.c; // 30
object.method();

此外,因爲動態的,他們能夠很容易地改變(添加,刪除,修改)本身的特性:

object.method5 = function () {...}; // 添加新方法
object.d = 40; // 添加新屬性 "d"
delete object.c; // 刪除屬性 "с"
object.a = 100; // 修改屬性 "а"
 
// 結果是: object: {a: 100, b: 20, d: 40, method: fn, method5: fn};

也就是說,在賦值的時候,若是某些特性不存在,則建立它而且將賦值與它進行初始化,若是它存在,就只是更新。

在這種狀況下,代碼重用不是經過擴展類來實現的,(請注意,咱們沒有說類沒辦法改變,由於這裏根本沒有類的概念),而是經過原型來實現的。

原型是一個對象,它是用來做爲其餘對象的原始copy,或者若是一些對象沒有本身的必要特性,原型能夠做爲這些對象的一個委託而當成輔助對象。

基於委託

任何對象均可以被用來做爲另外一個對象的原型對象,由於對象能夠很容易地在運行時改變它的原型動態。

注意,目前咱們正在考慮的是概論而不是具體實現,當咱們在ECMAScript中討論具體實現時,咱們將看到他們自身的一些特色。

例(僞代碼):

x = {a: 10, b: 20};
y = {a: 40, c: 50};
y.[[Prototype]] = x; // x是y的原型
 
y.a; // 40, 自身特性
y.c; // 50, 也是自身特性
y.b; // 20 – 從原型中獲取: y.b (no) -> y.[[Prototype]].b (yes): 20
 
delete y.a; // 刪除自身的"а"
y.a; // 10 – 從原型中獲取
 
z = {a: 100, e: 50}
y.[[Prototype]] = z; // 將y的原型修改成z
y.a; // 100 – 從原型z中獲取
y.e // 50, 也是從從原型z中獲取
 
z.q = 200 // 添加新屬性到原型上
y.q // 修改也適用於y

這個例子展現了原型做爲輔助對象屬性的重要功能和機制,就像是要本身的屬性一下,和自身屬性相比,這些屬性是委託屬性。這個機制被稱爲委託,而且基於它的原型模型是一個委託的原型(或基於委託的原型 ) 。引用的機制在這裏稱爲發送信息到對象上,若是這個對象得不到響應就會委託給原型來查找(要求它嘗試響應消息)。

在這種狀況下的代碼重用被稱爲基於委託的繼承或基於原型的繼承。因爲任何對象能夠當成原型,也就是說原型也能夠有本身的原型。 這些原型鏈接在一塊兒造成一個所謂的原型鏈。 鏈也像靜態類中分層次的,可是它能夠很容易地從新排列,改變層次和結構。

x = {a: 10}
 
y = {b: 20}
y.[[Prototype]] = x
 
z = {c: 30}
z.[[Prototype]] = y
 
z.a // 10
 
// z.a 在原型鏈裏查到:
// z.a (no) ->
// z.[[Prototype]].a (no) ->
// z.[[Prototype]].[[Prototype]].a (yes): 10

若是一個對象和它的原型鏈不能響應消息發送,該對象能夠激活相應的系統信號,多是由原型鏈上其它的委託進行處理。

該系統信號,在許多實現裏都是可用的,包括基於括動態類的系統:Smalltalk中的#doesNotUnderstand,Ruby中的​​method_missing;Python中的__getattr__,PHP中的__call;和ECMAScript中的__noSuchMethod__實現,等等。

例(SpiderMonkey的ECMAScript的實現):

var object = {
 
  // catch住不能響應消息的系統信號
  __noSuchMethod__: function (name, args) {
    alert([name, args]);
    if (name == 'test') {
      return '.test() method is handled';
    }
    return delegate[name].apply(this, args);
  }
 
};
 
var delegate = {
  square: function (a) {
    return a * a;
  }
};
 
alert(object.square(10)); // 100
alert(object.test()); // .test() method is handled

也就是說,基於靜態類的實現,在不能響應消息的狀況下,得出的結論是:目前的對象不具備所要求的特性,可是若是嘗試從原型鏈裏獲取,依然可能獲得結果,或者該對象通過一系列變化之後擁有該特性。

關於ECMAScript,具體的實現就是:使用基於委託的原型。 然而,正如咱們將從規範和實現裏看到的,他們也有自身的特性。

Concatenative模型

老實說,有必要在說句話關於另一種狀況(儘快在ECMASCript沒有用到):當原型從其它對象複雜原來代替原生對象這種狀況。這種狀況代碼重用是在對象建立階段對一個對象的真正複製(克隆)而不是委託。這種原型被稱爲concatenative原型。複製對象全部原型的特性,能夠進一步徹底改變其屬性和方法,一樣做爲原型能夠改變本身(在基於委託的模型中,這個改變不會改變現有存在的對象行爲,而是改變它的原型特性)。 這種方法的優勢是能夠減小調度和委託的時間,而缺點是內存使用率搞。

Duck類型

回來動態弱類型變化的對象,與基於靜態類的模型相比,檢驗它是否能夠作這些事和對象有什麼類型(類)無關,而是是否可以相應消息有關(即在檢查之後是否有能力作它是必須的) 。

例如:

// 在基於靜態來的模型裏
if (object instanceof SomeClass) {
  // 一些行爲是運行的
}
 
// 在動態實現裏
// 對象在此時是什麼類型並不重要
// 由於突變、類型、特性能夠自由重複的轉變。
// 重要的對象是否能夠響應test消息 
if (isFunction(object.test)) // ECMAScript
 
if object.respond_to?(:test) // Ruby
 
if hasattr(object, 'test'): // Python

這就是所謂的Dock類型 。 也就是說,物體在check的時候能夠經過本身的特性來識別,而不是對象在層次結構中的位置或他們屬於任何具體類型。

基於原型的關鍵概念

讓咱們來看一下這種方式的主要特色:
基本概念是對象
對象是徹底動態可變的(理論上徹底能夠從一個類型轉化到另外一個類型)
對象沒有描述本身的結構和行爲的嚴格類,對象不須要類
對象沒有類類但能夠能夠有原型,他們若是不能響應消息的話能夠委託給原型
在運行時隨時能夠改變對象的原型;
在基於委託的模型中,改變原型的特色,將影響到與該原型相關的全部對象;
在concatenative原型模型中,原型是從其餘對象克隆的原始副本,並進一步成爲徹底獨立的副本原件,原型特性的變換不會影響從它克隆的對象
若是不能響應消息,它的調用者能夠採起額外的措施(例如,改變調度)
對象的失敗能夠不禁它們的層次和所屬哪一個類來決定,而是由當前特性來決定

不過,還有一個模型,咱們也應該考慮。

基於動態類

咱們認爲,在上面例子裏展現的區別「類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 A
del 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, 20
test.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())); // time
alert(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, 30
print(a.get_private()) # OK, 20
print(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, 30
a.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, "private"
alert(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),經過訪問該對象的相應屬性能夠改變內部變量的值:

// Rhino
var foo = (function () {
  var x = 10; // "private"
  return function () {
    print(x);
  };
})();
foo(); // 10
foo.__parent__.x = 20;
foo(); // 20

有時,在JavaScript裏經過在變量前加下劃線來達到「private」和「protected」數據的目的(但與Python相比,這裏只是命名規範):

var _myPrivateData = 'testString';

對於括號括住執行上下文是常用,但對於真正的輔助數據,則和對象沒有直接關聯,只是方便從外部的API抽象出來:

(function () {
 
  // 初始化上下文
 
})();

多重繼承

多繼承是代碼重用改進的一個很方便的語法糖(若是咱們一次能繼承一個類,爲何不能一次繼承10個?)。 然而因爲多重繼承有一些不足,才致使在實現中沒有流行起來。

ECMAScript不支持多繼承(即只有一個對象,能夠用來做爲一個直接原型),雖然其祖先自編程語言有這樣的能力。 但在某些實現中(如SpiderMonkey)使用__noSuchMethod__能夠用於管理調度和委託來替代原型鏈。

Mixins

Mixins是代碼重用的一種便捷方式。 Mixins已建議做爲多重繼承的替代品。 這些獨立的元素均可以與任何對象進行混合來擴展它們的功能(所以對象也能夠混合多個Mixins)。 ECMA-262-3規範沒有定義「Mixins」的概念,但根據Mixins定義以及ECMAScript擁有動態可變對象,因此使用Mixins簡單地擴充特性是沒有障礙的。

典型的例子:

// helper for augmentation
Object.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 X
alert([X.a, X.b, X.c, X.d]); 10, 20, 30, 40

請注意,我採起在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__例子是這樣,但也讓咱們展現瞭如何明確地使用委託:

例如:

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的實現 。

其它參考
Using Prototypical Objects to Implement Shared Behavior in Object Oriented Systems (by Henry Lieberman);
Prototype-based programming;
Class;
Object-oriented programming;
Abstraction;
Encapsulation;
Polymorphism;
Inheritance;
Multiple inheritance;
Mixin;
Trait;
Interface;
Abstract class;
Object composition;
Aspect-oriented programming;
Dynamic programming language.

同步與推薦

本文已同步至目錄索引:深刻理解JavaScript系列

深刻理解JavaScript系列文章,包括了原創,翻譯,轉載等各種型的文章,若是對你有用,請推薦支持一把,給大叔寫做的動力。
相關文章
相關標籤/搜索