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;
class Super {}
class Sub extends Super {}
const sub = new Sub();
// 子类构造函数的原型链指向的直接是父类的构造函数
Sub.__proto__ === Super;