继承是面向对象编程中讨论最多的话题。很多面向对象语言都支持两种继承:接口继承和实现继承。前者只继承方法签名,后者继承实际的方法。接口继承在ECMAScript中是不可能的,因为函数没有签名。实现继承是ECMAScript唯一支持的继承方式,而这主要是通过原型链实现的。
目录
一、原型链
1. 基本思路
- 子类型的原型为父类型的一个实例
2. 使用
function Father() {
this.name = 'bjl'
this.colors = ['yellow','black']
}
function Son() {}
// Son继承Father原型
Son.prototype = new Father()
// 创建Son实例对象son1
let son1 = new Son()
son1.name = 'bao'
son1.colors.push('green') // 修改原型中引用类型的值colors
console.log(son1.name) // bao
console.log(son1.colors) // ['yellow','black','green']
// 创建Son实例对象son2
let son2 = new Son()
console.log(son2.name) // bjl
// son2会受到影响(如果son1在修改colors值时,直接赋值则不会影响其他实例)
console.log(son2.colors) // ['yellow','black','green']
3. 优缺点
优点:
1)父类在原型上新增的属性和方法都可以被子类访问到
2)来自原型上的属性和方法都会被共享
缺点:
1)共享原型是个优点但也存在问题
2)不能给继承的父类传递参数
二、盗用构造函数
1. 基本思路
- 在子类构造函数中调用父类构造函数。使用 apply() 和 call() 方法以新创建的对象为上下文执行构造函数(上一篇博文详细讲解了 apply() 和 call() 方法的使用)
2. 使用
function Father() {
this.colors = ['yellow','black']
}
function Son() {
// this指向Father类中的对象,继承Father类
// 此方法相当于每创建一个新的Son实例,都会初始化代码重新继承Father类
Father.call(this) // 使用apply()也可以
}
let son1 = new Son()
son1.colors.push('green')
console.log(son1.colors) // ['yellow','blcak','green']
let son2 = new Son()
console.log(son2.colors) // ['yellow','black']
3. 优缺点
优点:
可以在子类构造函数中向父类构造函数传递参数(解决了原型包含引用值导致的继承问题)。
function Father(name,age) {
this.name = name
this.age = age
}
function Son(name,age,sex) {
Father.call(this,name,age)
this.sex = sex
}
let son = new Son('bjl',18,'女')
console.log(son) // {name:'bjl',age:18,sex:'女'}
缺点:
继承来的只有父类的属性,而原型上的属性是访问不到的;没有办法复用,每个子类都有父类实例函数的副本,影响性能。
function Father() {
this.name = 'bjl'
}
// 在原型上定义属性
Father.prototype.age = 18
function Son() {
Father.call(this)
}
let son = new Son()
console.log(son.name) // bjl
console.log(son.age) // undefined
三、组合继承(使用最多的继承方式)
1. 基本思路
- 综合了原型链和盗用构造函数,将两者的优点集中在了一起
- 使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性。这样既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性
2. 使用
function Father(name) {
this.name = name
this.colors = ['yellow','black']
}
// 在原型上添加属性
Father.prototype.sex = '女'
function Son(name) {
// 继承属性
Father.call(this,name)
}
// 继承原型上sex属性
Son.prototype = new Father()
let son1 = new Son('bjl')
son1.colors.push('green') // 修改colors的值(不会与其他实例产生影响)
console.log(son1.name) // bjl
console.log(son1.colors) // ['yellow','black','green']
console.log(son1.sex) // '女'
let son2 = new Son('bao')
console.log(son2.name) // bao
console.log(son2.colors) // ['yellow','black']
console.log(son2.sex) // '女'
3. 优缺点
优点:
1)可以继承父类属性和方法,也能继承父类原型的属性和方法
2)不存在引用属性共享问题
3)可以传参
4)函数可复用
缺点:
无论在什么情况下,都会调用两次父类的构造函数:一次是在创建子类原型的时候,另一次是在子类构造函数的内部。
四、原型式继承
1. 出发点
- 即使不自定义类型也可以通过原型实现对象之间的信息共享
2. 使用
1)与使用原型模式是一样的,属性中包含的引用值始终会在相关对象间共享
function object(o) { // 传递一个字面量函数
function F() {} // 创建一个构造函数
F.prototype = o // 把字面量函数赋值给构造函数的原型
return new F() // 最终返回出实例化的构造函数
}
let person = {
name: 'bjl',
family: ['爸爸','妈妈']
}
// 把person对象中的属性放在了实例对象p1、p2的原型上
let p1 = object(person)
p1.family.push('哥哥')
console.log(p1.name) // bjl
console.log(p1.family) // ['爸爸','妈妈','哥哥']
let p2 = object(person)
console.log(p2.name) // bjl
// 引用值会受到影响
console.log(p2.family) // ['爸爸','妈妈','哥哥']
2)使用场景:适合在不需要单独创建构造函数,但仍然需要在对象间共享信息的场合
3. Object.create() 方法
1)此方法将原型式继承的概念规范化了
let person = {
name: 'bjl',
family: ['爸爸','妈妈']
}
let p1 = Object.create(person)
console.log(p1.name) // bjl
2)此方法包含两个参数:第一个参数作为新对象原型的对象(person),第二个参数是给新对象定义额外属性的对象(这种方式添加的属性会遮蔽原型对象上的同名属性,但不会与其他实例产生影响)
let person = {
name: 'bjl',
family: ['爸爸','妈妈']
}
// 注意第二个参数的写法,是一个对象
let p1 = Object.create(person, {
name: {
value: 'bao'
}
})
console.log(p1.name) // bao
let p2 = Object.create(person)
console.log(p2.name) // bjl
五、寄生式继承
1. 基本思路
- 创建一个实现继承的函数,以某种方式增强对象,然后返回这个对象
2. 使用
function createAnother(o) {
// 使用上面提到过的Object.create()方法
let clone = Object.create(o) // 将o对象赋值在新对象的原型上
clone.sayHi = function() { // 以某种方式增强这个对象
console.log('Hi')
}
return clone // 返回这个对象
}
let person = {
name: 'bjl',
family: ['爸爸','妈妈']
}
let p1 = createAnother(person)
p1.sayHi() // Hi
console.log(p1) // 实例对象p1上存在sayHi函数,而name、family属性存在于实例对象p1的原型上
3. 缺点
- 使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率
六、寄生式组合继承(很多大厂使用的继承方式)
1. 基本思路
-
上面说到的组合继承存在一定的问题:最重要的效率问题就是父类构造函数始终会被调用两次
- 寄生式组合继承的基本思路是不通过调用父类构造函数给子类原型复赋值,而是取得父类原型的一个副本。简单点来说就是,使用寄生式继承来继承父类原型,然后将返回的新对象赋值给子类原型
2. 使用
function inheritPrototype(father,son) {
let prototype = Object.create(father.prototype)
prototype.constructor = son
son.prototype = prototype
}
function Father(name) {
this.name = name
}
Father.prototype.sex = '女'
function Son(name,age) {
Father.call(this,name)
this.age = age
}
// 调用函数
inheritPrototype(Father,Son)
let son = new Son('bjl',18)
console.log(son)
3. 优点
- 上述例子的高效率体现在它只调用了一次 Father 构造函数,并且因此避免了在 Son.prototype 上面创建不必要的、多余的属性,与此同时,原型链还能保持不变