類的繼承

我是leon發表於2018-07-30

本文講述JavaScript中類繼承的實現方式,並比較實現方式的差異。

一、何為繼承

繼承,是子類繼承父類的特徵和行為,使得子類物件具有父類的例項域和方法。 繼承是物件導向程式設計中,不可或缺的一部分。

1.1 優點

  • 減少程式碼冗餘 父類可以為子類提供通用的屬性,而不必因為增加功能,而逐個修改子類的屬性
  • 程式碼複用 同上
  • 程式碼易於管理和擴充套件 子類在父類基礎上,可以實現自己的獨特功能

1.2 缺點

  • 耦合度高 如果修改父類程式碼,將影響所有繼承於它的子類
  • 影響效能 子類繼承於父類的資料成員,有些是沒有使用價值的。但是,在例項化的時候,已經分配了記憶體。所以,在一定程度上影響程式效能。

二、例子

例子以圖書館中的書入庫歸類為例。 以下是簡化後的父類Book(也可稱為基類)。 目的是通過繼承該父類,產出Computer(計算機)子類。 並且,子類擁有新方法say,輸出自己的書名。

function Book(){
    this.name = ''; // 書名
    this.page = 0; // 頁數
    this.classify = ''; // 型別
}
Book.prototype = {
    constructor: Book,
    init: function(option){
        this.name = option.name || '';
        this.page = option.page || 0;
        this.classify = option.classify || '';
    },
    getName: function(){
        console.log(this.name);
    },
    getPage: function(){
        console.log(this.page);
    },
    getClassify: function(){
        console.log(this.classify);
    }
};
複製程式碼

接下來會講解子類Computer幾種繼承方式的實現和優化方法。開始飆車~

三、例項式繼承

function Computer(){
    Book.apply(this, arguments);
}
Computer.prototype = new Book();
Computer.prototype.constructor = Computer;
Computer.prototype.init = function(option){
    option.classify = 'computer';
    Book.prototype.init.call(this, option);
};
Computer.prototype.say = function(){
    console.log('I\'m '+ this.name);
}
複製程式碼

3.1 呼叫父類構造器進行初始化

function Computer(){
    Book.apply(this, arguments);
}
複製程式碼

Computer的建構函式裡,呼叫父類的建構函式進行初始化操作。使子類擁有父類一樣的初始化屬性。

3.2 將父類的原型傳遞給子類

Computer.prototype = new Book();使用new操作符對父類Book進行例項化,並將例項物件賦值給子類的prototype
這樣,子類Computer就可以通過原型鏈訪問到父類的屬性。

3.3 缺點

  • 父類Book的建構函式被執行了2次
    • 一次是在Computer的建構函式裡Book.apply(this, arguments);
    • 一次是在Computer.prototype = new Book();
      這種模式,存在一定的效能浪費。
  • 父類例項化無法傳參
    Computer.prototype = new Book();,這種例項化方式,無法讓Book父類接收不固定的引數集合。

四、原型式繼承

function Computer(){
    Book.apply(this, arguments);
}
Computer.prototype = Object.create(Book.prototype);
Computer.prototype.constructor = Computer;
Computer.prototype.init = function(option){
    option.classify = 'computer';
    Book.prototype.init(option);
};
Computer.prototype.say = function(){
    console.log('I\'m '+ this.name);
}
複製程式碼

這裡的改進:是使用Object.create(Book.prototype)。它的作用是返回一個繼承自原型物件Book.prototype的新物件。且該物件下的屬性已經初始化。 用Object.create生成新物件,並不會呼叫到Book的建構函式。 這種方式,也可以通過原型鏈實現繼承。

五、Object.create的簡單版相容

由於低版本的瀏覽器是不支援Object.create的。所以這裡簡單介紹下相容版本:

Object.create = function(prototype){
    function F(){}
    F.prototype = prototype;
    return new F();
}
複製程式碼

原理是定義一個空的建構函式,然後修改其原型,使之成為一個跳板,可以將原型鏈傳遞到真正的prototype。

六、函式化繼承

上述兩種實現方式,都存在一個問題:不存在私有屬性私有方法。也就是說,存在被篡改的風險。 接下來就用函式化來化解這個問題。

function book(spec, my){
    var that = {};

    // 私有變數
    spec.name = spec.name || ''; // 書名
    spec.page = spec.page || 0; // 頁數
    spec.classify = spec.classify || ''; // 型別

    var getName = function(){
        console.log(spec.name);
    };
    var getPage = function(){
        console.log(spec.page);
    };
    var getClassify = function(){
        console.log(spec.classify);
    };

    that.getName = getName;
    that.getPage = getPage;
    that.getClassify = getClassify;

    return that;
}

function computer(spec, my){
    spec = spec || {};
    spec.classify = 'computer';
    var that = book(spec, my);

    var say = function(){
        console.log('I\'m '+ spec.name);
    };
    that.say = say;

    return that;
}

var Ninja = computer({name: 'JavaScript忍者祕籍', page: 350});
複製程式碼

函式化的優勢,就是可以更好地進行封裝和資訊隱藏。
也許有人疑惑為什麼用以下這種方式宣告和暴露方法:

var say = function(){
    console.log('I\'m '+ spec.name);
};
that.say = say;
複製程式碼

其實是為了保護物件自身的完整性。即使that.say被外部篡改或破壞掉,function computer內部的say方法仍然能夠正常工作。 另外,解釋下thatspecmy的作用:

  • that是一個公開資料儲存容器,暴露出去的資料介面,都放到這個容器
  • spec是用來儲存建立新例項所需的資訊,屬於例項之間共同編輯的資料
  • my是用來儲存父類、子類之間共享的私密資料容器,外部是訪問不到的。

七、ES6繼承

最後,看下現代版ES6的類繼承。不禁感慨以前的刀耕火種,是多麼折磨人?

class Book {
    constructor(){
        this.name = ''; // 書名
        this.page = 0; // 頁數
        this.classify = ''; // 型別
    }
    init(option) {
        this.name = option.name || '';
        this.page = option.page || 0;
        this.classify = option.classify || '';
    }
    getName() {
        console.log(this.name);
    }
    getPage (){
        console.log(this.page);
    }
    getClassify (){
        console.log(this.classify);
    }
}
class Computer extends Book{
    constructor(...args){
        super(...args);
    }
    init(option) {
        super.init(option);
        this.classify = 'computer';
    }
    say() {
        console.log('I\'m '+ this.name);
    }
}
複製程式碼

結語

雖然ES5終究會被淘汰,但是瞭解下其工作原理,還是很有必要。因為很多原始碼還是有用到裡面的模式。
附帶的價值就是,ES5的繼承玩到飛起,ES6的繼承就是小菜一碟。


喜歡我文章的朋友,可以通過以下方式關注我:

相關文章