深入理解继承的实现和优缺点

283 阅读8分钟

实现继承前必须先储备原型/原型链/类和构造函数知识

引入继承

继承是面向对象当中的一个概念。如果一个类别B继承自另一个类别A,就把这个B称为A的子(派生)类,而把A称为B的父(超)类。继承可以使得子类具有父类别的各种属性和方法,而不需要再次编写相同的代码

在下面代码中可以看到PersonStudent有很多的相似之处,Person有的在Student中都有,这时候我们就可以让Student继承自Person,减少Student中和Person相同的代码,让其通过Studen创建的实例对象也有通过Person创建的实例对象里的方法和属性

function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.running = function () {
  console.log(this.name + "在running");
};

function Student(name, age, school, score) {
  this.name = name;
  this.age = age;
  this.school = school;
  this.score = score;
}
Student.prototype.running = function () {
  console.log(this.name + "在running");
};
Student.prototype.jumping = function () {
  console.log(this.name + "在jumping");
};
var s1 = new Student("bob", 18, "清华", 750);
s1.jumping()

继承应该达到的状态:

  • 子类可以使用父类中的属性和方法
  • 子类不同的实例之间不会互相影响
  • 子类实例能够向父类传参
  • 能实现多继承(一个子类可继承多个父类)
  • 父类的方法能被复用(不会过多的消耗内存),而不是每创建一个子类实例都生成一份父类方法

原型链实现继承

  • 方式一Student.prototype = Person.prototype,缺点如下:

    • 因为子类和父类是共享一个原型的,它们两个会互相影响,子类修改原型方法时,父类也会修改
  • 方式二Student.prototype.__proto__ = Person.prototype ,缺点如下:

    • __proto__不是标准,可能有兼容问题
    • 直接修改 __proto__ 可能会导致性能问题,V8 引擎团队曾经表示,频繁修改 __proto__ 可能会导致引擎优化失效,从而影响性能
  • 方式三var prototype = Object.getPrototypeOf(Student.prototype); Object.setPrototypeOf(prototype, Person.prototype),缺点如下:

    • 代码可读性差,间接修改原型链,可能引入意想不到的副作用,也影响 JavaScript 引擎的优化
  • 方式四Student.prototype = new Person(),本质其实是方式二但不是直接修改,而是创建新的实例并赋值

    • 不能继承父类的属性,子类通过new创建的实例s1对象里面没有name属性和age属性
    function Person(name, age) {
      this.name = name;
      this.age = age;
    }
    Person.prototype.running = function () {
      console.log(this.name + "在running");
    };
    
    function Student(school, score) {
      this.school = school;
      this.score = score;
    }
    
    // Student.prototype = Person.prototype; // 方式一
    // Student.prototype.__proto__ = Person.prototype // 方式二
    // 方式三
    // var prototype = Object.getPrototypeOf(Student.prototype);
    // Object.setPrototypeOf(prototype, Person.prototype)
    Student.prototype = new Person(); // 方式四
    Student.prototype.jumping = function () {
      console.log(this.name + "在jumping");
    };
    var s1 = new Student("清华", 750);
    s1.running();
    console.log(s1) // {school: '清华', score: 750}
    

    方式1代码原型图示如下

    image.png

    方式2、3和4代码原型图示如下:

image.png

缺点

  • 不能继承父类的属性,子类通过new创建的实例s1对象里面没有name属性和age属性

借用构造函数继承属性

也称经典继承,此方法只实现了属性的继承,在子类构造函数的内部通过apply()call()方法调用父类构造函数

  • 解决了s1对象里面没有name属性和age属性的问题
    function Person(name, age) {
      this.name = name;
      this.age = age;
    }
    Person.prototype.running = function () {
      console.log(this.name + "在running");
    };
    
    function Student(school, score) {
      Person.call(this, name, age); // 借用构造函数
      this.school = school;
      this.score = score;
    }
    Student.prototype.jumping = function () {
      console.log(this.name + "在jumping");
    };
    var s1 = new Student("bob", 18, "清华", 750);
    console.log(s1) // {name: 'bob', age: 18, school: '清华', score: 750}
    s1.running() // 报错:s1.running is not a function
    

缺点

  • 只能继承父类的实例属性和方法,不能继承原型链上的属性和方法

组合继承

组合继承结合了原型链继承和借用构造函数的优点,既能继承父类的实例属性,又能继承原型链上的方法

function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.running = function () {
  console.log(this.name + "在running");
};

function Student(school, score) {
  Person.call(this, name, age); // 借用构造函数
  this.school = school;
  this.score = score;
}
Student.prototype = new Person(); // 原型链
Student.prototype.jumping = function () {
  console.log(this.name + "在jumping");
};
var s1 = new Student("bob", 18, "清华", 750);
s1.running();
console.log(s1) // {name: 'bob', age: 18, school: '清华', score: 750}

