js繼承圖解

耳東蝸牛發表於2020-12-05

語雀地址排版好一些:繼承

繼承是物件導向程式設計中討論最多的話題。很多物件導向語言都支援兩種繼承:介面繼承和實現繼承。前者只繼承方法簽名,後者繼承實際的方法。介面繼承在 ECMAScript 中是不可能的,因為函式沒有簽名。實現繼承是 ECMAScript 唯一支援的繼承方式,而這主要是通過原型鏈實現的。

 

原型知識前置

var Person = function(name){
  this.name = name; // tip: 當函式執行時這個 this 指的是誰?
};
Person.prototype.getName = function(){
  return this.name;  // tip: 當函式執行時這個 this 指的是誰?
}
var person1 = new Person('Mick');

 

image.png

 

繼承

原型鏈繼承

原型鏈定義為ECMA的主要繼承方式。

function Parent () {
    this.name = 'kevin';
}

Parent.prototype.getName = function () {
    console.log(this.name);
}

function Child () {

}

Child.prototype = new Parent();

var child1 = new Child();

console.log(child1.getName()) 

 

原先是這樣:

image.png

 

現在是這樣:

image.png

 

Child建立的物件的__proto__指向new Parent物件。contructor指向Parent建構函式。這裡有一個問題是沒有將原型物件的contructor指向Child。加一行程式碼就可以了。
 

Child.prototype.constructor = Child;
 

缺點

  • 引用型別的屬性被所有例項共享,值型別不會被共享,是因為當我們在執行[[get]]操作的時候會去查詢原型鏈。執行[[put]]的時候,會自動給當前的物件建立一個屬性
  • 在建立 Child 的例項時,不能向Parent傳參

 

子型別在例項化時不能給父型別的建構函式傳參。事實上,我們無法在不影響所有物件例項的情況下把引數傳進父類的建構函式。再加上之前提到的原型中包含引用值的問題,就導致原型鏈基本不會被單獨使用。

 

借用建構函式繼承【經典繼承】

為了解決原型包含引用值導致的繼承問題,一種叫作“盜用建構函式”(constructor stealing)的技術在開發社群流行起來(這種技術有時也稱作“物件偽裝”或“經典繼承”)。基本思路很簡單:在子類建構函式中呼叫父類建構函式。因為畢竟函式就是在特定上下文中執行程式碼的簡單物件,所以可以使用apply()和 call()方法以新建立的物件為上下文執行建構函式。

function Parent (name) {
    this.name = name;
}

function Child (name) {
    Parent.call(this, name);
}

var child1 = new Child('kevin');

console.log(child1.name); // kevin

var child2 = new Child('daisy');

console.log(child2.name); // daisy
原先是這樣:

image.png

 

現在是這樣:

image.png

 

從上圖可以清楚看到:

當前的繼承方式是通過new操作的時候,繫結child的this執行Parent的方法,去執行一次Parent內部的方法。和原型鏈啥的沒有任何關係。

 

缺點

盜用建構函式的主要缺點,也是使用建構函式模式自定義型別的問題:必須在建構函式中定義方法,因此函式不能重用【因為二者之間的函式是兩個函式,本質是不相等的函式】。此外,子類也不能訪問父類原型上定義的方法,因此所有型別只能使用建構函式模式。由於存在這些問題,盜用建構函式基本上也不能單獨使用。

 

 

組合繼承

組合繼承(有時候也叫偽經典繼承)綜合了原型鏈和盜用建構函式,將兩者的優點集中了起來。

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

Parent.prototype.getName = function () {
    console.log(this.name)
}

function Child (name, age) {
    Parent.call(this, name);
    this.age = age;
}

Child.prototype = new Parent();
Child.prototype.constructor = Child;

var child1 = new Child('kevin', '18');

child1.colors.push('black');

console.log(child1.name); // kevin
console.log(child1.age); // 18
console.log(child1.colors); // ["red", "blue", "green", "black"]

var child2 = new Child('daisy', '20');

console.log(child2.name); // daisy
console.log(child2.age); // 20
console.log(child2.colors); // ["red", "blue", "green"]

 

image.png

 

組合繼承彌補了原型鏈和盜用建構函式的不足,是 JavaScript 中使用最多的繼承模式。而且組合繼承也保留了 instanceof 操作符和 isPrototypeOf()方法識別合成物件的能力。

 

原型式繼承

2006 年,Douglas Crockford 寫了一篇文章:《JavaScript 中的原型式繼承》(“Prototypal Inheritance in JavaScript”)。這篇文章介紹了一種不涉及嚴格意義上建構函式的繼承方法。他的出發點是即使不自定義型別也可以通過原型實現物件之間的資訊共享。

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

let person = { 
 name: "Nicholas", 
 friends: ["Shelby", "Court", "Van"] 
}; 

let anotherPerson = object(person); 
anotherPerson.name = "Greg"; 
anotherPerson.friends.push("Rob"); 

let yetAnotherPerson = object(person); 
yetAnotherPerson.name = "Linda"; 
yetAnotherPerson.friends.push("Barbie"); 
console.log(person.friends); // "Shelby,Court,Van,Rob,Barbie"

 

image.png

 

原型式繼承雖然說是不自定義型別來實現原型物件之間的資訊共享,但是我們可以發現object裡面還是自定義了一個型別,去實現原型鏈繼承的方式。

 

特定場景:原型式繼承非常適合不需要單獨建立建構函式,你有一個物件,想在它的基礎上再建立一個新物件。

 

ECMAScript 5 通過增加 Object.create()方法將原型式繼承的概念規範化了。這個方法接收兩個引數:作為新物件原型的物件,以及給新物件定義額外屬性的物件(第二個可選)。在

 

寄生式繼承

與原型式繼承比較接近的一種繼承方式是寄生式繼承(parasitic inheritance),也是 Crockford 首倡的一種模式。寄生式繼承背後的思路類似於寄生建構函式和工廠模式:建立一個實現繼承的函式,以某種方式增強物件,然後返回這個物件。

 

這種模式依賴於上面的object方法,這裡直接使用Obect.create()方法

function createObj (o) {
    var clone = Object.create(o);
    clone.sayName = function () {
        console.log('hi');
    }
    return clone;
}

這裡的圖和上面的基本相同,就不畫了。這裡說的是一種思想,在createObj的方法裡統一對我們建立的物件做一些處理。

 

寄生組合式繼承

組合繼承其實也存在效率問題。最主要的效率問題就是父類建構函式始終會被呼叫兩次:一次在是建立子類原型時呼叫,另一次是在子類建構函式中呼叫。本質上,子類原型最終是要包含超類物件的所有例項屬性,子類建構函式只要在執行時重寫自己的原型就行了。

 

可以翻到組合繼承的地方,看到Parent會被呼叫兩次,call一次,原型物件一次。

 

image.png

 

寄生式組合繼承通過盜用建構函式繼承屬性,但使用混合式原型鏈繼承方法。基本思路是不通過呼叫父類建構函式給子類原型賦值,而是取得父類原型的一個副本。說到底就是使用寄生式繼承來繼承父類原型,然後將返回的新物件賦值給子類原型。

 

function inheritPrototype(subType, superType) {
    let prototype = Object.create(superType.prototype); // 建立物件
    prototype.constructor = subType; // 增強物件
    subType.prototype = prototype; // 賦值物件
}

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);
    this.age = age;
}

inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function() {
    console.log(this.age);
};

var subTupe1 = new SubType('name1', 1)
var subTupe2 = new SubType('name2', 2)

image.png

 

 

總結繼承

image.png

 

image.png

 

 

 

相關文章