继承是面向对象编程一个非常重要的特性。很多面向对象语言都支持两种继承:接口继承和实现继承。接口继承就是只继承方法签名,而实现继承是继承实际的方法。ES中是不存在接口继承的,因为函数没有方法签名,实现继承是ES唯一的继承方式,而这主要是通过原型来实现的。
原型链继承
也称为 prototype模式
function Super (){
this.name = 'Super'
}
Super.prototype.getName = function(){
return this.name
}
function Sub (){
this.name = 'Sub'
}
// 让Sub继承Super
Sub.prototype = new Super()
//保持继承链的正确
Sub.prototype.constructor = Sub
let instance = new Sub()
console.log(instance.getName()); //Sub
这种方式完成继承虽然很方便,但也是有问题的。
- 首先是原型机制本身的问题,因为原型是所有实例共享的,那也就意味着通过实例对原型上引用属性进行修改,而且会反映在每一个实例上。
- 另一个问题是,在使用原型实现继承的时候,子构造函数的原型实际上变成了父构造函数的实例。这意味着原先的实例属性摇身一变成为了原型属性。我们举例看一下:
function Super (){
this.colors = ['red','blue','green']
}
function Sub (){}
// 让Sub继承Super
Sub.prototype = new Super()
let instance1 = new Sub()
instance1.colors.push('black')
console.log(instance1.colors); //[ 'red', 'blue', 'green', 'black' ]
let instance2 = new Sub()
console.log(instance2.colors); //[ 'red', 'blue', 'green', 'black' ]
这个例子中colors本来是Super的实例属性,但是当Sub继承Super之后,Sub.prototype变成了Super的实例。colors也就变成了Sub实例所共享的一个原型属性。
- 还有一个问题就是子构造函数在实例化时不能给父构造函数传参。
已上问题就导致原型链继承实际基本上不会被单独使用
盗用构造函数
为了解决原型原型是哪个包含引用值导致的问题,一种叫做盗用构造函数(constructor stealing)的方式出现了(也叫构造函数绑定、对象伪装、经典继承等等)。这种方法很简单,就是通过call/apply在子构造函数内执行父构造函数。看下面的例子:
function Super (){
this.colors = ['red','blue','green']
}
function Sub (){
// 继承Super
Super.call(this)
}
let instance1 = new Sub()
instance1.colors.push('black')
console.log(instance1.colors); //[ 'red', 'blue', 'green', 'black' ]
let instance2 = new Sub()
console.log(instance2.colors); //[ 'red', 'blue', 'green' ]
这意味着在Sub实例对象上上执行了一遍Super函数中的代码,从而让Sub的每一个实例都有了自己的colors属性。
盗用构造函数的另一个优点就是可以给父构造函数传递参数。
function Super (name){
this.name = name
}
function Sub (name){
// 继承Super
Super.call(this, name)
//Sub实例属性
this.age = 15
}
let instance1 = new Sub('小明')
console.log(instance1.name); //小明
console.log(instance1.age); //15
这里有一个注意点就是最好先调用父构造函数,之后再添加子构造函数属性。以免属性被覆盖
组合继承
组合继承结合了原型链继承和盗用构造函数,将两者的优点结合到了一起。基本思路是使用原型链继承原型上的属性和方法,通过盗用构造函数继承实例属性。看下面的例子:
function Super (name){
this.name = name
this.colors = ["red", "blue", "green"]
}
Super.prototype.sayName = function(){
console.log(this.name);
}
function Sub (name, age){
// 继承Super
Super.call(this, name)
this.age = age
}
Sub.prototype = new Super()
Sub.prototype.constructor = Sub
Sub.prototype.sayAge = function(){
console.log(this.age);
}
let instance1 = new Sub('小明', 15)
instance1.colors.push('black')
console.log(instance1.colors); //[ 'red', 'blue', 'green', 'black' ]
instance1.sayName() //小明
instance1.sayAge() //15
let instance2 = new Sub('小王', 18)
instance2.sayName() //小王
instance2.sayAge() //18
组合继承弥补了原型链继承和盗用构造函数的不足,是JavaScript中使用最多的继承模式。而且组合继承也保留了instanceof和isPrototypeOf方法识别的能力。
不知道大家有没有发现组合继承两个不足的地方。
1、父构造函数的实例属性依然会存在于子构造函数的原型上,虽然访问时会被子构造函数的同名实例属性所覆盖
2、继承过程中调用了两次SuperType,第一次call和第二次new
原型式继承
原型式继承不是严格意义上构造函数的继承方法。可以通过一个函数来完成:
function create (o){
function F(){}
F.prototype = o
F.prototype.constructor = F
return new F()
}
这个函数创建一个临时的构造函数,把传进来的对象作为这个构造函数的原型,并返回它的实例。
看下面的例子:
function create (o){
function F(){}
F.prototype = o
return new F()
}
let Person = {
name:'小王',
friends:['张三', '李四']
}
let p1 = create(Person)
p1.name = '小张'
p1.friends.push('小张')
console.log(p1.name); //小张
console.log(p1.friends); //[ '张三', '李四', '小张' ]
let p2 = create(Person)
p2.friends.push('小李')
console.log(p2.name); //小王
console.log(p2.friends); //[ '张三', '李四', '小张', '小李' ]
这里Person会作为实例p1和p2的原型被共享。
ECMAScript5 对原型式继承给出了一个规范化的方法,Objece.create()。这个方法返回一个新对象,接收两个参数:作为新对象原型的对象,和给新对象定义额外属性的对象。当只有一个参数时,Objece.create()与上面的create方法效果一样:
let Person = {
name:'小王',
friends:['张三', '李四']
}
let p1 = Object.create(Person)
p1.name = '小张'
p1.friends.push('小张')
console.log(p1.name); //小张
console.log(p1.friends); //[ '张三', '李四', '小张' ]
let p2 = Object.create(Person)
p2.friends.push('小李')
console.log(p2.name); //小王
console.log(p2.friends); //[ '张三', '李四', '小张', '小李' ]
其实Object.create()的原理用下面一段代码就能说明:
let p1 = {}
Object.setPrototypeOf(p1, Person)
就是把对象p1的内部属性[[portotype]]设置成了Person。可以验证一下:
let Person = {
name:'小王',
friends:['张三', '李四']
}
let p1 = Object.create(Person)
console.log(Object.getPrototypeOf(p1) === Person); //true
console.log(p1.__proto__ === Person); //true
Object.create()的第二个参数与Object.defineProperties()的第二个参数一样。以这种方式新增的属性会遮蔽原型对象(也就是第一个参数)上面的属性:
let Person = {
name:'小王',
friends:['张三', '李四']
}
let p1 = Object.create(Person, {
name:{
value:'小张',
enumerable:true,
writable:true,
configurable:true
}
})
console.log(p1.name); //小张
原型式组合继承
还记的上面组合继承最后提到的两个不足吗。其实可以通过结合原型式来弥补这两个不足:
function Super (name){
this.name = name
this.colors = ["red", "blue", "green"]
}
Super.prototype.sayName = function(){
console.log(this.name);
}
function Sub (name, age){
Super.call(this, name)
this.age = age
}
// Sub.prototype = new Super() 把这行代码替换成下面一行
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
Sub.prototype.sayAge = function(){
console.log(this.age);
}
这里只调用了一次Super构造函数,而且避免了Sub.prototype上不必要也用到的属性,因此此方式效率更高。可以说是继承的最佳模式。
结语
未来面向对象以及继承应该最先使用ES6的Class,因为Class已经原生规避了上面所说的种种不足,虽然其实现依然是基于原型prototype的。