继承是OO语言中的一个最为人津津乐道的概念.许多OO语言都支持两种继承方式: 接口继承 和 实现继承 .接口继承只继承方法签名,而实现继承则继承实际的方法.由于js中方法没有签名,在ECMAScript中无法实现接口继承.ECMAScript只支持实现继承,而且其 实现继承 主要是依靠原型链来实现的.
概念
简单回顾下构造函数,原型和实例的关系:
每个构造函数(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) var child2 = new Child() console.log(child2.names) 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: ƒ} Child {names: Array(2), getName: ƒ}
|
优点
- 避免了引用类型的属性被所有实例共享
- 可以在 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)
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);
person1.firends.push('taylor'); console.log(person2.friends);
|
注意:修改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)
参考