缺点父类构造函数会被调用两次 ,所有的子类实例事实上会拥有两份父类的属性,Student.prototype 上会有 nameage 属性,这些属性属于 Person 的实例,不应该出现在 Student.prototype

原型式继承方法

这种模式要从道格拉斯·克罗克福德(著名的前端大师,JSON的创立者)在2006年写的一篇文章说起: 在JavaScript中使用原型式继承

  • 之所以称为原型式继承,因为这种继承机制是通过对象的原型来实现的
  • 直接创建新对象,而不是通过 Student.prototype = new Person()
  • 我们先要理解他要创建的对象需要具备什么条件:
    • 必须创建出来一个对象(可以是new的实例可以是普通对象)

    • 这个对象的__proto__必须指向父类的prototype

    • 需要将这个对象赋值给子类的prototype

  • 这种方式解决了组合继承的缺点
function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.running = function () {
  console.log(this.name + "在running");
};
function Student(name, age, school, score) {
  Person.call(this, name, age); // 借用构造函数
  this.school = school;
  this.score = score;
}

/* 
  函数object做的操作:
    创建一个新对象并返回
    让这个对象的__proto__指向父类的prototype
*/
function object(superPrototype) {
  // 方式一:道格拉斯·克罗克福德的方法,无任何兼容性问题
  // function F() {}
  // F.prototype = superPrototype;
  // return new F();

  // 方式二
  // var obj = {};
  // // obj.__proto__ = superPrototype; // 可能有兼容性问题
  // Object.setPrototypeOf(obj, superPrototype);
  // return obj;

  // 方式三
  return Object.create(superPrototype);
}
Student.prototype = object(Person.prototype);
Student.prototype.constructor = Student
Student.prototype.jumping = function () {
  console.log(this.name + "在jumping");
};

var s1 = new Student("bob", 18, "清华", 750);
s1.running();
console.log(s1) // {name: 'bob', age: 18, school: '清华', score: 750}

代码原型图示如下: image.png

缺点

  • 注意这种方法constructor 属性丢失,需要手动设置过来。
  • 但当不止Student继承自Person时,还有Teachpolice等等也继承自Person,这时就需要重复写很多遍3435行的代码,那和工厂模式结合就有了下面的继承方式

寄生式继承

寄生式继承是与原型式继承紧密相关的一种思想,并且同样由道格拉斯·克罗克福德提出和推广

  • 寄生式继承的思路是结合原型式继承和工厂模式的一种方式
  • 创建一个封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再将这个对象返回
// 子类和父类寄生于这个函数实现继承
function inheritPrototype(subType, superType) {
  var obj = Object.create(superType.prototype);
  obj.constructor = subType; // 增强对象的constructor属性
  subType.prototype = obj; // 赋值对象
}
function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.running = function () {
  console.log(this.name + "在running");
};
function Student(name, age, school, score) {
  Person.call(this, name, age); // 借用构造函数
  this.school = school;
  this.score = score;
}

inheritPrototype(Student, Person);
Student.prototype.jumping = function () {
  console.log(this.name + "在jumping");
};

var s1 = new Student("bob", 18, "清华", 750);
s1.running();
console.log(s1) // {name: 'bob', age: 18, school: '清华', score: 750}
  • 这种方式已经很好地实现了继承,下面这种方式是结合原型式和寄生式进行的封装优化

寄生组合式继承(ES5最终方案)

这种方式是结合原型式和寄生式进行的封装优化,是ES5实现继承的最终方案

/* 
  函数object做的操作:
    创建一个新对象并返回
    让这个对象的__proto__指向父类的prototype
*/
function object(superPrototype) {
  // 兼容性判断
  if (typeof Object.create !== 'function') { 
   Object.create = function (proto) { 
      function F() {} 
      F.prototype = proto; 
      return new F(); 
    }; 
  }
  return Object.create(superPrototype)
}
// 子类和父类寄生于这个函数实现继承
function inheritPrototype(subType, superType) {
  var obj = object(superType.prototype);
  obj.constructor = subType; // 增强对象的constructor属性
  subType.prototype = obj; // 赋值对象
}
function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.running = function () {
  console.log(this.name + "在running");
};
function Student(name, age, school, score) {
  Person.call(this, name, age); // 借用构造函数
  this.school = school;
  this.score = score;
}

inheritPrototype(Student, Person);
Student.prototype.jumping = function () {
  console.log(this.name + "在jumping");
};

var s1 = new Student("bob", 18, "清华", 750);
s1.running();
console.log(s1) // {name: 'bob', age: 18, school: '清华', score: 750}

ES6类的继承

使用extends关键字,是ES5继承的语法糖

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  running() {
    console.log(this.name + "在running");
  }
  static jumping() {
    console.log(this.name + "在jumping");
  }
}

class Student extends Person {
  constructor(name, age, school, score) {
    super(name, age);
    this.school = school;
    this.score = score;
  }
  study() {
    console.log(`${this.name}study`);
  }
  static talk() {
    console.log(`${this.name}talk`);
  }
}
var s1 = new Student("bob", 20, "清华", 750);
s1.running(); 
s1.study();

