【JS】几种继承方式

207 阅读5分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

之前在写 C++ 和 JAVA 这类面向对象语言时,总会遇到对象之间的继承,它可以分为接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。

由于 JS 中方法没有签名,在 ECMAScript 中无法实现接口继承,ECMAScript 只支持实现继承,其实现继承主要是依靠原型链来实现的。

JS 中构造函数、原型链和实例的关系:每个构造函数都有一个原型对象prototype,每一个原型对象都包含一个指向构造函数的指针 constructor,每一个实例都包含一个指向原型对象的内部指针 __proto__

ES5

原型链继承

所有引用类型默认都继承了 Object,这个继承也是通过原型链实现的。

重点:让子类的原型等于父类的实例。

  • 子类型有时需要覆盖超类型或者添加超类型的方法,在给原型添加方法的代码一定要放在替换原型之后
  • 创建子类实例时,无法向父类传参
  • 所有新实例都会共享父类实例的属性。(原型上的属性是共享的,一个实例修改了原型属性,另一个实例的原型属性也会被修改!)
  • 子类的原型的 constructor 会指向子类的新原型(父类的实例)的原型 __proto__constructor
// 父类
function Person(name) {
    this.name = name;
    this.sum = function() {
        console.log(this.name)
    }
}

Person.prototype.age = 18;
         
// 子类
function Child() {
    this.name = 'kid'
}
     
Child.prototype = new Person(); // 原型链继承重点

借用构造函数继承

重点在子类构造函数内部调用父类构造函数,使用 callapply 方法将父类构造函数引入子类构造函数。

  • 优点:

    • 解决了原型链继承的缺点
    • 可以继承多个构造函数(call / apply 多个)
    • 可以向父类传参(call、apply)
  • 缺点:

    • 只能继承父类构造函数的属性,没有继承父类原型的属性
    • 无法实现构造函数的复用
// 父类
function Person(name) {
    this.name = name;
    this.sum = function() {
        console.log(this.name)
    }
}

Person.prototype.age = 18;
     
// 子类
function Child() {
    // 借用构造函数继承,继承了父类的属性
    Person.call(this, 'hhhqzh');
    this.name = 'kid';
}

组合继承(原型链 + 借用构造函数)

将原型链继承与借用构造函数继承组合起来,既实现了对父类原型属性和方法的继承(原型链)又实现了对父类实例属性的继承(借用构造函数)

  • 优点:

    • 子类构造函数继承的属性是私有
    • 继承了父类原型上的属性和方法,且可以对父类构造函数进行传参
  • 缺点:

    • 两次调用父类构造函数(耗内存))
    • 子类原型的 constructor 会指向子类的新原型(父类实例)的原型 __proto__constructor
// 父类
function Person(name) {
    this.name = name;
    this.sum = function() {
        console.log(this.name)
    }
}

Person.prototype.age = 18;
     
// 子类
function Child(name) {
    // 借用构造函数继承
    Person.call(this, name); 
}
    
// 原型链继承
Child.prototype = new Person(); 
Child.prototype.constructor = Child;
     
let child = new Child('hhhqzh');
     
console.log(child.name); // hhhqzh 继承了父类构造函数的属性
console.log(child.age); // 18 继承父类原型的属性

原型式继承

借助原型可以基于已有的对象创建新对象,同时不必因此创建自定义类型。

重点:必须要有一个对象作为另一个对象的基础。

  • 优点

    • 类似于复制一个对象,用函数包装
    • Object.create 利用了这个原理,Object.create方法创建一个新对象,使用现有的对象(第一个参数)来提供新创建的对象的__proto__ ,第二个参数指定了需要添加到新对象中的属性名以及这些属性的属性描述符(enumerable、writable、constructor、value 等)。
  • 缺点:

    • 所有实例都继承原型上的属性
    • 无法实现复用,实例的属性都是后添加的
// 父类
function Person(name) {
    this.name = name;
    this.sum = function() {
        console.log(this.name)
    }
}

Person.prototype.age = 18;
     
// 先封装一个函数容器,在函数内部创建一个临时构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个构造函数的实例。
function object(obj) {
    function F(){};
    // 使用新对象的原型指向现有的对象
    F.prototype = obj;
    return new F();
}
     
let person = new Person('hhhqzh');
let child = object(person);
console.log(child.name); // hhhqzh 继承了父类函数的属性

寄生式继承

创建一个仅用于封装集成过程的函数,在该函数内部(任何能返回新对象的函数,比如原型式继承)来增强对象,最后返回对象。

  • 缺点:没用到原型,无法实现复用。
// 父类
function Person(name) {
    this.name = name;
    this.sum = function() {
        console.log(this.name)
    }
}

Person.prototype.age = 18;
     
function object(obj) {
    function F();
    F.prototype = obj;
    return new F();
}
     
// 创建一个封装继承过程的函数,该函数在内部以某种方式增强对象。
function creeateAnother(original){
    let clone = object(original); // 继承过程
    clone.name = 'ppptyp';
    clone.sayHi = function() {
    console.log("hi");    
}
    return clone;
}
     
let person = new Person('hhhqzh');
let child = creeateAnother(person);
child.sayHi(); // hi

寄生组合式继承

即通过借用构造函数来继承属性,又通过原型式继承的混成形式来继承方法。

本质上是使用寄生式继承来继承父类型的原型(父类的原型等于一个实例),然后再将这个实例指定给子类型的原型。

  • 优点: 只调用了一次构造函数
// 父类
function Person(name) {
    this.name = name;
    this.sum = function() {
        console.log(this.name)
    }
}

Person.prototype.age = 18;
     
// 子类
function Child(name) {
    Person.call(this, name); // 借用构造函数继承
}
     
// 原型式继承
function object(obj) {
    function F();
    F.prototype = obj;
    return new F();
}
     
// 寄生
function creeateAnother(Child, Person){
    let prototype = object(Person.prototype); // 原型式继承过程
    prototype.constructor = Child;
    Child.prototype = prototype;
}
     
// Child.prototype = new Person(); // 原型链继承
creeateAnother(Child, Person); // 替换原型链继承
let child = new Child('hhhqzh');
     
console.log(child.name); // hhhqzh 继承了父类构造函数的属性
console.log(child.age); // 18 继承父类原型的属性

ES6

  • 使用 extend 继承父类
  • 子类中存在一个 super 函数,用来调用父类的构造函数,并隐式返回一个 this,子类的构造函数的初始化全部都是基于这个 this。因此在 super 之前使用 this 会报错。
  • 可以在子类的静态方法中使用 super 调用父类的静态方法,不能删除 super 上的属性
  • 父类的静态方法 static,只能通过类直接调用,不会被实例调用,子类可继承
class Person {
    constructor(name) { 
        this.name = name 
    }
    
    run() {
    }
} 

// extends 相当于方法的继承 
class Child extends Person {
    constructor(name) {
        // super 相当于属性的继承 
        // 替换了 People.call(this, name) 
        super(name);
        this.age = 18;
    } 
    
    fight() {
    } 
}

最后

欢迎大家在评论区一起交流,一起进步!