写在前面:

本文记录的几种继承方式,努力向基于class语法糖实现的面向对象靠拢

class Person {    constructor(friends) {        this.friends = friends == null ? [] : friends;    }    sayFriends() {        console.log(`${this.name}'s friends are ${this.friends}`);    }}class Man extends Person {    constructor(name, age, friends) {        super(friends);        this.name = name;        this.age = age;    }    sayName() {        console.log(this.name);    }    sayAge() {        console.log(this.age);    }}const xialuo = new Man('xialuo', 20, ['qiuya']);const yuanhua = new Man('yuanhua', 21, []);

以上代码可以表明我们的目的:

  1. 创建Person类,有属性friends,方法sayFriends
  2. 创建Man类,在子类调用父类构造函数(super),继承Person类,有属性name、age,方法sayName,sayAge
  3. friends不能共享
  4. xialuo、yuanhua是Man也是Person的实例


1. 原型链继承

将父类实例作为子类的原型对象

function Person(friends) {    this.friends = friends == null ? [] : friends;}Person.prototype.sayFriends = function () {    console.log(this.friends);}// 无法向父类构造函数传递参数,第2点未达成function Man(name, age, friends) {    this.name = name;    this.age = age;}// 原型链继承Man.prototype = new Person();Man.prototype.sayName = function () {    console.log(this.name)}Man.prototype.sayAge = function () {    console.log(this.age);}var xialuo = new Man('xialuo', 20, ['qiuya']);var yuanhua = new Man('yuanhua');// friends被共享,第3点未达成console.log(xialuo.friends); // []console.log(yuanhua.friends); // []xialuo.friends.push('teacherWang');console.log(xialuo.friends); // ['teacherWang']console.log(yuanhua.friends); // ['teacherWang']// xialuo、yuanhua是Man也是Person的实例,第4点达成console.log(xialuo instanceof Man); // trueconsole.log(xialuo instanceof Person); // true

缺点:

  1. 无法向父类构造函数传参
  2. 父类的引用类型friends被共享


2. 借用构造函数继承

借用父类构造函数增强子类

function Person(friends) {    this.friends = friends == null ? [] : friends;    this.personMethod = () => {        console.log('personMethod run')    }}Person.prototype.sayFriends = function () {    console.log(this.friends);}function Man(name, age, friends) {    Person.call(this, friends);    this.name = name;    this.age = age;}Man.prototype.sayName = function () {    console.log(this.name)}Man.prototype.sayAge = function () {    console.log(this.age);}var xialuo = new Man('xialuo', 20, ['qiuya']);var yuanhua = new Man('yuanhua');// 未继承到父类Person的原型方法,第2点未达成// xialuo.sayFriends(); // TypeError: xialuo.sayFriends is not a functionxialuo.personMethod(); // 'personMethod run'console.log(xialuo.personMethod === yuanhua.personMethod); // false// friends未被共享,第3点达成!console.log(xialuo.friends); // ['qiuya']console.log(yuanhua.friends); // []xialuo.friends.push('teacherWang');console.log(xialuo.friends); // [''qiuya, 'teacherWang']console.log(yuanhua.friends); // []// xialuo.__proto__和Person.prototype找不到同一个对象,第4点未达成console.log(xialuo instanceof Man); // trueconsole.log(xialuo instanceof Person); // false

优点:

  1. 解决原型链继承中无法向父类传参的问题

缺点

  1. instanceof 无法判断实例是父类的实例
  2. 无法继承父类Person的原型方法,只能继承父类的自有方法,且无法复用(构造方法造成的)

Lint:为什么要区分原型方法和自有方法?

