天天看點

JavaScript 中的對象(二)- 繼承與多态

原型鍊

你如果熟悉一般靜态語言的繼承模式,比如 Java、C++,你會發現這些語言的繼承其實在代碼實作上面都是通過類來進行的,但是問題就是 JavaScript,具體說是在 ES5 以及之前的版本是沒有類這個概念的,那麼繼承如何進行呢?在 JavaScript 的對象建立中,我們知道每個對象都有一個 prototype,也就是它的原型,原型其實也是一個對象,但是一般來說同一類型的對象都會指向同一個原型,這也就導緻原型中的成員屬性或者是成員函數對對象來說就是靜态的,上一小節中我們還提到 prototype 裡面有一個構造函數指針指向對應的構造函數,那麼可不可以通過改變這個構造函數指針來進行繼承呢?答案正是這樣,一個對象中的原型指針指向這個對象的原型,原型也是對象,那麼也就是說,原型中還可以有其他的原型指針指向其他類型的原型,這樣就形成了原型鍊,同時派生類原型中的構造函數指針會被基類的原型指針所覆寫。這時如果要查找一個對象的屬性或者方法,首先在對象中去尋找,沒有就會去到原型中尋找,再沒有的話,就會去到原型中的原型尋找,。。。于是你會發現這其實就是我們平時說的繼承:

function SuperType () {
  this.array = ["super"];
}

SuperType.prototype.superAction = function () {
    console.log("super action");
}

function SubType () {
  this.subArray = ["sub"];
}

SubType.prototype = new SuperType();

SubType.prototype.subAction = function () {
  console.log("sub action");
}

const instance = new SubType();

console.log(SuperType.prototype.isPrototypeOf(instance)); // true
console.log(instance instanceof SuperType);  // true
console.log(instance.array); // ["super"]
console.log(instance.subArray); // ["sub"]
instance.superAction(); // super action
instance.subAction(); // sub action
複制代碼
           

這裡可以看到,我們可以通過将派生類型的原型指針指向一個新的基類對象來實作繼承,繼承後的派生類對象可以通路到基類和基類原型中的成員屬性和成員方法,用這樣的方式來實作繼承雖然簡單粗暴,但是基本還是可以實作繼承後的基本特性,比如說多态,如果我們在派生類型的對象中,或者是派生類對象的原型中添加基類已有的方法,根據我們剛剛講的範圍尋找(對象 -> 目前對象的原型 -> 下一層基類),你會發現這時對象中的方法會覆寫基類的同名方法,這其實就是多态。

構造函數竊取

上面的原型鍊你不難發現其實這裡面是有缺陷的

function SuperType () {
  this.array = [];
}

function SubType () {
  this.subArray = ["sub"];
}

SubType.prototype = new SuperType();

const instance = new SubType();
const instance2 = new SubType();

instance2.array.push("super");

console.log(instance.array); // ["super"]
複制代碼
           

原型鍊的缺陷其實就是之前講的原型的缺陷(所有的對象分享一個原型,那麼一個對象改變原型會導緻其他對象的原型也改變),當然這裡還有一點就是,其實建立派生類的對象是不會重新建立基類對象的,也就是說上面的例子 SuperType 的對象隻會在

SubType.protoType = new SuperType()

這個時候産生,那麼所有的派生類對象分享一個基類對象,這也就會出現我們上面提到的問題。

解決辦法當然很簡單,就是確定每個派生類對象都會被配置設定一個基類的對象,代碼實作如下:

function SuperType () {
  this.array = [];
}

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

const instance = new SubType();
const instance2 = new SubType();

instance2.array.push("super");

console.log(instance.array); // []
複制代碼
           

和之前代碼上的唯一差別就是在 SubType 函數中添加了一行

SuperType.call(this)

,保證每個派生類對象都能被配置設定到單獨的基類對象。

混合繼承

其實上面的構造函數竊取也是有缺陷的,就是會有不必要的資源的浪費,怎麼講?如果說基類中有函數的話,那麼這個函數會被複制很多次,還記得之前講過的構造器函數建立對象的方式嗎?我們可以把函數放在原型當中來避免這種不必要的資源浪費,這裡同理,我們可以把對象的屬性通過構造函數竊取的方式繼承,而靜态屬性或者是一些通用的方法函數使用原型鍊的方式來繼承:

