JS專題之繼承

前言

衆所周知,JavaScript 中,沒有 JAVA 等主流語言“類”的概念,更沒有“父子類繼承”的概念,而是通過原型對象和原型鏈的方式實現繼承。

於是,我們這一篇講一講 JS 中的繼承(委託)。

一、爲什麼要有繼承?

JavaScript 是面向對象編程的語言,裏面全是對象,而如果不通過繼承的機制將對象聯繫起來,勢必會造成程序代碼的冗餘,不方便書寫。

二、爲什麼又是原型鏈繼承?

好,既然是 OO 語言,那麼就加繼承屬性吧。但是 JS 創造者並不打算引用 class,不然 JS 就是一個完整的 OOP 語言了,而創造者 JS 更容易讓新手開發。

後來,JS 創造者就將 new 關鍵字創建對象後面不接 class,改成構造函數,又考慮到繼承,於是在構造函數上加一個原型對象,最後讓所有通過 new 構造函數 創建出來的對象,就繼承構造函函數的原型對象的屬性。

function Person() {
    // 構造函數
    this.name = "jay";
}

Person.prototype = {
    sex: "male"
}

var person1 = new Person();
console.log(person1.name);  // jay
console.log(person1.sex);  // male

所以,就有了 JavaScript 畸形的繼承方式:原型鏈繼承~

三、原型鏈繼承

function Parent() {
    this.names = ["aa", "bb", "cc"];
    this.age = 18;
}

function Child() {
    // ...
}

Child.prototype = new Parent();  // 改變構造函數的原型對象

var child1 = new Child();

// 繼承了 names 屬性
console.log(child1.names);  // ["aa", "bb", "cc"]
console.log(child1.age);   // 18
child1.names.push("dd");
child1.age = 20;
var child2 = new Child();
console.log(child2.names);  // ["aa", "bb", "cc", "dd"]
console.log(child2.age);  // 18

以上例子中,暴露出原型鏈繼承的兩個問題:

  1. 包含引用類型數據的原型屬性,會被所有實例共享,基本數據類型則不會。
  2. 在創建子類型實例時,無法向父類型的構造函數中傳遞參數。

四、call 或 apply 繼承

function Parent(age) {
    this.names = ["aa", "bb", "cc"]
    this.age = age;
}
function Child() {
    Parent.call(this, 18);
}

var child1 = new Child();

// 繼承了 names 屬性
console.log(child1.names);  // ["aa", "bb", "cc"]
child1.names.push("dd");
console.log(child1.age);  // 18

var child2 = new Child();
console.log(child2.names);  // ["aa", "bb", "cc"]
console.log(child2.age);  // 18

call 或 apply 的原理是在子類型的構造函數中,“借調”父類型的構造函數,最終實現子類型中擁有父類型中屬性的副本了。

call 或 apply 這種繼承方式在《JavaScript 高級程序設計》中叫作“借用構造函數(constructor stealing)”,解決了原型鏈繼承中,引用數據類型被所有子實例共享的問題,也能夠實現傳遞參數到構造函數中,但唯一的問題在於業務代碼也寫在了構造函數中,函數得不到複用。

五、組合繼承

組合繼承(combination inheritance)也叫作僞經典繼承,指的是,前面兩種方法:原型鏈繼承和 call 或 apply 繼承 組合起來,保證了實例都有自己的屬性,同時也能夠實現函數複用:

function Parent(age) {
    this.names = ["aa", "bb", "cc"]
    this.age = age;
}

Parent.prototype.sayName = function () {
    console.log(this.names);
}

function Child() {
    Parent.call(this, 18);  // 第一次調用
}

Child.prototype = new Parent();  // 第二次調用:通過原型鏈繼承 sayName 方法
Child.prototype.constructor = Child;  // 改變 constructor 爲子類型構造函數

var child1 = new Child();
child1.sayName();   // ["aa", "bb", "cc"]
child1.names.push("dd");
console.log(child1.age);  // 18

var child2 = new Child();
console.log(child2.names);  // ["aa", "bb", "cc"]
console.log(child2.age); 
child2.sayName();  // ["aa", "bb", "cc"]

