JavaScript的幾種繼承方式

OneStar發表於2022-04-06

這篇文章稱為筆記更為合適一些,內容來源於 《JavaScript高階程式設計 (第三版)》第六章 6.3 繼承

JavaScript的幾種繼承方式

  1. 原型鏈繼承
  2. 藉助建構函式繼承(經典繼承)
  3. 組合繼承:原型鏈 + 借用建構函式(最常用)
  4. 原型式繼承 (Object.create)
  5. 寄生式繼承
  6. 寄生組合式繼承(最理想)

    1. ES6中的繼承

1. 原型鏈繼承

子型別的原型為父型別的一個例項物件

function Parent() {
    this.name = 'bigStar';
    this.colors = ['red', 'blue', 'yellow'];
}
Parent.prototype.getName = function() {
    console.log(this.name)
}

function Child() {
    this.subName = 'litterStar';
}
// 核心程式碼: 子型別的原型為父型別的一個例項物件
Child.prototype = new Parent();

let child1 = new Child();
let child2 = new Child();
child1.getName(); // bigStar


child1.colors.push('pink');
// 修改 child1.colors 會影響 child2.colors
console.log(child1.colors); // [ 'red', 'blue', 'yellow', 'pink' ]
console.log(child2.colors); // [ 'red', 'blue', 'yellow', 'pink' ]
注意核心程式碼: Child.prototype = new Parent();

特點:

  1. 父類新增在建構函式上面的方法,子類都能訪問到

缺點:

  1. 來自原型物件的所有屬性被所有例項共享,child1修改 colors 會影響child2的 colors
  2. 建立子類例項時,無法向父類的建構函式傳參

2.藉助建構函式繼承(經典繼承)

在子類的建構函式中使用 call()或者 apply() 呼叫父型別建構函式

function Parent(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'yellow'];
}
Parent.prototype.getName = function() {
    console.log(this.name)
}

function Child(name, age) {
    // 核心程式碼:“借調”父型別的建構函式
    Parent.call(this, name);
    this.age = age;
}

let child1 = new Child('litterStar');
let child2 = new Child('luckyStar');
console.log(child1.name); // litterStar
console.log(child2.name); // luckyStar

// 這種方式只是實現部分的繼承,如果父類的原型還有方法和屬性,子類是拿不到這些方法和屬性的。
child1.getName(); // TypeError: child1.getName is not a function
注意核心程式碼: Parent.call(this, name);

特點:

  • 避免引用型別的屬性被所有例項共享
  • 建立子類例項時,可以向父類傳遞引數

缺點

  • 例項並不是父類的例項,只是子類的例項
  • 只能繼承父類的例項屬性和方法,不能繼承原型屬性和方法
  • 無法實現函式複用,每次建立例項都會建立一遍方法,影響效能

3.組合繼承:原型鏈 + 借用建構函式(最常用)

function Parent(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'yellow'];
}
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;


// 可以透過子類給父類的建構函式傳參
let child1 = new Child('litterStar');
let child2 = new Child('luckyStar');
child1.getName(); // litterStar
child2.getName(); // luckyStar

child1.colors.push('pink');
// 修改 child1.colors 不會影響 child2.colors
console.log(child1.colors); // [ 'red', 'blue', 'yellow', 'pink' ]
console.log(child2.colors); // [ 'red', 'blue', 'yellow' ]
注意核心程式碼: Parent.call(this, name);Child.prototype = new Parent();

特點

  • 融合了原型鏈繼承和借用建構函式的優點,稱為JavaScript中最常用的繼承模式。

    缺點

  • 呼叫了兩次父類建構函式,生成了兩份例項

    • 一次是設定子型別例項的原型的時候 Child.prototype = new Parent();
    • 一次是建立子型別例項的時候 let child1 = new Child('litterStar');, 呼叫 new 會執行 Parent.call(this, name);,此時會再次呼叫一次 Parent建構函式

4. 原型式繼承 (Object.create)

藉助原型可以基於現有方法來建立物件,var B = Object.create(A) 以A物件為原型,生成A物件,B繼承了A的所有屬性和方法。

const person = {
    name: 'star',
    colors: ['red', 'blue'],
}

// 核心程式碼:Object.create
const person1 = Object.create(person);
const person2= Object.create(person);

person1.name = 'litterstar';
person2.name = 'luckystar';

person1.colors.push('yellow');

console.log(person1.colors); // [ 'red', 'blue', 'yellow' ]
console.log(person2.colors); // [ 'red', 'blue', 'yellow' ]
注意核心程式碼: const person1 = Object.create(person);

特點

  • 沒有嚴格意義上的建構函式,藉助原型可以基於已有物件建立新物件

    缺點

  • 來自原型物件的所有屬性被所有例項共享,person1修改 colors 會影響person2的 colors,這點跟原型鏈繼承一樣。

5. 寄生式繼承

建立一個用於封裝繼承過程的函式,該函式在內部以某種方式來增強物件

function createObj (original) {
    // 透過呼叫函式創新一個新物件
    var clone = Object.create(original);
    // 以某種方式來增強這個物件
    clone.sayName = function () {
        console.log('hi');
    }
    // 返回這個物件
    return clone;
}

缺點: 每次建立物件都會建立一遍方法,跟藉助建構函式模式一樣

6.寄生組合式繼承(最理想的)

我們可以先回憶一下JavaScript最常用的繼承模式: 組合繼承(原型鏈 + 借用建構函式),它的最大缺點是會呼叫兩次父建構函式(Child.prototype = new Parent();let child1 = new Child('litterStar');)。

我們是否可以想辦法是呼叫一次?可以讓 Child.prototype 訪問到 Parent.prototype。

我們不能直接使用 Child.prototype = Parent.prototype來實現,因為會出現一些副作用,你可能在修改 Child.prototype 的時候會修改Parent.prototype

可以使用 Object.create(...)來實現

Object.create MDN上的解釋:它會建立一個新物件,使用現有的物件來提供新建立的物件的__proto__
function Parent(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'yellow'];
}
Parent.prototype.getName = function() {
    console.log(this.name)
}

function Child(name, age) {
    // 核心程式碼①
    Parent.call(this, name);

    this.age = age;
}
// 核心程式碼②
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
注意核心程式碼: Parent.call(this, name);Child.prototype = Object.create(Parent.prototype);

寄生組合式繼承,集寄生式繼承和組合式繼承的優點,是引用型別最理想的繼承正規化。

7. ES6 中class的繼承

ES6中引入了class關鍵字,可以透過extends關鍵字實現繼承。

class Parent {}
class Child extends Parent {
    constructor(name, age, color) {
        // 呼叫父類的constructor(name, age)
        super(name, age);
        this.color = color;
    }
    toString() {
        return this.color + ' ' + super.toString(); // 呼叫父類的toString()
    }
}

class關鍵字只是原型的語法糖,JavaScript繼承仍然是基於原型實現的。

參考

相關文章