1. 程式人生 > 程式設計 >JS中的六種繼承方式以及優缺點總結

JS中的六種繼承方式以及優缺點總結

目錄
  • 前言
  • 原型鏈繼承
  • 建構函式繼承
  • 組合繼承(原型鏈繼承和建構函式繼承組合)
  • 寄生式繼承
  • 組合寄生式繼承
  • extends繼承
  • 總結

前言

繼承是世界中必不可少的一個環節,號稱JS的三座大山之一,使用這種方式我們可以更好地複用以前的開發程式碼,縮短開發的週期、提升開發效率

在ES6之前,JS中的類都是通過建構函式模擬的,並不存在真正意義上的類,雖然ES6的類知識一個語法糖😂,這個時期的類是可以當作函式直接使用的,到了ES6之後,類是不可以再當作函式使用了

在開始聊繼承之前,首先需要明確的是類中存在兩種屬性:例項上的屬性和公共屬性,接下來談到的所有繼承方式都是圍繞這兩點來展開

function Animal(name) {
  // 例項上的屬性
  this.name = name;
}

// 公共屬性
Animal.prototype.eat = function() {
  // todo ...
}

如何避免將ES6之前的建構函式直接當作函式呼叫?

ES5時期解決方案:

function Animal() {
    // 若是直接呼叫,不是使用 new呼叫,丟擲異常
    if (!(this instanceof Animal)) {
        // new的原理,使用new的時候,this是Animal的例項,則 this instanceof Animal 為true
        throw new Error("請勿直接呼叫建構函式");
    }
}

ES6之後解決方案:

function Animal() {
    // 若是使用new,則new.target指向自身,否則為unwww.cppcns.com
defined,但是在繼承的時候不能使用,因為繼承例項上屬性的時候,原來的es5是使用 Animal.call(this)的方式 if (!new.target) { throw new Error("請勿直接呼叫建構函式"); } }

上述兩種方案都可以解決直接當作函式呼叫,若是直接呼叫控制檯會報錯:Uncaught Error: 請勿直接呼叫建構函式

接下來便一起看看JS中有哪些繼承方式

原型鏈繼承

原型鏈繼承是比較常見的繼承方式之一,其中涉及的建構函式、原型和例項,三者之間存在著一定的關係,即每一個建構函式都有一個原型物件,原型物件又包含一個指向建構函式的指標,而例項則包含一個原型物件的指標。

function Person(name) {
    this.name = name;
    this.permission = ["user","salary","vacation"];
}

Person.prototype.say = function () {
    console.log(`${this.name} 說話了`);
};

function Staff(age) {
    this.age = age;
}

Staff.prototype = new Person("張三");

const zs = new Staff(12);
console.log(zs.name); // 張三
zs.say(); // 張三 說話了

此時程式碼是符合期望,接下來再建立一個例項並修改name和permission

const zs = new Staff(12);
const zs2 = new Staff(18);
zs.permission.pop()
zs.name = '李四';

console.log(zs.name);
console.log(zs2.name);
console.log(zs.permission);
console.log(zs2.permission);

前兩個分別輸出是:李四、張三,後面兩個輸出結果一致,都為["user","salary"],為什麼會出現這種情況呢?
當執行zs.name = '李四';時,其實這個時候是賦值操作,賦值之後zs變為

JS中的六種繼承方式以及優缺點總結

而zs2.name是通過原型鏈繼續查詢,因此前面的兩個輸出是李四、張三

通過console.log(zs.__proto__ === zs2.__proto__);輸出為true,可以得知兩個例項使用的是同一個原型物件Person,他們的記憶體空間是共享的,當一個發生變化時,另外一個也隨之進行了變化

通過上述發現原型鏈繼承存在一些缺點

建構函式繼承

建構函式通常時藉助call、apply來完成繼承

function Person(name) {
    this.name = name;
    http://www.cppcns.comthis.permission = ["user","vacation"];
}

Person.prototype.say = function () {
    console.log(`${this.name} 說話了`);
};

function Staff(name,age) {
    Person.call(this,name);
    this.age = age;
}

Staff.prototype.eat = function () {
    console.log('吃東西啦~~~');
}

const zs = new Staff("張三",12);
console.log(zs);

上述程式碼控制檯輸出:

JS中的六種繼承方式以及優缺點總結

