js继承的7种继承方式

138 阅读5分钟

继承

继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的属性和方法,或子类从父类继承方法,使得子类具有父类相同的行为。

构造函数

一个函数可以作为创建对象实例的构造函数,也可以当做普通函数使用,区别就是在于是否使用了new关键字。
构造函数在创建对象的时候,会执行如下的操作:

  1. 在内存中创建一个对象
  2. 在新对象内部的[[Prototype]]属性指向构造函数的prototype属性
  3. 构造函数内部的this指向这个创建的新对象
  4. 执行构造函数内部的代码,比如添加一些属性、方法等
  5. 如果构造函数有返回值,该返回值为非空对象,则返回该对象,否则返回创建的新对象。

继承方式

原型链继承

核心:将父类的实例作为子类的原型。

SubType.prototype = new SuperType()
// 要修改子类构造函数的指向,否则子类实例的构造函数会指向SuperType。
SubType.prototype.constructor = SubType;
// 不能通过对象字面量的方式添加新方法
SubType.prototype.getSubValue = function () {
    return this.subproperty; 
};

优点:父类方法可以复用。

缺点:

  • 父类的引用属性会被所有子类实例共享
  • 子类构建实例时不能向父类传递参数

构造函数继承

核心:将父类构造函数的内容复制给子类的构造函数。

function SuperType(name) {
 this.colors = ["red","blue","green"];
 this.name = name;
 }
function SubType(name) {
 SuperType.call(this,name);
}
let instance1 = new SuperType('小明')

优点:

  • 父类的引用属性不会被共享
  • 子类构建实例时可以向父类传递参数

缺点:父类的方法不能复用,子类实例的方法每次都是单独创建的。

组合继承

核心:综合了原型链和构造函数,将两者的优点集中了起来。基本的思路是使用原型链继承原型上的属性和方法,而通过构造函数继承实例属性。这样既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性。

function SuperType() {
   this.name = 'parent';
   this.arr = [1, 2, 3];
}
SuperType.prototype.say = function() {
   console.log('this is parent')
}
function SubType() {
   SuperType.call(this) // 第二次调用SuperType
}
SubType.prototype = new SuperType() // 第一次调用SuperType
// 要修改子类构造函数的指向,否则子类实例的构造函数会指向SuperType。
SubType.prototype.constructor = SubType;

优点:

  • 父类的方法可以被复用;
  • 父类的引用属性不会被共享;
  • 子类构建实例时可以向父类传递参数

缺点:调用了两次父类的构造函数,覆盖了子类原型中的同名参数。这种被覆盖的情况造成了性能上的浪费。

原型式继承

核心:原型式继承并没有使用严格意义上的构造函数,是通过借助原型基于已有的对象创建新对象,同时还不必创建自定义类型。本质上就是对传入的对象进行了一次浅复制。

function object(person) {
 function F() {}
 F.prototype = person
 return new F()
}

let person = {
 name:'小明',
 colors:['red','blue']
}
// ECMAScript 5 通过新增 Object.create()方法规范化了原型式继承。
// let person1 = Object.create(person);

let person1 = object(person)
person1.colors.push('green')
let person2 = object(person)
person1.colors.push('yellow')
console.log(person) //['red','blue','green','yellow']

优点:父类方法可以复用。

缺点:

  • 父类的引用属性会被所有子类实例共享
  • 子类构建实例时不能向父类传递参数

寄生式继承

核心:使用原型式继承获得一个目标对象的浅复制,然后增强这个浅复制的能力。

function object(person) {
     function F() {};
     F.prototype = person;
     return new F();
}
function createAnother(original){
    let clone = object(original); // 通过调用函数创建一个新对象
    clone.sayHi = function() { // 以某种方式增强这个对象
        console.log("hi");
    };
    return clone; // 返回这个对象
}

缺点:通过寄生式继承给对象添加函数会导致函数难以重用,与构造函数模式类似。

寄生组合式继承

最佳的的继承方式,组合继承会调用两次父类构造函数,存在效率问题。其实本质上子类原型最终是要包含父类对象的所有实例属性,子类构造函数只要在执行时重写自己的原型就行了。基本思路是不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本。说到底就是使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型。

function object(person) {
 function F() {}
 F.prototype = person
 return new F()
}
function inheritPrototype(subType, superType){
   var prototype = object(superType.prototype); // 创建了父类原型的浅复制
   prototype.constructor = subType;             // 修正原型的构造函数
   subType.prototype = prototype;               // 将子类的原型替换为这个原型
}
 
function SuperType(name){
   this.name = name;
   this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
   alert(this.name);
};
 
function SubType(name, age){
   SuperType.call(this, name);
   this.age = age;
}
// 核心:因为是对父类原型的复制,所以不包含父类的构造函数,也就不会调用两次父类的构造函数造成浪费
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function(){
   alert(this.age);
}

ES6 Class继承

核心: ES6继承的结果和寄生组合继承相似,本质上,ES6继承是一种语法糖。但是,寄生组合继承是先创建子类实例this对象,然后再对其增强;而ES6先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。

class A {}
class B extends A {
 constructor() {
   super();
 }
}

ES6继承与ES5继承的异同:

相同点:本质上ES6继承是ES5继承的语法糖。

不同点:

  • ES6继承中子类的构造函数的原型链指向父类的构造函数,ES5中使用的是构造函数复制,没有原型链指向。
  • ES6子类实例的构建,基于父类实例,ES5中不是。