JS继承方法及原理梳理,面试看这篇就够了

931 阅读7分钟

继承

JS继承常用的八种方案

原型链继承

原型链继承,就是修改子构造函数的原型,将其指向父构造函数的实例,这个方法就是通过原型链继承父原型的属性和方法

// 原型链继承
function Parent() {}

Parent.prototype.property = true;

Parent.prototype.color = [];

Parent.prototype.getParentValue = function() {
  return this.property;
};

function Child() {}

Child.prototype = new Parent();

const myChild = new Child();

console.log(myChild.color); // []

myChild.color.push(1);

console.log(myChild.color); // [1]
console.log(Parent.prototype.color); // [1]

原型链继承的缺点就是原型上的引用类型的数据,在所有实例下的修改,会造成全部的修改,也就是各个实例对于引用类型的属性,没有独立的继承

借用构造函数继承

这个方法就是在子类的构造函数中,调用父类的函数,就等同于将父类的属性和方法,复制给子类

function Parent(value) {
  this.name = value;
  this.color = [];
}

function Child(value) {
  Parent.call(this, value);
}

缺点

  • 只能实现对于父构造函数上的属性和方法的继承,无法实现父构造函数原型上的属性和方法的继承
  • 每个实例都有父类函数的实例,浪费性能

组合继承

组合继承就是融合了原型链继承和借用构造函数继承的一个方法,利用原型链继承原型上的属性/方法,利用借用构造函数继承父构造函数上的属性/方法

function Parent(value) {
  this.name = value;
  this.sayParent = () => {
    console.log("this.name", this.name);
  };
}

Parent.prototype.getValue = function() {
  return this.name;
};

function Child(value) {
  Parent.call(this, value);
}
// 修改子构造函数的原型为父构造函数的实例
Child.prototype = new Parent();
// 修改子构造函数的原型的constructor为子构造函数本身
Child.prototype.constructor = Child;

缺点:

会调用两次Parent构造函数,一次是在Child.prototype = new Parent();,将子构造函数的原型修改为父构造函数的实例,会给Child.prototype添加name和sayParent,第二次调用是在Parent.call(this,value);,在声明Child的实例的时候,会给实例同样也添加一遍name和sayParent,实例的属性会覆盖原型的这两个同名属性,就相当于原型上会存在无用的属性和方法

原型式继承

思路:借助原型实现在已有对象的基础上,实现对象到对象的继承,这个思路后来被实现为Object.create()

function object(obj) {
  function F() {}
  F.prototype = o;
  return new F();
}

缺点:

  • 同样存在对于引用类型指向相同的问题,某一个实例修改,会导致所有实例的该属性修改
  • 无法传参

寄生式继承

核心就是在原型式继承的基础上增强对象

function object(obj) {
  function F() {}
  F.prototype = o;
  return new F();
}

// 寄生式继承
function createOther(obj) {
  let clone = object(obj);
  clone.testFunc = function() {
    console.log(123);
  };
  return clone;
}

缺点类比原型式继承,是一样的问题

寄生组合式继承

利用借用构造函数实现实例属性的继承,利用原型链实现方法和原型属性的继承,但是是通过直接让子类的原型指向父类的原型,同时修改子类原型的constructor

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

Parent.prototype.getValue = function() {
  return this.name;
};

Parent.sayHai = function() {
  console.log("hai");
};

function Child(value) {
  Parent.call(this, value);
}

// 设置子类的prototype,通过Object.create的方式创建一个以父类原型作为原型的对象,另外将constructor的value指向子类本身
Child.prototype = Object.create(Parent.prototype, {
  constructor: {
    value: Child,
    enumerable: false,
    writeable: true,
    configurable: false
  }
});

其实ES6的class继承,通过babel转义后,也就是通过类似寄生组合式继承实现的,只是传统的ES5只能实现到这步,还缺少一步,就是子类无法继承父类的静态方法,也就是上面定义的Parent.sayHai方法

其实所谓的静态方法,就是将这个概念作为对象看待,对标ES5,也就是函数也是对象,所以定义在函数上的属性和方法,在ES5都是没有办法继承的,只能显式的写,例如

Parent.sayHai = function() {
  console.log("hai");
};

Child.sayHai = Parent.sayHai;

这样显式的写,显然不符合继承的理念