可以看到不僅擁有Staff的屬性和方法,同時也繼承了Person的屬性,因為每次例項化的時候都會呼叫Person.call(this,name);,可以解決原型鏈繼承的問題

此時呼叫Person原型上的方法

zs.say()

這個時候控制檯會報錯:Uncaught TypeError: zs.say is not a function

組合繼承(原型鏈繼承和建構函式繼承組合)

原型鏈繼承和建構函式繼承都存在各自的問題和優勢,結合兩種繼承方式便生成了組合繼承

function Person(name) {
    this.name = name;
    this.permission = ["user","vacation"];
}

Person.prototype.say = function () {
	console.log(`${this.name} 說話了`);
};

function Staff(name,age) {
    // 第二次執行 Person
    Person.call(this,name);
    this.age = age;
}

Staff.prototype.eat = function () {
    console.log("吃東西啦~~~");
};

// 第一次執行 Person
Staff.prototype = new Person();
// 若是不將Staff constructor指回到Staff,此時的Staff例項zs.constructor則指向Person
Staff.prototype.constructor = Staff;

const zs = new Staff("張三",12);
const ls = new Staff("李四",12);
zs.permission.pop();
console.log(zs.permission);
console.log(ls.permission);
zs.say();
ls.say();

暫時控制檯的輸出都是正常的,也將上述兩種繼承的缺點解決了,但是此時又新增了兩個個問題:

  1. Person被執行了兩次,分別為:Person.call(this,name)和new Person(),期望執行一次,多執行的一次便會造成一次效能開銷
  2. 在之前Staff.prototype = new Person()定義一些公共屬性和方法時會被覆蓋掉,例如不能例項呼叫zs.eat(),控制檯會報錯Uncaught TypeError: zs.eat is not a function,若是在其之後定義則會汙染Person

寄生式繼承

通過利用Object.create獲得一份目標物件的淺拷貝,然後新增一些方法避免汙染基類,主要是解決組合繼承的第二個問題

主要將如下兩行程式碼進行替換

Staff.prototype = new Person();
Staff.prototype.constructor = Staff;

替換為:

Staff.prototype = Object.create(Person.prototype,{
    constructor: {
        // 若是不將Staff constructor指回到Staff,此時的Staff例項zs.constructor則指向Person
        value: Staff,},});

組合寄生式繼承

到目前為止,還有一個兩次例項化Person的問題沒有解決,接下來的組合寄生式繼承可完美解決上述問題,這也是ES6之前所有繼承方式中最優的繼承方式

完整程式碼如下:

function Person(name) {
    this.name = name;
    this.permission = ["user",name);
    this.age = age;
}

Staff.prototype = Object.create(Person.prototype,});

Staff.prototype.eat = function () {
    console.log("吃東西啦~~~");
};

其實繼承時,改變Staff.prototype指向並不止上述這些方式,還有一些其他方法

  • prototype.__proto__方式
Staff.prototype.__proto__ = Person.prototype

prototype.__proto__存在相容性問題,自己找不到,通過原型鏈繼續向上查詢,此時Animal和Tiger不會再共享同一地址,不會相互影響

  • Object.setPrototypeOf方式
Object.setPrototypeOf(Staff.prototype,Person.prototype)

es6語法,存在相容性,其原理就是原理就是 prototype.__proto__方式

extends繼承

在ES6之後,可以使用extends進行繼承,這也是目前開發中最常使用的方式,雖然目前瀏覽器支援度並不理想,但是在工程化如此完善的今天,這些都已經不是制約使用其的理由

class Person {
    constructor(name) {
        this.name = name;
        this.permission = ["user","vacation"];
    }

    say() {
        console.log(`${this.name} 說話了`);
    }
}

class Staff extends Person {
    constructor(name,age) {
        super(name);
        this.age = age;
    }

    eat() {
        console.log("吃東西啦~~~");
    }
}
www.cppcns.com

其實ES6的繼承通過babel編譯之後,採用也是組合寄生式繼承,因此我們需要重點掌握其繼承原理。

總結

到此這篇關於JS中六種繼承方式以及優缺點的文章就介紹到這了,更多相關JS繼承方式及優缺點內容請搜尋我們以前的文章或繼續SsbPWVHHJ瀏覽下面的相關文章希望大家以後多多支援我們!