function SuperType () {
  this.array = [];
}

SuperType.prototype.superAction = function () {
  console.log("super action");
}

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

SubType.prototype = new SuperType();

const instance = new SubType();
const instance2 = new SubType();

instance2.array.push("super");

console.log(instance.array); // []
複制代碼
           

這也是在實際當中最常用的一種模式,既保證了繼承的靈活性,又節省了資源。

寄生混合繼承

你可能會覺得前面講的混合繼承已經很好了,但是如果要追求極緻的話,不妨看看寄生混合繼承。如果你看混合繼承中的代碼你會發現,我們在使用原型鍊指定繼承的基類的時候建立了一次基類對象,然後後面建立派生類對象的時候又會通過構造函數竊取建立新的基類對象,但是現在這裡的問題是,使用原型鍊的繼承方式指定基類有必要重新建立新的基類對象嗎? 在混合繼承下,我們使用構造函數竊取繼承基類構造函數中的那些屬性,比如之前的例子中的

array

,我們使用原型鍊繼承基類的原型中的靜态成員和方法,這也就是說原型鍊僅僅是繼承的基類的原型,我們并不需要重新建構一個基類對象,于是我們可以以此為突破口,在講寄生混合繼承之前,我們先講講什麼是寄生繼承:

function object(o){
  function F(){}
  F.prototype = o;
  return new F();
}

function parasitic(o) {
  let another = object(o); // 建立新對象
  another.action = function () {
    console.log("action");
  }
  
  return another;
}

const person = {
  name = "Peter",
  age = 
}

const anotherPerson = parasitic(person);
anotherPerson.action(); // action
複制代碼
           

寄生繼承其實很好了解,就是在一個對象的基礎之上,通過函數的方式建構一個新的對象,這個新的對象在保留了之前對象中的屬性和方法的同時,也會被新添加新的屬性和方法。把這種思想用在我們的混合繼承上,在代碼實作上就如下:

function inheritPrototype(subType, superType) {
  const prototype = Object(superType.prototype);
  prototype.constructor = subType;
  subType.prototype = prototype;
}

function SuperType () {
  this.array = ["Bob"];
  this.name = "Fieer";
}

SuperType.prototype.read = function read() {
  console.log("read");
}

function SubType () {
  SuperType.call(this);
  this.subArray = ["tim"];
}

inheritPrototype(SubType, SuperType);

const instance = new SubType();
const instance2 = new SubType();

instance2.array.push("Peter");

console.log(instance instanceof SuperType);
console.log(SuperType.prototype.isPrototypeOf(instance));
console.log(instance.array); // ["Bob"]
console.log(instance2.array); // ["Bob", "Peter"]
複制代碼
           

當使用混合繼承方式的時候,我們可以避免在指定基類的時候重新建構基類對象,節省了資源。但是這裡有一點需要特别注意的是,

inheritProtoType()

函數是将基類的原型複制一份給派生類,如果派生類需要用到原型來存一些靜态成員,那麼請将這些操作放在

inheritProtoType()

函數之後,不然之前在派生類原型上的操作會是以被覆寫。

總結

關于 JavaScript 中的對象的知識就介紹到這裡,把這兩次的内容放到一起,我們可以得到下面這個知識地圖:

回到之前的問題,JavaScript 為什麼能夠在沒有類的基礎上實作面向對象?這個問題現在應該也不難解釋了,一切都在這個圖中,細緻的去想,圖中的模式也很好地展示了面向對象中的抽象、封裝、繼承和多态,隻是說實作的方法和一般我們所了解的語言不太一樣。從這張圖中你也可以看到知識的疊代,這張圖中的每個圓圈其實是一個個知識點,如果你把這些知識點換做成技術,你會發現一個新技術的出現其實是為了解決或者說是彌補之前技術的不足和缺陷,每當我們學習一項新的技術的時候,不妨去想想這個技術是從其他的什麼技術演變而來的,它為了解決什麼問題,這些問題為什麼之前的技術解決不了?将你的知識體系化,這樣久而久之就能夠融會貫通。

轉載于:https://juejin.im/post/5d1ecdd3e51d451063431882