转换后成ES5代码后:

function _callSuper(t, o, e) {
  return (
    (o = _getPrototypeOf(o)),
    _possibleConstructorReturn(
      t,
      _isNativeReflectConstruct()
        ? Reflect.construct(o, e || [], _getPrototypeOf(t).constructor)
        : o.apply(t, e)
    )
  );
}
function _possibleConstructorReturn(t, e) {
  if (e && ("object" == _typeof(e) || "function" == typeof e)) return e;
  if (void 0 !== e)
    throw new TypeError(
      "Derived constructors may only return object or undefined"
    );
  return _assertThisInitialized(t);
}
function _assertThisInitialized(e) {
  if (void 0 === e)
    throw new ReferenceError(
      "this hasn't been initialised - super() hasn't been called"
    );
  return e;
}
function _isNativeReflectConstruct() {
  try {
    var t = !Boolean.prototype.valueOf.call(
      Reflect.construct(Boolean, [], function () {})
    );
  } catch (t) {}
  return (_isNativeReflectConstruct = function _isNativeReflectConstruct() {
    return !!t;
  })();
}
function _getPrototypeOf(t) {
  return (
    (_getPrototypeOf = Object.setPrototypeOf
      ? Object.getPrototypeOf.bind()
      : function (t) {
          return t.__proto__ || Object.getPrototypeOf(t);
        }),
    _getPrototypeOf(t)
  );
}
function _inherits(t, e) {
  if ("function" != typeof e && null !== e)
    throw new TypeError("Super expression must either be null or a function");
  (t.prototype = Object.create(e && e.prototype, {
    constructor: { value: t, writable: !0, configurable: !0 }
  })),
    Object.defineProperty(t, "prototype", { writable: !1 }),
    e && _setPrototypeOf(t, e);
}
function _setPrototypeOf(t, e) {
  return (
    (_setPrototypeOf = Object.setPrototypeOf
      ? Object.setPrototypeOf.bind()
      : function (t, e) {
          return (t.__proto__ = e), t;
        }),
    _setPrototypeOf(t, e)
  );
}
function _typeof(o) {
  "@babel/helpers - typeof";
  return (
    (_typeof =
      "function" == typeof Symbol && "symbol" == typeof Symbol.iterator
        ? function (o) {
            return typeof o;
          }
        : function (o) {
            return o &&
              "function" == typeof Symbol &&
              o.constructor === Symbol &&
              o !== Symbol.prototype
              ? "symbol"
              : typeof o;
          }),
    _typeof(o)
  );
}
function _classCallCheck(a, n) {
  if (!(a instanceof n))
    throw new TypeError("Cannot call a class as a function");
}
function _defineProperties(e, r) {
  for (var t = 0; t < r.length; t++) {
    var o = r[t];
    (o.enumerable = o.enumerable || !1),
      (o.configurable = !0),
      "value" in o && (o.writable = !0),
      Object.defineProperty(e, _toPropertyKey(o.key), o);
  }
}
function _createClass(e, r, t) {
  return (
    r && _defineProperties(e.prototype, r),
    t && _defineProperties(e, t),
    Object.defineProperty(e, "prototype", { writable: !1 }),
    e
  );
}
function _toPropertyKey(t) {
  var i = _toPrimitive(t, "string");
  return "symbol" == _typeof(i) ? i : i + "";
}
function _toPrimitive(t, r) {
  if ("object" != _typeof(t) || !t) return t;
  var e = t[Symbol.toPrimitive];
  if (void 0 !== e) {
    var i = e.call(t, r || "default");
    if ("object" != _typeof(i)) return i;
    throw new TypeError("@@toPrimitive must return a primitive value.");
  }
  return ("string" === r ? String : Number)(t);
}
var Person = /*#__PURE__*/ (function () {
  function Person(name, age) {
    _classCallCheck(this, Person);
    this.name = name;
    this.age = age;
  }
  return _createClass(
    Person,
    [
      {
        key: "running",
        value: function running() {
          console.log(this.name + "在running");
        }
      }
    ],
    [
      {
        key: "jumping",
        value: function jumping() {
          console.log(this.name + "在jumping");
        }
      }
    ]
  );
})();
var Student = /*#__PURE__*/ (function (_Person) {
  function Student(name, age, school, score) {
    var _this;
    _classCallCheck(this, Student);
    _this = _callSuper(this, Student, [name, age]);
    _this.school = school;
    _this.score = score;
    return _this;
  }
  _inherits(Student, _Person);
  return _createClass(
    Student,
    [
      {
        key: "study",
        value: function study() {
          console.log("".concat(this.name, "study"));
        }
      }
    ],
    [
      {
        key: "talk",
        value: function talk() {
          console.log("".concat(this.name, "talk"));
        }
      }
    ]
  );
})(Person);
var s1 = new Student("bob", 20, "清华", 750);
s1.running();
s1.study();