JS繼承的幾種方式

2022-03-15 19:00:27
繼承作為物件導向語言的三大特性之一,可以在不影響父類別物件實現的情況下,使得子類物件具有父類別物件的特性;同時還能在不影響父類別物件行為的情況下擴充套件子類物件獨有的特性,為編碼帶來了極大的便利。

雖然 JavaScript 並不是一門物件導向的語言,不直接具備繼承的特性,但是我們可以通過某些方式間接實現繼承,從而能利用繼承的優勢,增強程式碼複用性與擴充套件性。

既然要實現繼承,肯定要有父類別,這裡我們定義了一個父類別 Animal 並增加屬性、範例函數和原型函數,具體程式碼如下:
// 定義一個父類別Animal
function Animal(name) {
    // 屬性
    this.type = 'Animal';
    this.name = name || '動物';
    // 範例函數
    this.sleep = function () {
        console.log(this.name + '正在睡覺!');
    }
}
// 原型函數
Animal.prototype.eat = function (food) {
    console.log(this.name + '正在吃:' + food);
};

原型鏈繼承

原型鏈繼承的主要思想是:重寫子類的 prototype 屬性,將其指向父類別的範例。

我們定義一個子類 Cat,用於繼承父類別 Animal,子類 Cat的實現程式碼如下:
// 子類Cat
function Cat(name) {
    this.name = name;
}
// 原型繼承
Cat.prototype = new Animal();
// 很關鍵的一句,將Cat的建構函式指向自身
Cat.prototype.constructor = Cat;

var cat = new Cat('加菲貓');
console.log(cat.type);    // Animal
console.log(cat.name);    // 加菲貓
console.log(cat.sleep()); // 加菲貓正在睡覺!
console.log(cat.eat('貓糧'));  // 加菲貓正在吃:貓糧
在子類 Cat 中,我們沒有增加 type 屬性,因此會直接繼承父類別 Animal 的 type 屬性,輸出字串“Animal”。

在子類 Cat 中,我們增加了 name 屬性,在生成子類 Cat 的範例時,name 屬性值會覆蓋父類別 Animal 的 name 屬性值,因此輸出字串“加菲貓”,而並不會輸出父類別 Animal 的 name 屬性“動物”。

同樣因為 Cat 的 prototype 屬性指向了 Animal 型別的範例,因此在生成範例 cat 時,會繼承範例函數和原型函數,在呼叫 sleep() 函數和 eat() 函數時,this 指向了範例 cat,從而輸出“加菲貓正在睡覺!”和“加菲貓正在吃:貓糧”。

需要注意其中有很關鍵的一句程式碼,如下所示:
Cat.prototype.constructor = Cat;
這是因為如果不將 Cat 原型物件的 constructor 屬性指向自身的建構函式的話,那將會指向父類別 Animal 的建構函式。
Cat.prototype.constructor === Animal; // true
所以在設定了子類的 prototype 屬性後,需要將其 constructor 屬性指向 Cat。

構造繼承

構造繼承的主要思想是在子類別建構函式中通過 call() 函數改變 this 的指向,呼叫父類別的建構函式,從而能將父類別的範例的屬性和函數繫結到子類的 this 上。
// 父類別
function Animal(age) {
    // 屬性
    this.name = 'Animal';
    this.age = age;
    // 範例函數
    this.sleep = function () {
        return this.name + '正在睡覺!';
    }
}
// 父類別原型函數
Animal.prototype.eat = function (food) {
    return this.name + '正在吃:' + food;
};
// 子類
function Cat(name) {
    // 核心,通過call()函數實現Animal的範例的屬性和函數的繼承
    Animal.call(this);
    this.name = name || 'tom';
}
// 生成子類的範例
var cat = new Cat('tony');
// 可以正常呼叫父類別範例函數
console.log(cat.sleep());  // tony正在睡覺!
// 不能呼叫父類別原型函數
console.log(cat.eat());  // TypeError: cat.eat is not a function
通過程式碼可以發現,子類可以正常呼叫父類別的範例函數,而無法呼叫父類別原型物件上的函數,這是因為子類並沒有通過某種方式來呼叫父類別原型物件上的函數。

複製繼承

