继承是OO语言中的一个最为人津津乐道的概念.许多OO语言都支持两种继承方式: 接口继承 和 实现继承 .接口继承只继承方法签名,而实现继承则继承实际的方法.由于js中方法没有签名,在ECMAScript中无法实现接口继承.ECMAScript只支持实现继承,而且其 实现继承 主要是依靠原型链来实现的.

js异步加载

概念

简单回顾下构造函数,原型和实例的关系:
每个构造函数(constructor)都有一个原型对象(prototype),原型对象都包含一个指向构造函数的指针(constructor),而实例(instance)都包含一个指向原型对象的内部指针(proto)

proto是隐式原型,prototype是显式原型

每个对象都有一个隐式原型,指向该对象的原型。实例化后通过proto属性指向构造函数的显式原型prototype,
原型链是由各个原型对象组成,每个对象都有proto属性,指向创建该对象的构造函数的原型,通过隐式原型proto属性将对象链起来,组成原型链,用来实现属性方法继承和共享

继承

原型链继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Parent() {
this.names = ['cpp', 'wmh']
}
Parent.prototype.sayName = function() {
console.log(this.names)
}
function Child() {}
Child.prototype = new Parent()

var child1 = new Child()
child1.names.push('James')
console.log(child1.names) // ["cpp", "wmh", "James"]
var child2 = new Child()
console.log(child2.names) // ["cpp", "wmh", "James"]
console.log(Parent.prototype.constructor === Parent)

缺点

  • 当原型链中包含引用类型属性的时候,引用类型的属性会被所有实例共享
  • 当创建Child子类的时候,不能向父类构造函数Parent传参
    为此,下面将有一些尝试以弥补原型链的不足!

构造函数继承 constructor stealing (经典继承)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Parent() {
this.names = ['cpp', 'wmh']
this.getName = function() {
console.log(this.names)
}
this.getName()
}
function Child() {
Parent.call(this)
}
var child1 = new Child()
var child2 = new Child()
console.log(child1)
console.log(child2)

VM911:4 (2) ["cpp", "wmh"]
VM911:4 (2) ["cpp", "wmh"]
Child {names: Array(2), getName: ƒ} // getName 调用1次
Child {names: Array(2), getName: ƒ} // getName 调用2次

优点

  • 避免了引用类型的属性被所有实例共享
  • 可以在 Child 中向 Parent 传参

缺点

方法都在构造函数中定义,导致每次创建实例都会执行constructor方法

组合继承

组合继承, 有时候也叫做伪经典继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥两者之长的一种继承模式。即原型链继承 + 构造函数继承

基本思路:

原型链实现对原型属性和方法的继承
构造函数实现对实例属性和方法的继承

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Parent(name) {
console.log('Parent const')
this.name = name;
}
Parent.prototype.getName = function() {
console.log(this.name)
}
function Child(name) {
Parent.call(this, name)
}
Child.prototype = new Parent()
var child1 = new Child('cpp')
var child2 = new Child('wmh')
console.log(child1)
console.log(child2)

// Parent const
// Parent const
// Parent const
VM1202:14 Child {name: "cpp"}
VM1202:15 Child {name: "wmh"}

优点

融合原型链继承 + 构造函数继承两者优点,js常用的继承模式

缺点

  • 创建实例会调用两次父类构造函数

原型式继承Object.create

ES5 Object.create 的模拟实现,将传入的对象作为创建的对象的原型(对象的proto)。

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function mockCreate(obj) {
function Fn() {}
Fn.protptype = obj
return new Fn()
}
var person = {
name: 'kevin',
friends: ['daisy', 'kelly']
}
var person1 = mockCreate(person);
var person2 = mockCreate(person);

person1.name = 'person1';
console.log(person2.name); // kevin

person1.firends.push('taylor');
console.log(person2.friends); // ["daisy", "kelly", "taylor"]

注意:修改person1.name的值,person2.name的值并未发生改变,并不是因为person1和person2有独立的 name 值,而是因为person1.name = ‘person1’,给person1添加了 name 值,并非修改了原型上的 name 值。

缺点

包含引用类型的属性值始终都会被实例共享,这点跟原型链继承缺点一样。

寄生式继承

思路

创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再返回对象. 如下.

实现

1
2
3
4
5
6
7
8
9
10
function createObj (o) {
var clone = Object.create(o); // 原型式继承
clone.sayName = function () { // 增强对象
console.log('hi');
}
return clone; // 记得返回这个对象
}
var parent = {name: 'cpp'}
var child = createObj(parent)
console.log(child)

这个例子中的代码基于parent返回了一个新对象child. 新对象不仅具有parent的所有属性和方法, 而且还被增强了, 拥有了sayName()方法.
缺点也是 父类包含引用类型的属性值会被实例继承

寄生组合 继承

组合继承最大的缺点就是会两次调用父类构造函数,先回顾下组合继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Parent (name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child (name, age) {
Parent.call(this, name);
this.age = age;
}
Child.prototype = new Parent();
var child1 = new Child('kevin', '18');
console.log(child1)`

一次是设置子类型实例的原型的时候:
Child.prototype = new Parent();
一次在创建子类型实例的时候:
var child1 = new Child('kevin', '18');
寄生组合式继承就是为了降低调用父类构造函数的开销而出现的

实现

1
2
3
4
5
6
7
8
9
10
11
12
function Parent(foo) {
this.foo = foo
}
Parent.prototype.printFoo = function() {
console.log(this.foo)
}
function Child(bar) {
this.bar = bar
Parent.call(this)
}
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child // 重新指标指向自己

封装

1
2
3
4
5
6
7
8
9
10
11
function mockObject(o) {
function F() {}
F.prototype = o;
return new F();
}
function prototype(child, parent) {
child.prototype = mockObject(parent.prototype);
child.prototype.constructor = child;
}
// 当我们使用的时候:
prototype(Child, Parent);

es6版继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Super {
constructor(foo) {
this.foo = foo
}
printInfo() {
console.log(this.foo)
}
}
class Sub extends Super {
constructor(foo, bar) {
super(foo)
this.bar = bar
}
}

ES5的继承,实质是先创造子类的实例对象,然后将再将父类的方法添加到this上。
ES6的继承,先创造父类的实例对象(所以必须先调用super方法,然后再用子类的构造函数修改this)

参考