彻底弄懂JS继承

429 阅读6分钟

ES5中的继承

原型链继承

父类的实例作为子类的原型

// SubType子类 SuperType父类
SubType.prototype = new SuperType();

SubType.prototype.constructor = SubType;

优点

父类方法可以复用

缺点

  • 父类的所有引用属性都会被子类所共享。更改一个子类的引用属性,其他子类也会受影响。
  • 创建子类的实例时,不能向父类的构造函数传递参数。

构造函数继承

在子类构造函数中调用父类的构造函数,可以在子类构造函数中使用call()和apply()方法。

SuperType.call(SubType);

优点

解决原型链继承的两个缺点。

  • 可以在子类构造函数中向父类传递参数
  • 父类构造函数中的的引用属性不会被共享

缺点

  • 子类不能访问父类原型上定义的方法,因为所有方法属性都写在构造函数中,每次创建实例都会初始化。

组合式继承

function SuperType() {
	this.name = 'parent';
}

SuperType.prototype.say = function() {
	// ...
}

function SubType() {
	SuperType.call(this)	// 第二次调用SuperType
}

SubType.prototype = new SuperType();	// 第一次调用SuperType

使用原型链上继承原型上的属性和方法,而通过构造函数继承实例属性,这样既可以把方法定义在原型上以实现复用,又可以让每个实例拥有自己的属性。

其实就是把上面两种方法结合起来。

优点

  • 父类的方法可以复用
  • 可以在子类构造函数中向父类传递参数
  • 父类构造函数中的引用属性不会被共享

缺点

无论在什么情况下都会调用两次超类,一次是在创建子类原型的时候,一次是在子类构造函数的内部。

原型式继承

不用严格意义上的构造函数,借助原型可以根据已有的对象创建新对象。 本质上是对参数对象的一个浅复制。

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

优点

  • 父类方法可复用

缺点

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

ECMAScript5通过新增Object.create()方法规范了原型式继承。这个方法接收两个参数:一 个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。在传入一个参数的情况下,Object.create()和object()行为相同。

寄生式继承

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

function createAnother(original) {
	var clone = Object.create(original);
        // 主要通过为构造函数新增属性和方法以增强函数
	clone.sayHi = function() {
    		cosole.log('HI');
     		return clone;
    	}
}

缺点

  • 无法解决原型式继承存在的缺点
  • 会由于无法做到方法的复用而降低效率,这一点与构造函数是类似的。

寄生式组合继承

本质上是对父类原型的复制,所以不包含父类的构造函数,也就不会调用两次父类的构造函数造成浪费。

其实就是将组合继承和寄生式继承结合起来。

function inheritPrototype(subType, superType) {
    // 创建了父类原型的浅复制
    var prototype = Object.create(superType.prototype);
    // 修正原型的构造函数
    prototype.constructor = subType;
    // 将子类的原型替换为这个原型
    subType.prototype = prototype;
}

优点

  • 只调用一次父类构造函数
  • 子类实例可以向父类传递参数
  • 父类方法可以复用
  • 父类的引用不会被所有的子类所共享

总结

整体上来看,继承都基于两种方式:

1.通过原型链,即子类的原型指向父类的实例从而实现原型共享。

2.借用构造函数,即通过js的apply、call实现子类调用父类的属性、方法。

原型链方式可以实现所有属性方法共享,但无法做到属性、方法独享。

而借用构造函数除了能独享属性、方法外还能在子类构造函数中传递参数,但代码无法复用。

组合继承就是把以上两种继承方式一起使用,把共享的属性、方法用原型链继承实现,独享的属性、方法用借用构造函数实现。

但是!组合继承有一个小bug,实现的时候调用了两次超类(父类)。

我们可以发现,上面的三种方法都是直接利用父类的构造函数,原型链方式是将其作为子类的原型,构造函数方式是将其内容直接复制给子类构造函数。

所以,一种新的想法出现了——原型式继承。不再用严格意义上的构造函数,借助原型可以根据已有的对象创建新对象,也就是通过对原型对象的浅复制来创建子类

而寄生式继承则是在原型式继承的基础上,在函数内部通过为构造函数新增属性和方法以增强函数。其实也就是想要为子类自定义属性和函数,这点和构造函数继承是类似的。

结合上面的所有方法,我们得到了寄生组合式继承。这种继承方式使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型,从而解决了以上会出现的所有缺点。

ES6中的继承

ES6继承与寄生组合式继承相似,本质上是一种语法糖,通过extends关键字实现继承。

class A() {}

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

子类必须在constructor中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工,如果不调用super方法,子类就得不到this对象。

实现原理

class B extends A {}

B.__proto__ === A;    //继承属性
B.prototype.__proto__ === A.prototype;	// 继承方法

一个继承语句同时会存在两条继承链,一条实现属性继承,一条实现方法的继承

ES5函数继承/ES6类 除了写法以外有什么区别?

  • 不存在变量提升
new Foo();	//ReferenceError
class Foo {}

ES6之所以不会把变量声明提升到代码头部,是为了保证子类在父类之后定义。

  • 类和模块的内部默认使用严格模式

  • 类的内部定义的所有方法都是不可枚举的。(包括静态方法和实例方法)

class Point {
    constructor(x, y) {
        // ...
    }
    
    toString() {
		// ...
    }
}

Object.keys(Point.prototype)	// []

Object.getOwnPropertyNames(Point.prototype)	// ["constructor", "toString"]
  • 必须使用new来调用,否则会报错
class Foo {
  constructor() {
    this.foo = 40;
  }
}
const foo = Foo(); // TypeError: Class constructor Foo cannot be invoked without 'new'
  • class所有方法(包括静态方法和实例方法)都没有原型对象prototype,不能用new来调用。
class Foo {
  constructor() {
    this.foo = 40;
  }
  print() {
    console.log(this.foo);
  }
}
const foo = new Foo();
const fooPrint = new foo.print(); // TypeError: foo.print is not a constructor
  • 继承方式不同
function Super() {}
function Sub() {}

Sub.prototype = new Super();
Sub.prototype.constructor = Sub;

var sub = new Sub();

// 子类构造函数的原型链指向的不是父类的构造函数,而是再上面一层的Function.prototype
Sub.__proto__ === Function.prototype;

ES5继承.png

class Super {}
class Sub extends Super {}

const sub = new Sub();

// 子类构造函数的原型链指向的直接是父类的构造函数
Sub.__proto__ === Super;

WechatIMG401.png