但是ES6可以通过Object.setPrototypeOf(subClass, superClass)实现,这也是class继承的底层实现原理,class继承就可以继承静态方法,就是通过这个实现的

这里的概念其实有点绕,Object.setPrototypeOf方法,是将第一个参数的对象原型指向第二个参数,等同于subClass.__proto__ = superClass,要注意区分Child.prototypeChild.__proto__是两个不同的概念

Child.prototype是构造函数原型,Child.__proto__是对象原型,而静态属性和静态方法,都是定义在Parent这个函数的对象属性上的,所以可以通过Object.setPrototypeOf来实现对象属性的继承

ES6的class继承,就是通过这个实现的,下面贴上class继承通过babel转义之后的代码

function _inherits (subClass, superClass) { 
		// ...
		subClass.prototype = Object.create(superClass && superClass.prototype, { 
				constructor: { 
						value: subClass, 
						enumerable: false, 
						writable: true, 
						configurable: true 
				} 
		}); 
		if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; 
}

混入式继承

混入式继承可以同时继承多个类的构造函数属性和方法,也可以继承多个类的原型属性和方法,原理就是类似寄生组合式继承,通过call继承构造函数属性,通过修改原型继承其他类的原型属性,最后修改constructor指向

// 混入方式
function ChildClass() {
  SuperClass.call(this);
  OtherSuperClass.call(this);
}

// 继承一个类
ChildClass.prototype = Object.create(SuperClass.prototype);
// 混合其他
Object.assign(ChildClass.prototype,OtherSuperClass.prototype);
// 重新指定constructor
ChildClass.prototype.constructor = ChildClass;

ES6的继承extends

ES6的继承实现的babel转义代码已经在上面贴出来了,其实主要思路也就是寄生组合式继承,只是多了一步,通过Object.setPrototypeOf修改子类作为对象属性的原型,这样就可以继承父类的静态方法

// ES6继承
class MyParent {}

class MyChild extends MyParent {}

总结

ES5继承和ES6继承的主要区别

  • ES5的继承是先创建子类的实例对象,然后再将父类的方法添加到this上
  • ES6是先创建父类的实例对象this,然后再用子类的构造函数修改this,因为子类没有自己的this对象,所以必须先调用父类的super()方法,否则新建实例报错

函数声明和类声明的区别:函数声明会有提升,类声明不会,必须先声明类,再访问,否则会抛出错误

ES6的class中,为什么需要使用super()

super()指的是父类的构造函数,js的规范规定了不能在调用super()之前使用this,其核心原因就是因为子类是没有this的,必须通过调用super()创建当前的this,在对当前的this进行修改属性等操作

假设允许在super()之前就调用this,那么很可能出现以下的情况

class Person {
  constructor(name) {
    this.name = name;
  }
}

class PolitePerson extends Person {
  constructor(name) {
    this.greetColleagues(); // 🔴  这是禁止的,往后见原因
    super(name);
  }
  
  // 如果在方法中调用this的属性,则会直接报错,因为这个时候this尚且没有
 greetColleagues() {
    alert('Good morning folks!');
    alert('My name is ' + this.name + ', nice to meet you!');
  }
}

React中的super()

在React中,及时不传props给super,也能在构造函数之后拿到this.props,这是因为React内部的机制,会在类的实例构造后,将props绑定到实例上

  // React 内部
  const instance = new YourComponent(props);
  instance.props = props;

同时React允许不显式的写super(props),但是并不推荐都用super()替代super(props),因为React的机制是在构造函数之后绑定props到this上,所以如果不传props给super,那么在构造函数中使用this.props则还是会未定义,而如果传了propssuper(),那么在super(props)下面使用this.props都是没问题的

// React 內部
class Component {
  constructor(props) {
    this.props = props;
    // ...
  }
}

// 你的程式碼內部
class Button extends React.Component {
  constructor(props) {
    super(); // 😬 我们忘了传入 props
    console.log(props);      // ✅ {}
    console.log(this.props); // 😬 未定义
  }
  // ...
}

class Button extends React.Component {
  constructor(props) {
    super(props); // ✅ 传入 props
    console.log(props);      // ✅ {}
    console.log(this.props); // ✅ {}
  }
  // ...
}

关于为什么需要调用super()的参考