一文搞透JS中的继承

611 阅读5分钟

在 JS 中并不存在类,class 只是语法糖,本质还是函数。所以在JS中的继承方式中,本质上就是在重写原型对象。

继承实现的本质就是重写原型对象 我们先来看一下ES5中的继承方式

使用原型链实现继承

function SuperType(){
    this.colors = ['red','blue','green']
}
function SubType(){
    // 省略
}
// 此处就是在重写子类的原型对象
SubType.prototype = new SuperType()
var instance1 = new SubType()
instance1.colors.push('aaa'); 
console.log(instance1.colors)//['red','blue','green','aaa']
var instance2 = new SubType()
console.log(instance2.colors)//['red','blue','green','aaa']

这种实现方式存在以下问题:

问题一: 当原型链中包含引用类型值的原型时,该引用类型值会被所有实例共享;

问题二: 在创建子类型(例如创建Son的实例)时,不能向超类型(例如Father)的构造函数中传递参数.

注意:Child.prototype = new Parent()这段代码重写了Child的constructor属性,Child.prototype.constructor === Parent; //true 所以如果使用var child2 = new Child() 的话,child2的构造函数也不再是Child,而是Parent,也就是说此时child2.constructor === Parent;//true child2.constructor=== Child; //false(这里的实例child2不是函数,而是对象,因此没有prototype属性)

借用构造函数实现继承

function SuperType(){
    this.colors = ['red','blue','green']
}
function SubType(){
    //继承了SuperType,使得以后使用new SubType()创造出来的示例会调用SuperType()
    SuperType.call(this)
}
SubType.prototype = new SuperType()
var instance1 = new SubType()
instance1.colors.push('aaa'); 
console.log(instance1.colors)//['red','blue','green','aaa']
var instance2 = new SubType()
console.log(instance2.colors)//['red','blue','green']

这种模式还可以向父类传递参数

function SuperType(name){
    this.name = name
}
function SubType(){
    //继承了父类的时候,同时还向父类传递了参数
    SuperType.call(this,"nike")
    this.age = 29
}
var instance = new SubType();
console.log(instance.name);  //nike
console.log(instance.age);   //29

借用构造函数实现继承存在以下问题: 问题一:每次创建一个 Child 实例对象时候都需要执行一遍 Parent 函数 问题二:函数不可复用。父类中定义的方法,对子类型而言是不可见的. 每个实例都拷贝一份,占用内存大。 这里要特别解释一下这里的函数不可复用 假设SuperType 在构造函数里定义了一个方法:

function  SuperType(name){
    this.colors=["red","blue","green"];
    this.name=name;
    this.sayName = function() {
        console.log(this.name);
    };
}

那么每一次调用new SuperType,就会在实例内部定义一次这个方法——你定义1000个实例,就会定义1000次这个方法;对于color 和name 这两个变量来说,这无可厚非;但实例方法大多数是相同的,所以这里更推荐把方法定义在SuperType.prototype上,这样每个实例构造出来就自动继承这个方法,不用在构造函数里一次次地写。

组合继承

指的是将原型链和借用构造函数的技术组合到一块,从而发挥两者之长的一种继承模式.

function Father(name){
    this.name = name;
    this.colors = ["red","blue","green"];
}
Father.prototype.sayName = function(){
    alert(this.name);
};
function Son(name,age){
    Father.call(this,name);//继承实例属性,第一次调用Father()
    this.age = age;
}
Son.prototype = new Father();//继承父类方法,第二次调用Father()
Son.prototype.sayAge = function(){
    alert(this.age);
}
var instance1 = new Son("louis",5);
instance1.colors.push("black");
console.log(instance1.colors);//"red,blue,green,black"
instance1.sayName();//louis
instance1.sayAge();//5

var instance1 = new Son("zhai",10);
console.log(instance1.colors);//"red,blue,green"
instance1.sayName();//zhai
instance1.sayAge();//10

组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点。 问题:组合继承其实调用了两次父类构造函数, 造成了不必要的消耗。一次是在创建子类型原型的时候, 另一次是在子类型构造函数内部.

寄生组合式继承

function Parent(value) {
  this.val = value
}
Parent.prototype.getValue = function() {
  console.log(this.val)
}

function Child(value) {
  Parent.call(this, value)
}
// 不必为了指定子类型的原型而调用父类型的构造函数,我们所需要的无非就是父类型原型的一个副本而已。
Child.prototype = Object.create(Parent.prototype, {
  constructor: {
    value: Child,
    enumerable: false,
    writable: true,
    configurable: true
  }
})

const child = new Child(1)

child.getValue() // 1
child instanceof Parent // true

现在开发场景我们通常都是能使用ES6都是用ES6实现继承了

Class继承

class Parent {
  constructor(value) {
    this.val = value
  }
  getValue() {
    console.log(this.val)
  }
}
class Child extends Parent {
  constructor(value) {
    super(value)
    this.val = value
  }
}
let child = new Child(1)
child.getValue() // 1
child instanceof Parent // true

class实现继承的核心在于使用extends表明继承自哪个父类,并且在子类构造函数中必须调用super ,因为这段代码可以看成 Parent.call(this, value)

手动现实extend

function extend(subClass,superClass){
    var prototype = Object(superClass.prototype);//创建对象
    prototype.constructor = subClass;//增强对象
    subClass.prototype = prototype;//指定对象
}

extend的高效率体现在它没有调用superClass构造函数,因此避免了在subClass.prototype上面创建不必要,多余的属性. 于此同时,原型链还能保持不变; 因此还能正常使用 instanceof 和 isPrototypeOf() 方法.

总结:

类的继承就是两点:

1.子类调用父类的构造函数(因为子类要获得父类的属性和方法,所以需要这一步),在es5中是通过在子类的构造函数中使用call来调用父类的构造方法,在es6中则是通过super方法实现的)

2.更改子类的原型链(es5中更改原型链的方式是通过Child.prototype = new Parent()等方式,es6中则是通过extends关键字的方式来实现的)

继承相关的面试题

1.手写ES5或者ES6的继承方式并指出其中的优缺点? 见上

2.ES5和ES6在继承的实现上有什么区别?

ES5是先创造一个独立的子类的实例对象,然后再将父类的方法添加到这个对象上面(Parent.apply(this)),即“实例在前,继承在后”

ES6的继承机制则是先将父类的属性和方法,加到一个空的对象上面,然后再将该对象作为子类的实例,即“继承在前,实例在后”.先创建父类的实例对象this(所以必须先调用父类的super()方法),然后再用子类的构造函数修改this.通过关键字class定义类, extends关键字实现继承. 子类必须在constructor方法中调用super方法否则创建实例报错. 因为子类没有this对象, 而是使用父类的this, 然后对其进行加工。super关键字指代父类的this, 在子类的构造函数中, 必须先调用super, 然后才能使用this,原因是子类constructor会覆盖父类的constructor,导致你父类构造函数没执行,所以手动执行下。