JS中的继承
以下内容总结自《js高级程序设计 第三版》
在许多面向对象的语言中,都支持接口继承和实现继承。
接口继承:只继承方法名;实现继承:继承实际的方法。
而在JS中,只支持实现继承,其原理是依靠原型链来实现的。
一、 原型链
首先来说说什么是原型链,原型链是通过原型串联起多个对象的一条链。说到原型,又要提到构造函数和实例这两个概念。
构造函数:即构造实例对象的函数;
实例:就是对象;
在构造函数中,有一个prototype属性,也就是原型对象,这个对象有两个作用:
1. constructor属性:指回构造函数本身;
2. 公共的属性和方法:供构造函数创建的实例复用;
而构造函数构造出来的实例对象,则包含一个指向构造函数的原型对象的内部指针。这个内部指针通常是对外隐藏的,不过在ES6中可以通过Object.getProtoOf()方法获取,这里我们暂时使用{{Prototype}}来代指这个指针。
这里有一个非常重要的 "公式",这对学习原型链十分有用:
对象.{{Prototype}} = 对象构造函数.prototype
来看一段代码
function SuperType() {
this.property = true
}
SuperType.prototype.getSuperValue = function () {
return this.property
}
function SubType() {
this.subproperty = false
}
SubType.prototype = new SuperType()
SubType.prototype.getSubValue = function () {
return this.subproperty
}
var instance = new SubType()
alert(instance.getSuperValue)
这里有两个类型:SuperType和SubType。SubType通过修改prototype属性为SuperType的实例从而实现了继承。通过SubType创建的实例就能够使用SuperType中的属性和方法。
instance这个实例调用getSuperType()方法的过程:
- 查询自身有无该方法,没有,执行2
- 查询原型链中有无该方法,找到方法,执行。
注意
1. 默认原型
所有的函数默认原型都是Object的实例。
所以所有的对象都可以调用Object.prototype中的方法。
2. 确定原型和实例关系的两种方法
-
instanceof
例:
instance instanceof Object // true
instance instanceof SubType // true
instance instanceof SuperType // true
-
isProtoTypeOf()
例:
Object.prototype.isPrototypeOf(instance) // true
SubType.prototype.isPrototypeOf(instance) // true
SuperType.prototype.isPrototypeOf(instance) // true
3. 重写子类原型中的方法会覆盖父类中的原方法
例
function SuperType() {
this.property = true
}
SuperType.prototype.getSuperValue = function () {
return this.property
}
function SubType() {
this.subproperty = false
}
SubType.prototype = new SuperType()
SubType.prototype.getSubValue = function () {
return this.subproperty
}
// 重写超类中的方法
SubType.prototype.getSuperValue = function(){
return false
}
var instance = new SubType()
alert(instance.getSuperValue) // false
还有一点需要注意,向子类原型中添加方法,不能使用字面量的方式,这会导致原型的指向改变,因为字面量对象已经是一个新对象了。
function SuperType() {
this.property = true
}
SuperType.prototype.getSuperValue = function () {
return this.property
}
function SubType() {
this.subproperty = false
}
SubType.prototype = new SuperType()
// 使用字面量会改变原型指向!!!
SubType.prototype = {
getSubValue: function () {
return this.subproperty
},
someOtherMethod: function () {
return false
}
}
var instance = new SubType()
alert(instance.getSuperValue) // error!
这时的SubType已经和SuperType没有关系了。
4. 原型链存在的问题
共享原型的引用属性
一旦超类中有一个引用类型的属性,子类的所有实例对象会共享这个属性,一个实例去改变该属性,其他实例都会发生变化。
function SuperType() {
this.colors = ['red', 'blue', 'green']
}
function SubType() {}
SubType.prototype = new SuperType()
var instance1 = new SubType()
instance1.colors.push('black')
alert(instance1.colors) // red,blue,green,black
var instance2 = new SubType()
alert(instance2.colors) // red,blue,green,black
无法向超类构造函数传参
子类实例创建时,是无法向超类传参的,只能被动的使用超类中的属性和方法。
二、借用构造函数
什么是借用构造函数呢?
看下例
function SuperType() {
this.colors = ['red', 'blue', 'green']
}
function SubType() {
// 继承了SuperType
SuperType.call(this)
}
var instance1 = new SubType()
instance1.colors.push('black')
alert(instance1.colors) // red,blue,green,black
var instance2 = new SubType()
alert(instance2.colors) // red,blue,green
这里SubType借用了SuperType中的构造函数代码,看起来就像是在SubType中声明了一个colors数组一样,使得所有的实例都有属于自己的colors。
它解决了什么问题?
能向超类中传参了! 比原型链厉害一点:)
function SuperType(name) {
this.name = name
}
function SubType() {
SuperType.call(this, "Hello")
this.age = 18
}
var instance1 = new SubType()
alert(instance1.name) // Hello
alert(instance1.age) // 18
存在的问题
因为仅仅是借用构造函数,所以无法做到方法复用。这里可能会有疑问,方法在原型中定义不就行了嘛,恭喜你,你已经预想到下一节组合继承的内容了。
三、组合继承
组合继承,是非常经典的继承方式,也是JS中最常用的继承方式,使用了构造函数+原型的方式进行继承。构造函数使实例拥有自己的私有空间(私有属性和方法),原型保证了实例的的公共空间(公共属性和方法)。
function SuperType(name) {
this.name = name
this.colors = ['red', 'blue', 'green']
}
SuperType.prototype.sayName = function () {
alert(this.name)
}
function SubType(name, age) {
// 继承属性
SuperType.call(this, name)
this.age = age
}
// 继承方法
SubType.prototype = new SuperType()
SubType.prototype.constructor = SubType
SubType.prototype.sayAge = function () {
alert(this.age)
}
var instance1 = new SubType('Hello', 18)
instance1.colors.push('black')
alert(instance1.colors) // red,blue,green,black
instance1.sayName() // Hello
instance1.sayAge() // 18
var instance2 = new SubType('World', 19)
alert(instance2.colors) // red,blue,green
instance2.sayName() // World
instance2.sayAge() // 19
那组合继承就完美无缺了吗?并不是的,下面来看看四五六节。
四、原型式继承
这个继承方式有点鸠占鹊巢的感觉,看下例:
function object(o) {
function F() {}
F.prototype = o
return new F()
}
let person = {
name: "Hello",
friends: ['Fir1', 'Fir2', 'Fir3']
}
let anotherPerson = object(person)
anotherPerson.name = "World"
anotherPerson.friends.push('Fir4')
var yetAnotherPerson = object(person)
yetAnotherPerson.name = "Happy"
yetAnotherPerson.friends.push('Fir5')
alert(person.name) // Hello
alert(person.friends) // Fir1,Fir2,Fir3,Fir4,Fir5
看到这里可能会有疑问,为什么name没变而friends就变了呢?这是因为object()这个函数对传入的参数进行了一次浅复制。
何谓浅复制?即基本类型传值,引用类型传址。
通俗的讲,基本类型各用各的,引用类型共用一个。
下面来说说另外一个ES5中的方法
Object.create(par1,par2)
par1: 用作新对象原型的对象
par2: 额外的属性
这个方法在传一个参数的时候和上面例子中的object()方法行为相同;
let person = {
name: "Hello",
friends: ['Fir1', 'Fir2', 'Fir3']
}
let anotherPerson = Object.create(person)
anotherPerson.name = "World"
anotherPerson.friends.push('Fir4')
var yetAnotherPerson = Object.create(person)
yetAnotherPerson.name = "Happy"
yetAnotherPerson.friends.push('Fir5')
alert(person.name) // Hello
alert(person.friends) // Fir1,Fir2,Fir3,Fir4,Fir5
嗯,看起来没什么变化。
par2和Object.defineProperties()方法的第二个参数格式是相同的,通过这个参数传入的属性会覆盖原型对象上的同名属性。
let person = {
name: "Hello",
friends: ['Fir1', 'Fir2', 'Fir3']
}
let anotherPerson = Object.create(person, {
name: {
value: 'World'
}
})
alert(anotherPerson.name) // World
如果传入的是同名的引用类型属性,也会覆盖,就会避免共享这个问题了。
五、寄生式继承
寄生式继承和原型式继承相关,和工厂模式有点类似。
function createAnother(o) {
let clone = object(o) // 通过调用object()函数创建一个新对象
clone.sayHi() = function () { // 增强对象
alert('Hi')
}
return clone // 返回对象
}
let person = {
name: "Hello",
friends: ['Fir1', 'Fir2', 'Fir3']
}
let anotherPerson = createAnother(person)
anotherPerson.sayHi() // Hi
缺点
无法做到函数复用。
嗯? 又不能复用?那再用上原型组合一下?好,这就是下一节的内容了。
用处
为寄生组合式继承做铺垫
六、寄生组合式继承
听着名字有点长,不过不用慌,知道寄生继承和组合继承已经算是了解本节的大部分内容了,来看个例子
// ===================这是组合继承========================================
function SuperType(name) {
this.name = name
this.colors = ['red', 'blue', 'green']
}
SuperType.prototype.sayName = function () {
alert(this.name)
}
function SubType(name, age) {
SuperType.call(this, name) // 第二次调用SuperType()
this.age = age
}
SubType.prototype = new SuperType() // 第一次调用SuperType()
SubType.prototype.constructor = SubType
SubType.prototype.sayAge = function () {
alert(this.age)
}
为什么要再看一遍组合继承呢?这次是来了解它的缺陷的。可以看上面的注释,在组合继承中,调用了两次超类构造函数。
第一次调用: SubType.prototype 会获得name和colors两个属性
第二次调用: 也就是在创建子类实例时调用,子类实例对象同样会获得name和colors这两个属性,
同时会屏蔽原型中的对应同名属性。
嗯,这么一看,这么操作确实有点浪费。
所以,寄生式继承就登场了,它解决了两次调用的问题,可以说是继承方式中的王者。
寄生组合继承基本模式
function inheritPrototype(subType, superType) {
let prototype = object(superType.prototype) // 创建对象
prototype.constructor = subType // 增强对象
subType.prototype = prototype // 指定对象
}
这里只是实现了寄生组合继承的最简单形式。这里做了三步操作:
第一步:创建一个超类原型的副本
第二步:给这个副本添加constructor属性
第三步:子类原型指向这个副本
可能说这些有些难懂,没关系,我们可以和组合继承对比来讲
function SuperType(name) {
this.name = name
this.colors = ['red', 'blue', 'green']
}
SuperType.prototype.sayName = function () {
alert(this.name)
}
function SubType(name, age) {
SuperType.call(this, name) // 调用了一次SuperType()
this.age = age
}
// 这步操作和组合继承不同
inheritPrototype(SubType,SuperType)
SubType.prototype.sayAge = function () {
alert(this.age)
}
区别:
上面说过组合继承调用两次,而寄生组合继承只调用了一次,
也就是inheritPrototype(SubType,SuperType)这步操作避免了一次多余的调用,
取而代之的是通过object(super.prototype)方法创建了原型指向SuperType.prototype一个新的对象,
使SubType.protoType指向了新对象
也就是
SubType.protoType = new SuperType() ==> SubType.protoType = 新对象
一句话总结:“狸猫换太子”
完结。