天天看點

JavaScript OOP常見模式總結(二)一、建立對象模式二、繼承模式

前言:之前我總結了JavaScript OOP常見的幾種模式,今天繼續把剩下的幾種模式整理總結一遍。這幾種模式相對于之前的工廠模式,構造函數模式等基礎模式來說算是進階版,有興趣可以先看之前那篇博文熟悉一下幾種基礎的OOP模式,《JavaScript OOP常見模式總結》 http://blog.csdn.net/hongchh/article/details/52181393

一、建立對象模式

1. 動态原型模式

該模式将所有資訊都封裝在構造函數中,可以在構造函數中初始化原型,并且保持了同時使用構造函數和原型的優點。在執行構造函數時,會通過檢驗某個應該存在的方法是否有效再決定是否需要初始化原型對象。以下面代碼為例,所有資訊都封裝在了構造函數中,并且,為了避免多次初始化原型對象,使用了if條件語句來判斷getProperty()方法是否存在。是以,僅在第一次調用構造函數時會初始化原型對象,建立obj1時初始化了原型對象,後面建立obj2對象時就僅執行添加屬性部分的代碼。這種模式的好處就是可以把屬性和方法的定義都全部寫到一起(都封裝在構造函數中),不用獨立去寫構造函數和原型。本質上群組合模式沒有什麼差別。

function MyObject(property) {
  // 添加屬性
  this.property = property;
  // 添加方法
  if (typeof this.getProperty != "function") {
    MyObject.prototype.getProperty = function() {
      return this.property;
    };
  }
}

var obj1 = new MyObject('xxxx');
var obj2 = new MyObject('yyyy');
console.log(obj1.getProperty()); // 輸出"xxxx"
console.log(obj2.getProperty()); // 輸出"yyyy"
           

2. 寄生構造函數模式

這種模式的基本思想是建立一個函數用于封裝建立對象的代碼,然後傳回新建立的對象。表面上看起來跟工廠模式沒什麼差別,就隻是在建立對象時使用了new操作符。寄生模式傳回得到對象跟構造函數或構造函數原型之間沒有任何關系,工廠模式存在的弊病在這種模式下也存在。

function MyObject(property) {
  var o = new Object();
  o.property = property;
  o.getProperty = function() {
    return this.property;
  };
  return o;
}

var obj = new MyObject('xxxx'); // 使用new操作符建立對象,工廠模式則是直接調用工廠函數
console.log(obj.getProperty()); // 輸出"xxxx"
           

3. 穩妥構造函數模式

首先介紹一個概念,穩妥對象(durable object),沒有公共屬性,其方法也不引用this對象,這種對象就稱為穩妥對象。穩妥構造函數模式與寄生構造函數模式相似,不同的是,建立對象的執行個體方法不引用this,不适用new操作符調用構造函數。以下面代碼為例,構造函數中的方法都沒有引用this對象,變量obj中儲存的是一個穩妥對象,除了通過getProperty()和getPrivate()方法通路對象的屬性之外,沒有其他辦法能夠通路到property和private屬性,這兩個屬性就相當于C++中類的私有成員變量一樣。這種模式本質上是建構了閉包,讓私有變量存在于對象上某個函數的閉包中,隻有通過調用對象上特定的函數才能通路到它閉包中的變量。這樣做的好處就是防止資料被其他程式改動,保證安全性。此模式适合在一些安全性要求較高的執行環境中使用。

function MyObject(property) {
  var o = new Object();
  // 定義參數之外的其他私有變量或方法
  var private= "yyyy";
  o.getProperty = function() {
    return property;
  };
  o.getPrivate = function() {
    return private;
  };
  return o;
}

var obj = MyObject('xxxx');
console.log(obj.getProperty()); // 輸出"xxxx"
console.log(obj.getPrivate()); // 輸出"yyyy"
console.log(obj.property); // undefined
console.log(obj.private); // undefined
           

二、繼承模式

1. 原型式繼承

這種模式的主要思想是借助原型來基于已有的對象建立新對象,同時不必是以建立自定義類型。以下面代碼為例,inherit()函數内部建立了一個臨時構造函數F,并且F的原型對象指向了傳入inherit()的對象obj,表示F類型繼承obj類型。這種模式本質上是做了一次淺複制,像下面代碼一樣,由于obj1被放置到obj2的原型對象的位置上,是以在修改obj2的property1屬性時,obj1的屬性也跟着變了。這種模式建立出來的新對象可以為其添加新的屬性或方法,并且不影響原對象。是以,這種模式适合在構造相似對象,并且為新對象動态添加獨有的屬性方法時使用。ES5中,Object.create()方法就是實作了這種原型式繼承。