複製繼承的主要思想是首先生成父類別的範例,然後通過 for...in 遍歷父類別範例的屬性和函數,並將其依次設定為子類範例的屬性和函數或者原型物件上的屬性和函數。
// 父類別
function Animal(parentAge) {
    // 範例屬性
       this.name = 'Animal';
       this.age = parentAge;
       // 範例函數
       this.sleep = function () {
           return this.name + '正在睡覺!';
       }
    }
    // 原型函數
    Animal.prototype.eat = function (food) {
       return this.name + '正在吃:' + food;
    };
    // 子類
    function Cat(name, age) {
       var animal = new Animal(age);
       // 父類別的屬性和函數,全部新增至子類中
       for (var key in animal) {
           // 範例屬性和函數
           if (animal.hasOwnProperty(key)) {
               this[key] = animal[key];
           } else {
               // 原型物件上的屬性和函數
               Cat.prototype[key] = animal[key];
           }
       }
       // 子類自身的屬性
       this.name = name;
    }
    // 子類自身原型函數
    Cat.prototype.eat = function (food) {
       return this.name + '正在吃:' + food;
    };
   
    var cat = new Cat('tony', 12);
    console.log(cat.age);  // 12
    console.log(cat.sleep()); // tony正在睡覺!
    console.log(cat.eat('貓糧')); // tony正在吃:貓糧
在子類別建構函式中,對父類別範例的所有屬性進行 for...in 遍歷,如果 animal.hasOwnProperty(key) 返回“true”,則表示是範例的屬性和函數,則直接繫結到子類的 this 上,成為子類範例的屬性和函數;

如果 animal.hasOwnProperty(key) 返回“false”,則表示是原型物件上的屬性和函數,則將其新增至子類的 prototype 屬性上,成為子類的原型物件上的屬性和函數。

生成的子類範例 cat 可以存取到繼承的 age 屬性,同時還能夠呼叫繼承的 sleep() 函數與自身原型物件上的 eat() 函數。

組合繼承

組合繼承的主要思想是組合了構造繼承和原型繼承兩種方法。

一方面在子類別建構函式中通過 call() 函數呼叫父類別的建構函式,將父類別的範例的屬性和函數繫結到子類的 this 中;

另一方面,通過改變子類的 prototype 屬性,繼承父類別的原型物件上的屬性和函數。
// 父類別
function Animal(parentAge) {
    // 範例屬性
    this.name = 'Animal';
    this.age = parentAge;
    // 範例函數
    this.sleep = function () {
        return this.name + '正在睡覺!';
    };
    this.feature = ['fat', 'thin', 'tall'];
}
// 原型函數
Animal.prototype.eat = function (food) {
    return this.name + '正在吃:' + food;
};
// 子類
function Cat(name) {
    // 通過建構函式繼承範例的屬性和函數
    Animal.call(this);
    this.name = name;
}
// 通過原型繼承原型物件上的屬性和函數
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;

var cat = new Cat('tony');
console.log(cat.name);   // tony
console.log(cat.sleep()); // tony正在睡覺!
console.log(cat.eat('貓糧'));  // tony正在吃:貓糧

寄生組合繼承

事實上面組合繼承的方案已經足夠好,但是針對其存在的缺點,我們仍然可以進行優化。

在進行子類的 prototype 屬性的設定時,可以去掉父類別範例的屬性和函數。
// 子類
function Cat(name) {
    // 繼承父類別的範例屬性和函數
    Animal.call(this);
    this.name = name;
}
// 立即執行函數
(function () {
    // 設定任意函數Super()
    var Super = function () {};
    // 關鍵語句,Super()函數的原型指向父類別Animal的原型,去掉父類別的範例屬性
    Super.prototype = Animal.prototype;
    Cat.prototype = new Super();
    Cat.prototype.constructor = Cat;
})();
其中最關鍵的語句為如下所示的程式碼:
Super.prototype = Animal.prototype;
只取父類別 Animal 的 prototype 屬性,過濾掉 Animal 的範例屬性,從而避免了父類別的範例屬性繫結兩次。

寄生組合繼承的方式是實現繼承最完美的一種,但是實現起來較為複雜,一般不太容易想到。

在大多數情況下,使用組合繼承的方式就已經足夠,當然能夠使用寄生組合繼承更好。