JavaScript進階之繼承

EdwardXuan發表於2019-03-20

導讀

封裝繼承多型是物件導向的三大特性。

繼承是JavaScript老生常談的問題,也基礎中的基礎。最基本的就是知道JS中繼承種類,優缺點並能夠手寫例子。下面我引用掘金Oliveryoung老兄一篇文章的思維導圖,他的這篇文章寫的很棒。可以點選

JavaScript進階之繼承
繼承相關的文章實在是過多,糾結了很久還是落筆,主要原因是:

  1. 繼承也是JavaScript進階系列的組成部分,同時也是自己對於知識點的梳理;
  2. 對於剛好看到這篇文章的小夥伴,可以溫故知新;
  3. 把自己的理解寫出來,有錯誤的能夠指出,一起進步;

下面我以思維導圖為依據,根據不同思路,給出不同的繼承方式,掃盲這塊知識點。

原型鏈繼承

利用JavaScript原型鏈的,把父物件鏈到子物件的原型中一些特性,我舉如下的例子,並給出程式碼:

//父類物件
var Animal = function(){
    this.compose = ["head","body","legs"]; //小動物的組成部分
    this.statistics = {count:0}; //小動物的數量
    this.category = "animal";
}
//子類物件        
var Cat = function(name){
      this.category = "cat";
      this.name = name; //小動物名稱
}

Cat.prototype = new Animal(); //原型鏈繼承
        
var tom = new Cat("Tom");
tom.compose.push("blue eyes");
tom.statistics.count++;
console.log(tom);
複製程式碼

列印結果如下:

JavaScript進階之繼承

下面是物件之間的關聯關係。

JavaScript進階之繼承

tom貓的_proto_屬性指向Cat.prototype,而Cat.prototype = new Animal()Animal物件的"一切"都繼承了下來

我們接著執行如下程式碼:

 var jemmy = new Cat("jemmy");
 jemmy.compose.push("black eyes");
 jemmy.statistics.count++
 console.log(jemmy);
複製程式碼

列印結果:

JavaScript進階之繼承

公有引用屬性(statistics)沒有問題,但是私有引用屬性(compose)被汙染,所以原型鏈繼承缺點:

  1. 汙染私有引用屬性
  2. 無法向父物件傳參

構造器繼承

既然私有引用屬(compose)屬性不能掛載在Cat.prototype,那我們把它掛載在自雷物件上,使用call或者apply來改變context,不熟悉callapply請猛戳這裡

//父類
var Animal = function (category) {
    this.compose = ["head", "body", "legs"];
    this.statistics = { count: 0 };
    this.category = category || "animal";
}

//子類
var Cat = function (name) {
    Animal.call(this,"cat");
    this.name = name;
}


var tom = new Cat("Tom");
tom.compose.push("blue eyes");
tom.statistics.count++;
console.log(tom);
var jemmy = new Cat("Jemmy");
jemmy.compose.push("black eyes");
jemmy.statistics.count++
console.log(jemmy);
複製程式碼

列印結果如下:

JavaScript進階之繼承
物件之間的關係圖如下:

JavaScript進階之繼承

構造器繼承解決了原型鏈繼承的兩大問題,但是又暴露了公有引用屬性不能共享的問題。矯枉過正!

