JavaScript實現繼承的幾種重要範式

一 原型鏈

1. 代碼示例

function SuperType() {
  this.superProperty = true;
}

SuperType.prototype.getSuperValue = function() {
  return this.superProperty;
}

function SubType() {
  this.subProperty = false;
}

SubType.prototype = new SuperType(); //將 SuperType類型的實例 做爲 SubType類型的 原型對象, 這樣就重寫了SubType的原型對象(沒有使用SubType默認的原型對象), 實現了繼承。

SubType.prototype.getSubValue = function() {
  return this.subProperty;
}

const subTypeInstance = new SubType();
console.log(subTypeInstance.getSuperValue());//true

詳見個人另外一篇博客《原型與原型鏈》 的"2、實現繼承的主要範式:原型鏈
"。html

2、 借用構造函數(經典繼承)

1.代碼示例

function SuperType() {
  this.colors = ['red', 'blue', 'green'];
}

function SubType() {
  SuperType.call(this);//在子類型構造函數內部調用超類型構造函數,繼承了SuperType
}

var subTypeInstance1 = new SubType();
subTypeInstance1.colors.push('yellow');
console.log(subTypeInstance1);//['red', 'blue', 'green','yellow']

var subTypeInstance2 = new SubType();
console.log(subTypeInstance2.colors);//['red', 'blue', 'green']

基本思想就是在子類型構造函數內部調用超類型構造函數。es6

2. 優勢

(1)子類型每一個實例都會繼承一份獨立的超類型屬性副本

經過使用call方法(或apply),其實是在將來新建立子類型實例時當場調用了超類型的構造函數,也就是在初始化子類型實例時才把超類型的屬性添加到子類型實例上。那麼子類型的每一個實例都會擁有一份獨立的超類型屬性副本。 這樣不一樣的子類型實例對同一個繼承來的屬性進行修改(例如對數組屬性進行push),也不會互相影響。數組

(2)能夠在子類型構造函數中向超類型構造函數傳遞參數
function SuperType(name) {
  this.name = name;
}

function SubType(name) {
  SuperType.call(this,name);
}

var subTypeInstance1 = new SubType('Bonnie');
console.log(subTypeInstance1.name);//"Bonnie"

var subTypeInstance2 = new SubType('Summer');
console.log(subTypeInstance2.name);//"Summer"

3. 缺點

(1)不能作到函數複用:沒法避免構造函數模式的問題——使用構造函數模式建立的每一個實例都包含着各自獨有的同名函數,故函數複用無從談起。app

(2)子類型建立方式限制:而在超類型的原型中定義的方法,對子類而言是不可見的,因此全部子類型都只能經過構造函數模式建立。函數

3、 組合繼承(僞經典繼承)

1. 代碼示例

function SuperType(name) {
  this.name = name;
  this.colors = ['red', 'blue', 'green'];
}
SuperType.prototype.sayName = function() {
  console.log(this.name);
}

function SubType(name, age) {
  //繼承屬性
  SuperType.call(this, name); //第二次調用超類型構造函數SuperType
  //本身的屬性
  this.age = age;
}

//繼承方法:SubType.prototype也會獲得繼承的屬性,不過會被上述構造函數中call方法繼承的屬性做爲實例屬性覆蓋掉。
SubType.prototype = new SuperType(); //第一次調用超類型構造函數SuperType
SubType.prototype.constructor = SubType;//重寫prototype會割裂子類型原型與子類型構造函數的關係,故要加上這麼一句
//本身的方法
SubType.sayAge = function() {
  console.log(this.age);
}

將原型鏈和借用構造函數組合起來:使用原型鏈實現對超類型原型上的方法和屬性的繼承,使用借用構造函數實現對超類型實例屬性和方法的繼承。通常超類型原型上就只有方法,超類型實例上只有屬性(即 組合使用構造函數模式和原型模式,參見3.4)。這樣一來,該方式就是:用原型鏈實現對超類型原型上方法的繼承,用借用構造函數實現對超類型實例上屬性的繼承。ui

2. 優勢

組合繼承用 原型鏈實現對 超類型原型上方法的繼承,用 借用構造函數實現對 超類型實例上屬性的繼承。這樣可讓子類型的不一樣實例既分別擁有獨立的屬性(尤爲是引用類型屬性,如colors數組),又能夠共享相同的方法。this

組合繼承避免了原型鏈和借用構造函數的缺陷,融合了它們的優勢,是 JavaScript中最經常使用的繼承模式prototype

3. 缺點

不管在什麼狀況下,都會調用兩次超類型構造函數:一次是在建立子類型原型時,一次是在子類型構造函數內部。這樣的話,雖然子類型會包含超類型的所有屬性,可是對於超類型的實例屬性而言,調用子類型的構造函數時會重寫一遍這些實例屬性。設計

4、 原型式繼承

1. 代碼示例

function object(o) {
  function F(){};//先構造一個臨時性的構造函數
  F.prototype = o;//將傳入的對象做爲該構造函數的原型
  return new F();//返回臨時類型的新實例
}

//使用
var person = {
  name:'Bonnie',
  friends: ['Summer', 'Spring']
}

var person1 = object(person);
person1.name = 'Huiyun';
person1.friends.push('Tony');

var person2 = object(person);
person2.name = 'Huiyun';
person2.friends.push('Joy');

console.log(person1.friends);//["Summer", "Spring", "Tony", "Joy"]
console.log(person2.friends);//["Summer", "Spring", "Tony", "Joy"]
console.log(person.friends);//["Summer", "Spring", "Tony", "Joy"]