組合繼承將繼承分爲兩步,一次是創建子類型關聯父類型原型對象的時候,另一次是在子類型構造函數的內部。是 JS 最常用的繼承方式。

六、原型式繼承

原型式繼承說白了,就是將父類型作爲一個對象,直接變成子類型的原型對象。

function object(o){
  function F(){}
  F.prototype = o;
  return new F();
}
 
var parent = {
    age: 18,
    names: ["aa", "bb", "cc"]
};
 

var child1 = object(parent);

// 繼承了 names 屬性
console.log(child1.names);  // ["aa", "bb", "cc"]
child1.names.push("dd");
console.log(child1.age);  // 18

var child2 = object(parent);
console.log(child2.names);  // ["aa", "bb", "cc", "dd"]
console.log(child2.age); // 18

原型式繼承其實就是對原型鏈繼承的一種封裝,它要求你有一個已有的對象作爲基礎,但是原型式繼承也有共享父類引用屬性,無法傳遞參數的缺點。

這個方法後來有了正式的 API: Object.create({...})

所以當有一個對象,想讓子實例繼承的時候,可以直接用 Object.create() 方法。

七、寄生式繼承

寄生式繼承是把原型式 + 工廠模式結合起來,目的是爲了封裝創建的過程。

function createAnother(original){ 
    var clone= object(original);    //通過調用函數創建一個新對象
    clone.sayHi = function(){      //以某種方式來增強這個對象
        console.log("hi");
    };
    return clone;                  //返回這個對象
}
 
var person = {
    age: 18,
    names: ["aa", "bb", "cc"]
};
 
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); // "hi"

八、 寄生組合式繼承

剛纔說到組合繼承有一個會兩次調用父類的構造函數造成浪費的缺點,寄生組合繼承就可以解決這個問題。

function inheritPrototype(subType, superType){
    var prototype = object(superType.prototype); // 創建了父類原型的淺複製
    prototype.constructor = subType;             // 修正原型的構造函數
    subType.prototype = prototype;               // 將子類的原型替換爲這個原型
}
 
function SuperType(age){
    this.age = age;
    this.names = ["aa", "bb", "cc"];
}
 
SuperType.prototype.sayName = function(){
    console.log(this.names);
};
 
function SubType(age){
    SuperType.call(this, age);
    this.age = age;
}
// 核心:因爲是對父類原型的複製,所以不包含父類的構造函數,也就不會調用兩次父類的構造函數造成浪費
inheritPrototype(SubType, SuperType);

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

var child1 = new SubType(22)
child1.sayAge()  // 22
child1.sayName()  // ["aa", "bb", "cc"]

九、ES6 class extends

class Parent {
    constructor(name) {
    this.name = name;
    }
    doSomething() {
            console.log('parent do something!');
    }
    sayName() {
        console.log('parent name:', this.name);
    }
}

class Child extends Parent {
    constructor(name, parentName) {
    super(parentName);
    this.name = name;
    }
    sayName() {
         console.log('child name:', this.name);
    }
}

const child = new Child('son', 'father');
child.sayName();            // child name: son
child.doSomething();        // parent do something!

const parent = new Parent('father');
parent.sayName();   // parent name: father

ES6 的 class extends 本質上是 ES5 的語法糖。
ES6實現繼承的具體原理:

class Parent {
}
 
class Child {
}
 
Object.setPrototypeOf = function (obj, proto) {
  obj.__proto__ = proto;
  return obj;
}
 
// B 的實例繼承 A 的實例
Object.setPrototypeOf(Child.prototype, parent.prototype);
 
// B 繼承 A 的靜態屬性
Object.setPrototypeOf(Child, Parent);

總結

javascript 由於歷史發展原因,繼承方式實際上是通過原型鏈屬性查找的方式,但正規的叫法不叫繼承而叫“委託”,ES6 的 class extends 關鍵字也不過是 ES5 的語法糖。所以,瞭解 JS 的原型和原型鏈非常重要,詳情請翻看我之前的文章《JavaScript原型與原型鏈》

參考:
《JavaScript 高級程序設計》

2019/02/10 @Starbucks

歡迎關注我的個人公衆號“謝南波”,專注分享原創文章。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章