我們把公有引用屬性使用原型鏈繼承,私有引用屬性使用構造器繼承,引出組合繼承(構造器+原型組合繼承

組合繼承

我們將compose屬性掛載在物件屬性上,statistics屬性掛載在原型上,結合前兩種繼承:

    //父類
    var Animal = function (category) {
        //私有屬性
        this.compose = ["head", "body", "legs"];
        this.category = category || "animal";
    }
    Animal.prototype.statistics = {count: 0}; //公有屬性放在父物件的原型上

    //子類
    var Cat = function (name) {
        Animal.call(this,"cat");//將非共享的屬性通過call加入到子物件中
        this.name = name;
    }

    Cat.prototype =  Animal.prototype; //掛載到子物件的原型中
    // console.log(Cat.prototype.constructor == Animal)  //true
    Cat.prototype.constructor = Cat;//糾正子物件的建構函式

    var tom = new Cat("Tom");
    tom.compose.push("blue eyes");
    tom.statistics.count++;
    console.log(tom);
    var jemmy = new Cat("Jemmy");
    jemmy.compose.push("black eyes");
    jemmy.statistics.count++
    console.log(jemmy);
複製程式碼

列印結果:

JavaScript進階之繼承
物件之間的關係圖如下:
JavaScript進階之繼承
融合原型鏈繼承和建構函式繼承的優點,也是常見的繼承策略。

原型繼承

原型繼承主要利用ES5出現的Object.create()函式,這裡改寫一下上述的例子:

var Animal = function (category) {
            this.compose = ["head", "body", "legs"];
            this.category = category || "animal";
            this.statistics = {count: 0}; 
}

var animal = new Animal();
var tom = Object.create(animal);
tom.name = "Tom";
tom.category = "cat";
tom.compose.push("blue eyes");
tom.statistics.count++;
console.log(tom);

var jemmy = Object.create(animal);
jemmy.name = "Jemmy";
jemmy.category = "cat";
jemmy.compose.push("black eyes");
jemmy.statistics.count++;
console.log(jemmy);
複製程式碼

物件結構如下:

JavaScript進階之繼承
原型繼承和原型鏈繼承一樣,只是使用Object.create()的方式進行繼承。

寄生式繼承

鑑於原型式繼承的封裝性不是很好,寄生式繼承主要用於解決這個問題。

    //父類
    var Animal = function (category) {
        this.compose = ["head", "body", "legs"];
        this.category = category || "animal";
        this.statistics = { count: 0 };
    }

    //把原型式繼承封裝成一個create函式
    function create(parent, name, category, eyes) {
        let obj = Object.create(parent);
        obj.name = name;
        obj.category = category;
        obj.compose.push(eyes);
        obj.statistics.count++;
        return obj;
    }

    var animal = new Animal();
    var tom = create(animal, "Tom", "cat", "blue eyes");
    console.log(tom);
    var jemmy = create(animal, "Jemmy", "cat", "black eyes");
    console.log(jemmy);
複製程式碼

這種方式比原型式繼承封裝性更好。但是缺點還是沒解決的。在寄生式基礎上,結合構造器繼承,就是寄生組合式繼承

寄生組合式繼承

寄生組合式繼承,更多了還是利用組合式繼承的思想。

    //父類
    var Animal = function (category) {
        //私有屬性
        this.compose = ["head", "body", "legs"];
        this.category = category || "animal";
    }

    Animal.prototype = {
        //公有屬性,還是放在原型上
        statistics : { count: 0 }
    }

    var Cat = function(name){
        //把私有屬性通過call繼承過來
        Animal.call(this,"cat");
        this.name = name;
    }

    function proto(Son,Parent) {
        //其實就是把組合繼承的原型部分封裝了一下
        Son.prototype = Object.create(Parent.prototype);
        Son.prototype.constructor = Son;
    }
    proto(Cat,Animal);
       
    var tom = new Cat("Tom");
    tom.compose.push("blue eyes");
    tom.statistics.count++;
    console.log(tom);
    var jemmy = new Cat("Jemmy");
    jemmy.compose.push("black eyes");
    jemmy.statistics.count++
        console.log(jemmy);
複製程式碼

列印結果和組合式繼承一致:

JavaScript進階之繼承

總結一下,JavaScript的繼承的思想主要由下面兩條構成:

  1. 將私有屬性通過call/apply在子物件建構函式中呼叫,直接繼承
  2. 將公有屬性通過原型鏈繼承

組合式繼承和寄生組合式繼承只是實現方式的不同,思想是一致的。

ES6

classextends的出現,使繼承變得簡單!

        class Animal{
            constructor(category){
                this.compose = ["head", "body", "legs"];
                this.category = category || "animal";
            }
        }

        Animal.prototype.statistics =  { count: 0 }

        class Cat extends Animal{
            constructor(name){
                super("cat");
                this.name = name;
            }
        }
        var tom = new Cat("Tom");
        tom.compose.push("blue eyes");
        tom.statistics.count++;
        console.log(tom);

        var jemmy = new Cat("Jemmy");
        jemmy.compose.push("black eyes");
        jemmy.statistics.count++
        console.log(jemmy);
複製程式碼

列印結果如下:

JavaScript進階之繼承
我們使用babel轉成ES5語法,快速轉換地址這裡: 轉換結果如下:

"use strict";

function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }

function _possibleConstructorReturn(self, call) { if (call && (_typeof(call) === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); }

function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }

function _getPrototypeOf(o) { 
    _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : 
    function _getPrototypeOf(o) { 
        return o.__proto__ || Object.getPrototypeOf(o); 
    };
    return _getPrototypeOf(o); 
}

function _inherits(subClass, superClass) { 
    if (typeof superClass !== "function" && superClass !== null) { 
        throw new TypeError("Super expression must either be null or a function"); 
    } 
    //子類的原型指向一個以(subClass為建構函式,superClass.prototype中的物件屬性)的物件
    subClass.prototype = Object.create(superClass && superClass.prototype, { 
        constructor: { value: subClass, writable: true, configurable: true } }); 
    if (superClass) _setPrototypeOf(subClass, superClass); 
}

function _setPrototypeOf(o, p) { 
    _setPrototypeOf = Object.setPrototypeOf || 
        function _setPrototypeOf(o, p) 
    	{ o.__proto__ = p; return o; }; 
    return _setPrototypeOf(o, p); 
}

function _instanceof(left, right) { if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) { return right[Symbol.hasInstance](left); } else { return left instanceof right; } }

function _classCallCheck(instance, Constructor) { if (!_instanceof(instance, Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var Animal = function Animal(category) {
  _classCallCheck(this, Animal);

  this.compose = ["head", "body", "legs"];
  this.category = category || "animal";
};

Animal.prototype.statistics = {
  count: 0
};

var Cat =
/*#__PURE__*/
function (_Animal) {
  _inherits(Cat, _Animal);

  function Cat(name) {
    var _this;

    _classCallCheck(this, Cat);

    _this = _possibleConstructorReturn(this, _getPrototypeOf(Cat).call(this, "cat"));
    _this.name = name;
    return _this;
  }

  return Cat;
}(Animal);
複製程式碼

大家可以仔細看看這段程式碼,寫的挺有意思的,看懂了基本上也就理解繼承了。在這裡 _inherits(Cat, _Animal)跟寄生組合式繼承中的proto(Cat,Animal)一樣,_getPrototypeOf(Cat).call(this, "cat")Animal.call(this,"cat");也一樣,值得一提的是ES6中的super(xxx)就是將父類的建構函式使用call進行傳參。

參考

  1. JavaScript深入之繼承的多種方式和優缺點
  2. 一文看懂 JS 繼承

相關文章