function inherit(obj) {
  function F() {}
  F.prototype = obj;
  return new F();
}

var obj1 = {
  property1 : 'xxxx',
  property2 : 'yyyy'
};

var obj2 = inherit(obj1);
console.log(obj2.property1); // 輸出"xxxx"
console.log(obj2.property2); // 輸出"yyyy"
obj2.property1 = 'zzzz';
console.log(obj1.property1); // 輸出"zzzz"
obj2.property3 = 'wwww';
console.log(obj1.property3); // undefined
           

2. 寄生式繼承

寄生式繼承的思路和寄生構造函數或工廠模式相似,建立一個用于封裝繼承過程的函數,在該函數中通過某種方式來增強對象實作繼承。這種模式與前面的原型式繼承一樣,适合在不需要自定義類型和構造函數的情況下使用。這種模式也有缺點,在為對象添加函數時,會由于不能做到函數複用而降低效率。以下面代碼為例,這種模式下看起來跟前面的原型式繼承好像也很類似,不過,這裡建立的每個繼承對象都會定義func()方法,前面的原型式繼承則沒有。也就是說,原型式僅僅是繼承了父類的屬性方法,然後子類對象自己可以随意添加自己特有的屬性方法,而寄生式繼承則是繼承了父類的屬性方法之後還有把子類對象共同的屬性方法也加上。

function inherit(obj) {
  function F() {}
  F.prototype = obj;
  var o = new F();
  o.func = function() {
    console.log('23333');
  };
  return o;
}

var obj1 = {
  property : 'xxxx'
};
var obj2 = inherit(obj1);
obj2.func(); // 輸出"23333"
           

3. 寄生組合式繼承

在介紹寄生組合式繼承之前,我們先回顧一下組合繼承。組合繼承是JS中最常用的繼承模式,但這種模式也存在着自己的問題,這個問題就是無論在什麼情況下,都會調用兩次父類的構造函數,一次是在建立子類型的時候,另一次是在子類型構造函數的内部。請看下面代碼示例,兩次調用父類的構造函數,顯而易見的缺點就是損失效率,函數調用次數我們希望越少越好。再者,我們仔細觀察可以發現,通過這兩次調用,子類對象上繼承了父類的屬性superName,但,子類的原型對象上也存在父類的屬性superName,這個屬性也會被子類對象上的屬性屏蔽,實際上我們希望原型對象隻繼承父類的函數方法,這就造成了定義多餘的屬性浪費資源。

function SuperType(superName) {
  this.superName = superName;
}
SuperType.prototype.saySuperName = function() {
  console.log(this.superName);
};

function SubType(superName, subName) {
  SuperType.call(this, superName); // 繼承父類屬性,調用SuperType()
  this.subName = subName;
}
SubType.prototype = new SuperType(); // 繼承父類方法,調用SuperType()
SubType.prototype.constructor = SubType;
SubType.prototype.saySubName = function() {
  console.log(this.subName);
};

var obj = new SubType('heheheheh', '233333');
obj.saySuperName(); // "heheheheh"
obj.saySubName(); // "233333"
           

我們希望僅調用一次父類的構造函數,并且在子類的原型對象上不要定義多餘的屬性。為了克服組合繼承的不足,寄生組合式繼承是最好的選擇。寄生組合式繼承通過借用構造函數來繼承屬性,通過原型鍊的混合形式來繼承方法,其思路就是:不必為了指定子類型的原型而調用父類的構造函數,而是将父類的原型拷貝一個副本給子類。本質上,就是使用拷貝的方式來繼承父類的原型,然後就把結果指定給子類的原型。代碼示例如下,inherit()函數的作用是複制父類的原型并指定給子類。寄生組合式繼承的高效率展現在它隻調用了一次父類的構造函數,并且避免了在子類的原型對象上建立多餘的屬性,并且,原型鍊依然保持不變,可以通過instanceof或isPrototypeOf()來判斷類型。

function inherit(Super, Sub) {
  // 拷貝父類原型
  var prototype = Object(Super.prototype);
  // 将父類原型副本指定給子類
  prototype.constructor = Sub;
  Sub.prototype = prototype;
}

function SuperType(superName) {
  this.superName = superName;
}
SuperType.prototype.saySuperName = function() {
  console.log(this.superName);
};

function SubType(superName, subName) {
  SuperType.call(this, superName); // 繼承父類屬性,調用SuperType()
  this.subName = subName;
}
inherit(SuperType, SubType); // 繼承父類的原型對象
SubType.prototype.saySubName = function() {
  console.log(this.subName);
};

var obj = new SubType('heheheheh', '233333');
obj.saySuperName(); // "heheheheh"
obj.saySubName(); // "233333"
           

繼續閱讀