JavaScript类的继承

114 阅读3分钟

1. ES5类的三种继承方式

一:原型链继承: 通过让子类的原型对象指向父类的实例,实现继承父类原型上的方法。会导致子类共享父类原型。后续父类原型修改时可能会影响到子类。

function Parent() {
    this.name = "Parent";
}

Parent.prototype.sayHello = function() {
    console.log("Hello, " + this.name);
}

function Child() {
    this.age = 5;
}

Child.prototype = new Parent();

二:构造函数继承:通过在子类的构造函数中调用父类的构造函数,从而继承父类的属性(this的指向改变)。缺点是无法继承父类原型上的方法,只能继承属性。

function Parent() {
    this.name = "Parent";
}

Parent.prototype.sayHello = function() {
    console.log("Hello, " + this.name);
}

function Child() {
    Parent.call(this);
    this.age = 5;
}

let c1 = new Child()
c1.sayHello(); // c1.sayHello is not a function

// 错误解释:Parent 构造函数仅设置了实例的属性name。当我们在 Child 构造函数中调用 Parent.call(this, name) 时,只是将 Parent 构造函数中的属性初始化到 Child 实例上,并没有涉及到 Parent.prototype 上的方法。

// 明确:Parent.call(this) 只会执行 Parent 构造函数的代码。

三:混合继承:结合构造函数继承和原型链继承的方式,通过调用父类构造函数继承属性,同时将子类原型对象指向一个新的对象,来继承父类原型的方法

通过使用 构造函数继承 解决了 属性继承的问题;通过使用 原型链继承,解决了方法继承的问题;

function Parent() {
    this.name = 'Parent';
}

Parent.prototype.sayHello = function () {
    console.log('Hello, ' + this.name);
};

function oneChild(name, age) {
    Parent.call(this, name);
    this.age = age;
}
oneChild.prototype = Object.create(Parent.prototype);
let c2 = new oneChild('tom', 21);
c2.sayHello();

// 混合继承:通过使用 构造函数继承解决了 属性继承的问题;通过使用原型链继承,解决了方法继承的问题;

2. ES6 中 class 的继承

extends 关键字, 用于创建一个类的子类,从而继承父类的所有属性和方法。

super 关键字有两种主要用途:

1、在子类的构造函数中调用父类的构造函数 2、在子类的方法中,调用父类的方法。super.parentMethod()

class Parent {
    constructor(name) {
        this.name = name;
    }
    
    sayHello() {
        console.log('Hello, ' + this.name);
    }
}

class Child extends Parent {
    constructor(name, age) {
        // 通过 super 调用父类的构造函数
        super(name);
        this.age = age;
    }
    
    introduce() {
        // 通过 super 调用父类方法
        super.sayHello()
        console.log(`I am ${this.name} and I am ${this.age} `)
    }
}

3. 自定义实现类的多继承

3.1 使用 mixins 混入

混入是一种将一个对象的属性和方法复制到另一个对象上的技术

// 基础类
class Parent1 {
    sayHello() {
        console.log("Hello from Parent1");
    }
}

// 基础类
class Parent2 {
    sayHi() {
        console.log("Hello from Parent2");
    }
}

// 目标类
class Child {
    constructor(name) {
        this.name = name
    }
}

const mixins = (target, ...sources) => {
    sources.map(source => {
        Object.getOwnPropertyNames(source).map(name => {
            if(name !== 'constructor') {
                target.prototype[name] = source[name]
            }
        })
    })
}

mixins(Child, Parent1.prototype, Parent2.prototype)

const tom = new Child('Tom')
tom.sayHello()
tom.sayHi()

3.2 使用 ES6类的混入

通过函数来实现混入,并使用ES6类语法。

/**
    * Object.getOwnPropertyDescriptor(obj, prop) 返回一个对象,该对象描述给定对象上特定属性的配置(不包含原型对象上的属性)
    *  - obj: 要查找其属性的对象。
    *  - prop:要检索其描述的属性的名称
    *  - return: 如果指定的属性存在于对象上,则返回其属性描述符
    *
    *
    * Object.defineProperty(obj, prop, descriptor) 在一个对象上定义一个新属性,或修改其现有属性,并返回此对象。
    *  - obj: 要定义属性的对象。
    *  - prop: 一个字符串或 Symbol,指定了要定义或修改的属性键。
    *  - descriptpr: 要定义或修改的属性的描述符。
    *  - return: 传入函数的对象,其指定的属性已被添加或修改。
*/

const mixins = (Base, ...Mixins) => {
  class Mix extends Base {
    constructor(...args) {
      super(...args);
      Mixins.forEach(Mixin => {
        copyProperties(this, new Mixin());
      });
    }
  }

  Mixins.forEach(Mixin => {
    copyProperties(Mix.prototype, Mixin.prototype);
    copyProperties(Mix, Mixin);
  });

  return Mix;
};

function copyProperties(target, source) {
  for (let key of Reflect.ownKeys(source)) {
    if (key !== 'constructor' && key !== 'prototype' && key !== 'name') {
      let desc = Object.getOwnPropertyDescriptor(source, key);
      Object.defineProperty(target, key, desc);
    }
  }
}

// 基础类
class Parent1 {
  sayHello() {
    console.log('Hello from Parent1');
  }
}
// 基础类
class Parent2 {
  sayGoodbye() {
    console.log('Goodbye from Parent2');
  }
}

class Base {}
class Child extends mixins(Base, Parent1, Parent2) {
  constructor(name) {
    super();
    this.name = name;
  }
}

const child = new Child('Tom');
child.sayHello();
child.sayGoodbye();