导读

封装继承多态是面向对象的三大特性。

继承是JavaScript老生常谈的问题,也基础中的基础。最基本的就是知道JS中继承种类,优缺点并能够手写例子。下面我引用掘金Oliveryoung老兄一篇文章的思维导图,他的这篇文章写的很棒。可以点击

JavaScript进阶之继承 - 图1
继承相关的文章实在是过多,纠结了很久还是落笔,主要原因是:

  1. 继承也是JavaScript进阶系列的组成部分,同时也是自己对于知识点的梳理;
  2. 对于刚好看到这篇文章的小伙伴,可以温故知新;
  3. 把自己的理解写出来,有错误的能够指出,一起进步;

下面我以思维导图为依据,根据不同思路,给出不同的继承方式,扫盲这块知识点。

原型链继承

利用JavaScript原型链的,把父对象链到子对象的原型中一些特性,我举如下的例子,并给出代码:

  1. //父类对象
  2. var Animal = function(){
  3. this.compose = ["head","body","legs"]; //小动物的组成部分
  4. this.statistics = {count:0}; //小动物的数量
  5. this.category = "animal";
  6. }
  7. //子类对象
  8. var Cat = function(name){
  9. this.category = "cat";
  10. this.name = name; //小动物名称
  11. }
  12. Cat.prototype = new Animal(); //原型链继承
  13. var tom = new Cat("Tom");
  14. tom.compose.push("blue eyes");
  15. tom.statistics.count++;
  16. console.log(tom);

打印结果如下:

JavaScript进阶之继承 - 图2

下面是对象之间的关联关系。

JavaScript进阶之继承 - 图3

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进阶之继承 - 图4

公有引用属性(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进阶之继承 - 图5
对象之间的关系图如下:

JavaScript进阶之继承 - 图6

构造器继承解决了原型链继承的两大问题,但是又暴露了公有引用属性不能共享的问题。矫枉过正!

我们把公有引用属性使用原型链继承,私有引用属性使用构造器继承,引出组合继承(构造器+原型组合继承

组合继承

我们将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进阶之继承 - 图7
对象之间的关系图如下:
JavaScript进阶之继承 - 图8
融合原型链继承和构造函数继承的优点,也是常见的继承策略。

原型继承

原型继承主要利用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进阶之继承 - 图9
原型继承和原型链继承一样,只是使用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进阶之继承 - 图10

总结一下,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进阶之继承 - 图11
我们使用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 继承