继承方法顶级总结

515 阅读9分钟

es5 的继承

1.原型链继承  

function Parent() {
  this.name = 'kevin';
}
Parent.prototype.getName = function () {
  console.log(this.name);
}
function Child() { }
//vvv原型链继承vvv
Child.prototype = new Parent();
//^^^原型链继承^^^

var child = new Child();
child.getName() //kevin

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

实例可继承的属性:实例的构造函数的属性,父类构造函数属性,父类原型的属性/方法。(新实例不会继承父类实例的属性)(乍一看好像挺完美的

缺点: 1. 引用类型属性被所有实例共享。

原因:因为child实例继承来的属性位于其原型对象中,如果通过child.name的方式改写原始值类型属性,其实不是重写原型对象中的name,而是在child实例自己的作用域中新建了一个name属性,后续的访问时会优先访问这个name。但是如果child.name是一个引用值例如数组,那么通过child.name.push()这样的方式会先访问并直接改写原型对象中的name,导致其他实例也被影响。

  1. 创建实例时,没有办法在不影响所有实例的情况下,向父类型的构造函数传递参数。(往父类传参这个需求倒也不常用)

  2. 子类构造函数的原型对象上的constructor属性被重写了,不再指回子类构造函数本身

2.构造函数继承

function Parent() {
  this.name = ['kevin'];
}
Parent.prototype.say = function (){
  console.log("hello");
}
function Child() {
  //vvv构造函数继承vvv
  Parent.call(this);
  //^^^构造函数继承^^^
}

var child1 = new Child();
var child2 = new Child();
child1.name.push("cc");
console.log(child2.name); //["kevin"] -> 引用类型不共享

chiild1.say(); //undefined -> 父类原型里的方法无法继承

重点: 用.call()或.apply()将父类构造函数引入子类函数中进行普通执行

优点: 解决了原型链的主要问题:引用类型共享

缺点:

  1. 只继承了父类构造函数的属性和方法,无法继承父类原型的属性和方法

  2. 方法都在构造函数中定义,每次创建实例除了调用子类构造函数,还会调用父类构造函数,性能浪费。

3.组合继承

function Parent() {
    this.name = ['kevin'];
}
Parent.prototype.getName = function () {
    console.log(this.name, this.age);
}
function Child(age) {
    //2.构造函数继承,第二次调用Parent(),覆盖原型中的同名属性
    Parent.call(this);
    
    this.age = age;
}
//1.原型链继承,第一次调用Parent()
Child.prototype = new Parent();

//使Child原型对象的构造函数指回Child,当然在一个自然的对象里这两个东西本来就应该相等:
Child.prototype.constructor=Child;

var child1 = new Child(19);
var child2 = new Child(20);
child1.name.push("cc");
child1.getName();  //["kevin", "cc"] 19
child2.getName();  //["kevin"] 20

优点:融合前两者,先原型链后构造函数,可传参&复用

缺点:

1.来自构造函数集成的缺点:每次创建实例除了调用子类构造函数,还会调用父类构造函数,性能浪费。

注意: 很多文章里提到的每次创建子类实例会调用两次父类构造函数的描述是不准确的,实际上和构造函数继承一样,还是子类和父类构造函数各一次。只是因为多了一步Child.prototype = new Parent();总体上只会比构造函数继承多调用一次父类。

2.父类里会有同名属性name(这才是寄生组合继承要优化的关键点,而不是很多文章提到的避免两次调用父类构造函数)

有同名属性会有什么问题呢?在浏览器中运行示例,输入child1:

image.png 可以发现在child1和child1.[[Prototype]]中都有属性name,这是因为:

首先通过原型链继承来的name属性位于child.[[Prototype]]中,第二次通过构造函数继承来的name属性位于child1实例中。两者不是简单的覆盖关系,只是因为在访问时的优先顺序,会先在实例本身的作用域中查找,找不到再去实例的原型对象的作用域中查找。所以才看似好像是构造函数重写了child1的name属性。但是这样是有问题的,例如如果用delete child1.name企图删除这个name属性的时候,会发现删除之后name依然可以访问到,这就是因为[[Prototype]]里的name还在。

我们想要的理想情况是child1.[[Prototype]]中没有额外多余属性,所以需要寄生继承来进行这部分的优化。

4.原型式继承

function Parent(){
    this.name="kevin";
}
//封装一个函数容器,输入的实例对象obj = F.prototype = 返回值.[[Prototype]] 
//F是子类的构造函数,返回F即子类的一个实例
function createObj(obj) { 
    function F() { } 
    F.prototype = obj; 
    return new F(); 
} 
var parent = new Parent(); 

//调用手写的方法
var child = createObj(parent); 
//利用es5 object.create实现
var child = Object.create(parent); 

console.log(child.name);//继承父类属性

原型式继承可以理解为寄生式继承的简陋版,有两种方法实现,其核心是使child.[[Prototype]] = obj:

  1. ES5提供的APIvar child = Object.create(obj)

image.png

  1. 上面例子中手写实现的方法var child = createObj(obj)

image.png

重点: 这种方法完全跳过了Child构造函数,直接用一个函数返回以obj为原型对象的一个实例。obj往往是父类的一个实例。两种方法细微区别:1生成的实例属于Parent类,2生成的实例属于Function类

优点: 每次创造child实例不会调用父类构造函数了,当然也不会调用子类构造函数,实际上这种情况压根都没有Child子类

缺点: 原型链继承有的缺点它都有,引用类型属性共享,不能往父类传递参数等;并且他不光不能往父类传参,它在创建时甚至不能往实例里传参,这一点也是后面寄生继承需要解决的。(更新:其实Object.create()可以接受两个参数,第二个参数为新对象定义额外属性的对象,但这个第二个参数不常用,需要传参的情况往往就直接用下面的寄生式继承了)

5.寄生式继承

//将原型式中的createObj用下面这段替代
function createObj(o) { 
    //核心还是这个Object.create() 
    var clone = Object.create(o); 
    clone.say = function() { console.log(this) }; 
    return clone
}  

重点: 就是给原型式继承外面套了个壳子。把原型式继承再次封装,然后在对象上增加新的可以独享的方法和属性,再把新对象返回

优点:同原型式继承优点,除此之外还部分解决了原型式的引用类型属性共享问题,创建实例时传参问题

缺点: 1.无法做到函数复用。同样的子类内部方法在每次实例化时都要创建一遍

2.实例创建时没有定义、只在父类中定义过的引用类型属性还是会有共享问题。

6.寄生组合式继承

很多文章说:

在组合继承中,无论在什么情况下,都会调用两次父类构造函数:一次是在创建子类型原型时,另一次是在子类型构造函数内部,寄生组合式继承就是为了解决这一问题才出现的

实际上,组合继承的子类被创建时,会先调用一次子类构造函数,再通过 构造函数继承 调用一次父类构造函数。而下面要说的寄生组合式继承,每次创建子类时同样是调用:一次子类构造函数,一次父类构造函数!!所以,出于减少构造函数调用次数,而使用寄生组合式继承的逻辑是完全不成立的

那么寄生组合继承到底是为了解决组合继承里的什么问题呢?答案也很简单:去除父类里的同名属性

由于寄生组合继承实质上是构造函数继承和寄生式继承的结合,那么让我们回顾一下这两者各自的缺陷:

构造函数继承的缺点:无法继承父类原型的属性和方法

寄生继承的缺点:父类实例中的引用类型有共享问题

那么寄生组合式继承的思路就非常明确了,划重点:

通过构造函数来继承父类实例属性和方法 & 通过原型链来继承父类原型的属性和方法

//借用寄生式继承,继承父类原型的方法,不涉及父类实例
//注意这里与纯寄生式继承的区别:
//纯寄生式继承的Object.create()传入一个父类实例,获得一个子类实例
//这里的Object.create()传入父类原型对象,获得子类的原型对象
//他们的共同点是:传入 === 传出.[[Prototype]],这里有点难以理解,可以参考下面的附图
function inheritPrototype(subType, superType) {
    var prototype = Object.create(superType.prototype); 
    // 创建对象,创建父类原型的一个副本 
    prototype.constructor = subType; 
    // 增强对象,弥补因重写原型而失去的默认的constructor 属性 
    subType.prototype = prototype; 
    // 指定对象,将新创建的对象赋值给子类的原型 
} 
// 父类初始化实例属性和原型的方法 
function SuperType(name) { 
    this.name = name; 
    this.colors = ["red", "blue", "green"]; 
} 
SuperType.prototype.sayName = function() { 
    console.log(this.name); 
}; 
// 借用构造函数继承父类构造函数的实例的属性(解决引用类型共享的问题) 
function SubType(name, age) { 
    SuperType.call(this, name); 
    this.age = age; 
} 
// 将子类型的原型重写替换成父类的原型 
inheritPrototype(SubType, SuperType); 
// 对子类添加自己的方法 
SubType.prototype.sayAge = function() { 
    console.log(this.age); 
};

let sub = new SubType('amy',25)
console.log(sub);

image.png

可以看出父类的Parent里已经没有了同名的name属性,完美!

附图:

image.png

es6的继承

class 本质还是函数,只是一个语法糖而已。

class Parent {
    constructor(x, y) {
        this.x = x;
        this.y = y
    }
    getX() {
        return this.x
    }
}
class Child extends Parent {
  constructor(x, y, name) {
    super(x, y);//调用父类的constructor(x,y)
    this.name = name;
  }
  getName() {
      return this.name
}
var child1 = new Child("x", "y", "ccg");
console.log(child1);//Child {x: "x", y: "y", name: "ccg"}

1.class 通过 extends 关键字实现继承。

2.super 关键字

super这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。

第一种情况,super作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次super函数。

这种情况类似ES5当中的构造函数继承,区别在于,ES5 是先创造子类的实例对象 this,再将父类的方法添加到 this 上面(Parent.apply(this)),而 ES6 是先创建父类的实例对象 this,所以必须调用 super 关键字。

class A {
  constructor() {
    console.log(new.target.name);
  }
}
class B extends A {
  constructor() {
    super();
  }
}
new A();
new B();

作为函数,super 只能在子类的构造函数中。但是如果作为对象时,可以在普通方法中指向父类的原型对象;

在静态方法中指向父类。(!由于 super 指向父类的原型对象,所以定义在父类实例的方法无法通过 super 调用)。

   class Parent {
      static myMethod(msg) {
        console.log('static', msg);
      }
      myMethod(msg) {
        console.log('instance', msg);
      }
    }
    class Child extends Parent {
      static myMethod(msg) {
        super.myMethod(msg);
      }
      myMethod(msg) {
        super.myMethod(msg);
      }
    }
    Child.myMethod(1);  //static 1
var child = new Child();
    child.myMethod(2);  //instance 2

使用 super,要显式指定使用方法。

class Parent {
  constructor() {
    super();
    console.log(super);//报错
  }
}

Class 的静态方法

ES6 中类有静态方法,即一个方法前加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用。

class Food {
static classMethod() {
   return 'hello'
}
}

Food.classMethod() // "hello"

var poo = new Food();
poo.classMethod() // TypeError: poo.classMethod is not a function

通过类直接调用,输出的是hello,实例化以后不能调用。