function Man() {    // 自有方法sayHello    this.sayHello = () => {        console.log('hello')    }}Man.prototype.sayWorld = () => {    console.log('world');}const xialuo = new Man();const yuanhua = new Man();/** * 私有方法在每次调用构造函数new时都会重新创建,无法复用。 * * 解决办法是将方法添加到原型对象prototype,实例会从Man.prototype调用函数,实现函数复用 */console.log(xialuo.sayHello === yuanhua.sayHello); // falseconsole.log(xialuo.sayWorld === yuanhua.sayWorld); // true


3. 组合式继承

原型链模式 + 借用构造函数模式,集二者之长

function Person(friends) {    this.friends = friends == null ? [] : friends;}Person.prototype.sayFriends = function () {    console.log(this.friends);}// 子类属性处理function Man(name, age, friends) {    // 子类继承父类属性    Person.call(this, friends); // 借用构造函数    // 子类生成自有属性    this.name = name;    this.age = age;}// 子类方法处理Man.prototype = new Person(); // 原型链模式,继承父类属性Man.prototype.constructor = Man; // lint: 修复因原型链模式改变的子类构造函数(不然会变成Person)// 子类自有方法Man.prototype.sayName = function () {    console.log(this.name)}Man.prototype.sayAge = function () {    console.log(this.age);}var xialuo = new Man('xialuo', 20, ['qiuya']);var yuanhua = new Man('yuanhua');// friends未被共享,第3点达成!console.log(xialuo.friends); // ['qiuya']console.log(yuanhua.friends); // []xialuo.friends.push('teacherWang');console.log(xialuo.friends); // [''qiuya, 'teacherWang']console.log(yuanhua.friends); // []console.log(xialuo instanceof Man); // trueconsole.log(xialuo instanceof Person); // true
优点:
集原型链模式和借用构造函数的优点

缺点:
创建一个xialuo实例,却调用两次构造函数

目前,我们还需要解决组合式继承的缺点,以得到js继承的最佳实践


4. 原型式继承

function object(o) {    function Fn() { }    Fn.prototype = o;    return new Fn();}function Person(name, age, friends) {    this.name = name;    this.age = age;    this.friends = friends == null ? [] : friends;}var p = new Person('xialuo', 20, ['qiuya']);var xialuo = object(p);p = new Person('yuanhua', 21, []);var yuanhua = object(p);

可以看到,object函数接收一个对象obj,作为临时对象Fn的原型对象,最终返回Fn。

也就是说,xialuo,yuanhua的属性,全在xialuo.__proto__,yuanhua.__proto__,而不是实例对象xialuo,yuanhua上。

这通常会导致原型指针混乱而造成this指向不明的问题。—— 维尔希宁

对了,ES5提供一个方法Object.create()替代了上述的object函数,内部实现是一样的。

<br>

5. 寄生组合式继承

寄生组合式继承是对组合式继承的改造,结合原型式继承,目的是解决组合式继承中两次调用父类构造函数的问题。

function object(obj) {    function Fn() { }    Fn.prototype = obj;    return new Fn();}function Person(friends) {    this.friends = friends == null ? [] : friends;}Person.prototype.sayFriends = function () {    console.log(this.friends);}function Man(name, age, friends) {    Person.call(this, friends);    this.name = name;    this.age = age;}Man.prototype = object(Person.prototype); // 在此处,减少一次父类构造函数调用Man.prototype.constructor = Man;Man.prototype.sayName = function () {    console.log(this.name)}Man.prototype.sayAge = function () {    console.log(this.age);}var xialuo = new Man('xialuo', 20, ['qiuya']);var yuanhua = new Man('yuanhua');console.log(xialuo.friends); // ['qiuya']console.log(yuanhua.friends); // []xialuo.friends.push('teacherWang');console.log(xialuo.friends); // [''qiuya, 'teacherWang']console.log(yuanhua.friends); // []console.log(xialuo instanceof Man); // trueconsole.log(xialuo instanceof Person); // true

收工!

本文的思想和代码参考:

JS实现继承的几种方式 - 幻天芒 - 博客园

面向对象的 JavaScript:封装、继承与多态