思想: 藉助原型能夠基於已有的對象(而非類型)建立新對象(而非建立自定義類型)。其實,object對傳入其中的對象執行了一次 淺複製code

2. 優勢

能夠基於已有的對象建立新對象,並且還沒必要建立新類型。適於基於已有對象加以修改獲得另外一個對象。

在只想讓一個對象與另外一個對象保持相似,又不想興師動衆建立構造函數的狀況下,原型式繼承徹底能夠勝任。

延伸:ES6的Object.create()

該方法規範化了原型式繼承,在只傳入一個參數的狀況下,和上述object達到的效果相同。該方法的第二個參數爲新對象額外屬性組成的對象,也就是簡化了上述object的後續用法。

語法:
Object.create(protoObj, [newPropertiesObj])

參數:

  • protoObj: 新建立對象的原型對象
  • newPropertiesObj:可選。 要添加(或重寫)到新對象上的可枚舉的實例屬性的屬性描述符及其名稱組成的對象(與Object.defineProperties()的第二個參數相同)。

newPropertyiesObj語法:

{
  prop1Name:{
    value: valueConent,
    writable: false(default)/true
    enumerable: false(default)/true
    configuragle: false(default)/true
  },
  prop2Name: {
    ...
  },
  ...
}

詳見https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperties

返回值: 一個帶有指定的原型對象屬性和本身新添加的實例屬性的對象

Eg:
var person = {
  name: 'Bonnie',
  friends: ['Summer','Spring']
}

var person1 = Object.create(person, {
  name:{
   value:'Huiyun'
  }
});

console.log(person1);//{name: "Huiyun"}
console.log(person1.friends);// ["Summer", "Spring"]

3.缺點

引用類型屬性共享: 該方法基於已有對象建立新對象,可是對新對象修改引用類型屬性,已有的基礎對象也會受到修改,基於基礎對象的其餘對象也會受到修改。

5、 寄生式繼承

1. 代碼示例

function createAnother(original) {
  var clone = object(original);//此處運用了4.4中的objec函數,也可使用Object.create(original)
  clone.sayHi = function() {
    console.log('Hi');
  }
  return clone;
}

思想:與建立對象的寄生構造函數模式和工廠模式相似,即建立了一個僅用於封裝繼承過程的函數,並在該函數內部以某種方式來加強對象,最後再像真地是它本身作了全部工做同樣返回對象。

2. 優勢

在主要考慮基於某個對象而非考慮自定義類型和構造函數的狀況下,寄生式繼承也是一種有用的模式。該模式能夠基於已有對象建立添加了函數的新對象。

3. 缺點

(1)不能作到函數複用:不能作到對新添加函數進行函數複用,這樣會下降效率。該缺點與 借用構造函數繼承相似。

(2)引用類型屬性共享:該方法也是基於已有對象建立新對象,可是對新對象修改引用類型屬性,已有的基礎對象也會受到修改,基於基礎對象的其餘對象也會受到修改。該缺點 與原型式繼承 同樣。

6、 寄生組合式繼承

1. 代碼示例

//實際上是寄生式繼承的一種應用:以SuperType.prototype爲基礎對象,建立SubType.prototype對象。這個SubType.prototype對象是SuperType.prototype的淺複製,同時SubType.prototype對象上又增添了額外的屬性constructor指向SubType。
function inheritPrototype(SubType, SuperType) {
  var prototype = Object.create(SuperType.prototype);
  prototype.constructor = SubType;
  Subtype.prototype = prototype;
}

//應用
function SuperType(name) {
  this.name = name;
  this.colors = ['red', 'blue'];
}
SuperType.prototype.sayName = function() {
  console.log(this.name);
};

//子類型實例經過借用構造函數繼承來獲取超類型實例上的屬性:
function Subtype(name, age) {
  SuperType.call(this,name);
  this.age = age;
}
//子類型的原型經過寄生式繼承來獲取超類型原型上的方法:
inheritPrototype(SubType,SuperType);
//給子類型原型添加本身的方法
SubType.prototype.sayAge = function() {
  console.log(this.age);
}

思想: 子類型的實例經過借用構造函數來獲取超類型實例上的屬性,子類型的原型經過寄生式繼承來獲取超類型原型上的方法(此處寄生式繼承是指子類型原型對超類型原型進行淺複製)。也就是說子類型經過借用構造函數繼承屬性,經過寄生式繼承來繼承方法。 和組合式繼承相比,該寄生組合式繼承沒必要爲了指定子類型的原型而調用超類型的構造函數,只是使用了超類型原型的一個副本;而只有在指定子類型的實例屬性時調用了超類型的構造函數(借用構造函數繼承);這樣該方法就只調用了一次超類型的構造函數。

2. 優勢

(1) 屬性獨立、方法共享:擁有組合式繼承的全部優勢:分別擁有獨立的屬性(尤爲是引用類型屬性,如colors數組),又能夠共享相同的方法。

(2) 高效率:避免了組合式繼承調用兩次超類型構造函數的缺點,只調用一次超類型構造函數,具備高效率。

(3) 原型鏈不變:可以正常使用instanceof和isPrototypeOf()

該寄生組合式繼承是最理想的繼承範式。

7、Es6的Class對繼承的實現*

參考資料

《JavaScirpt高級程序設計》6.3
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperties

相關文章
相關標籤/搜索