在上一篇文章中重新梳理了原型链,原型链一个重要的应用场景就是继承,继承是面向对象编程中的一个概念,简单来说就是子类继承父类的特征和行为,使得子类具有父类的属性和方法,这种编程思想也使得代码的可复用性、可读性和可维护性大大提升,是目前大型前端项目架构设计中不可或缺的一种编程思想。
ES5中的继承
在传统的面向对象编程语言,如Java中,继承分为接口继承和实现继承。但是在JavaScript中没有类似于Java中的方法签名,所以JS中的函数是没有重载的,也没有接口继承,下面介绍的几种JS继承方式都是实现继承。
- 原型链继承
- 经典继承
- 组合继承
- 原型式继承
- 寄生式继承
- 寄生组合式继承
原型链继承
由上一篇专栏可知,在JS的原型链中,构造函数A有prototype
属性指向其原型对象,其原型对象有constructor
属性指回构造函数,其实例有__proto__
属性指向A的原型对象。当A的原型对象为另外一个构造函数B的实例时,就说明实例可以通过原型链访问到B的原型对象上的方法,这就是原型链继承的思想,用代码表现如下:
function Parent(){
this.name = 'parent'
this.sex = 'male'
}
Parent.prototype.getName = function(){
console.log(this.name, this.sex)
}
function Child(){
this.name = 'Child' // 由于原型层级的原因会覆盖
this.age = 18
}
Child.prototype = new Parent() // 原型链继承关键步骤
Child.prototype.constructor = Child // 恢复原型对象的constructor指针
Child.prototype.getAge = function(){
console.log(this.age)
}
const instance = new Child();
instance.getName(); // Child male
instance.getAge(); // 18
// 原型链继承会保持实例instanceof和PrototypeOf的特性
console.log(instance instanceof Child) // true
console.log(instance instanceof Parent) // true
console.log(instance instanceof Object) // true
console.log(Child.prototype.isPrototypeOf(instance)) // true
console.log(Parent.prototype.isPrototypeOf(instance)) // true
console.log(Object.prototype.isPrototypeOf(instance)) // true
复制代码
原型链中存在的问题:子类在实例化时无法向父类的构造函数传参,这样就意味着父类构造函数里添加的属性只能是静态值,无法进行动态设置。另外原型链继承的方式会导致子类共享父类中的引用类型的属性,可以看下面的例子:
function Parent(){
this.fruits = ['apple', 'orange']
}
Parent.prototype.getFruits = function(){
console.log(this.fruits)
}
function Child(){}
Child.prototype = new Parent()
const instance1 = new Child();
const instance2 = new Child();
instance1.fruits.push('banana')
instance2.fruits.push('cherry')
instance1.getFruits() // ["apple", "orange", "banana", "cherry"]
instance2.getFruits() // ["apple", "orange", "banana", "cherry"]
复制代码
可以看到instance1
和instance2
同时修改了父类里的fruits
属性,由于原型链继承方式存在这两种问题,导致这种继承方式几乎不会被单独使用
经典继承(盗用构造函数继承)
为了避免原型链继承中共用父类属性的问题,经典继承的思想就是在子类的构造函数中调用父类的构造函数,使父类构造函数在子类的上下文中运行,可以看下面的例子:
function Parent(favorites){
this.fruits = ['apple', 'orange']
this.favorites = favorites
}
function Child(favorites){
Parent.call(this, favorites)
}
Child.prototype.getFruits = function(){
console.log(this.fruits, this.favorites)
}
const instance1 = new Child('banana')
const instance2 = new Child('cherry')
instance1.fruits.push('banana')
instance2.fruits.push('cherry')
instance1.getFruits() // ["apple", "orange", "banana"] "banana"
instance2.getFruits() // ["apple", "orange", "cherry"] "cherry"
复制代码
通过call()
或者apply()
方法使父类构造函数在子类上下文中执行,使得每个子类都独自拥有fruits
属性。并且,通过经典继承的方式,可以在调用构造函数时传入参数执行。
经典继承中存在的问题:经典继承方式中只能将属性和方法写在构造函数里,因此导致无法重用,并且实例也无法调用父类原型对象中的方法,所以这些问题导致经典继承的方式也不会被单独使用
组合继承(伪经典继承)
组合继承结合了原型链继承和经典继承的优点,其思想是利用原型链继承的方式继承父类原型对象上的方法,用经典继承的方式继承父类中的属性,可以看下面例子:
function Parent(favorites){
this.fruits = ['apple', 'orange']
this.favorites = favorites
}
Parent.prototype.getFavorites = function(){
console.log(this.favorites)
}
function Child(favorites){
Parent.call(this, favorites) // 使用了经典继承的特性
}
Child.prototype = new Parent(); // 使用了原型链继承的特性
Child.prototype.constructor = Child // 恢复原型对象的constructor指针
Child.prototype.getFruits = function(){
console.log(this.fruits)
}
const instance1 = new Child('banana')
const instance2 = new Child('cherry')
instance1.fruits.push('banana')
instance2.fruits.push('cherry')
// 可以看到实例都独自继承拥有了fruits属性
instance1.getFruits() // ["apple", "orange", "banana"]
instance2.getFruits() // ["apple", "orange", "cherry"]
// 实例能够使用父类原型对象上的方法
instance1.getFavorites() // banana
instance2.getFavorites() // cherry
复制代码
组合继承的方式弥补了原型链继承和经典继承方式的不足,并且通过组合继承方式创建的实例,能够保留instanceof
和isPrototypeOf()
判断之间关系的能力,组合继承也是JS中广发使用的一种继承方式。
原型式继承
Douglas Crockford在文章《Prototypal Inheritance in JavaScript》中介绍了原型式继承的思想,在文章中给出了一个函数:
function object(o){
// 首先创建一个临时构造函数
function F(){}
// 临时构造函数的原型对象赋值为传进来的对象
F.prototype = o
// 用临时构造函数创建一个实例并返回
return new F()
}
复制代码
可以看下面使用时的例子:
const superMarket = {
owner: 'ashun',
fruits: ['apple', 'orange'],
getInfo(){
console.log(this.owner, this.fruits)
}
}
const familyMart = object(superMarket)
// 由于原型层级,会遮盖原型对象上的简单属性
familyMart.owner = 'tom'
// 引用类型的属性会被共享
familyMart.fruits.push('banana')
// 可以使用原型对象上的方法
familyMart.getInfo()
const walMart = object(superMarket)
walMart.owner = 'Jack'
walMart.fruits.push('cherry')
walMart.getInfo()
复制代码
原型式继承适用于这种情况,你已经有一个对象,但你想在这个对象的基础上增强这个对象的能力或者修改这个对象的属性,此时可以使用原型式继承,把已有对象传给obejct()
,然后在返回的新对象中修改共享属性和调用共享方法。原型模式非常适合不想单独定义一个构造函数,但仍然需要共享数据的情况。但是需要注意的时,如果共享的是引用类型的数据,在修改时修改的会是同一份数据。
在ES5中提出了Object.create()
方法,把原型式继承规范化了,在上面的例子中将object替换成为Object.create
即可。
寄生式继承
寄生式继承结合了原型式继承和工厂模式的思想,创建一个实现继承的工厂方法函数,在函数中增强原始对象并返回,可以看下面这个例子:
// 实现一个寄生式继承的工厂函数
function createSuperMarket(originMarket){
let inner = Object.create(originMarket)
originMarket.getOwner = function(){
console.log(this.owner)
}
return inner
}
// 原始对象
const superMarket = {
owner: 'ashun',
fruits: ['apple', 'orange'],
getInfo(){
console.log(this.owner, this.fruits)
}
}
const familyMart = createSuperMarket(superMarket)
// 增强了getOwner()方法
familyMart.getOwner() // ashun
复制代码
与原型式继承一样,寄生式继承适合不想单独创建构造函数,但仍然想共享方法和属性的场景,但是通过寄生式继承给原始对象添加函数会导致函数难以复用,使用场景比较窄
寄生组合式继承
在解释这个继承方式之前,我们再来回顾一下组合继承的实现代码:
function Parent(favorites){}
function Child(){
Parent.call(this) // 第二次调用Parent()
}
Child.prototype = new Parent() // 第一次调用Parent()
const instance = new Child()
复制代码
可以看到在组合继承方式中,父类构造函数Parent()被调用了两次,如果在parent()
中定义了属性和方法,结果会导致在instance
和Child.prototype
上都会被赋予这些属性和方法,但是由于原型层级的原型,始终会使用instance
上的属性和方法,造成了Child.prototype
上相同的属性和方法的冗余(本来也不希望使用Child.prototype
上面的方法)
使用寄生组合式继承可以优化这一问题,其思想是不通过调用父类的构造函数给子类原型对象赋值,而是取得父类原型对象的一个副本,赋予子类原型对象,可以看下面例子:
function inheritPrototype(parent, child){
// 复制一份父类原型对象的副本
const prototype = Object.create(parent.prototype)
// 修复constructor指针
prototype.constructor = child
// 将父类原型对象的副本赋给子类原型对象
child.prototype = prototype
}
function Parent(favorites){
this.fruits = ['apple', 'orange']
this.favorites = favorites
}
Parent.prototype.getFavorites = function(){
console.log(this.favorites)
}
function Child(favorites){
Parent.call(this, favorites) // 使用了经典继承的特性
}
inheritPrototype(Parent, Child) // 使用寄生组合式继承代替组合继承
Child.prototype.getFruits = function(){
console.log(this.fruits)
}
const instance1 = new Child('banana')
const instance2 = new Child('cherry')
instance1.fruits.push('banana')
instance2.fruits.push('cherry')
// 可以看到实例都独自继承拥有了fruits属性
instance1.getFruits() // ["apple", "orange", "banana"]
instance2.getFruits() // ["apple", "orange", "cherry"]
// 实例能够使用父类原型对象上的方法
instance1.getFavorites() // banana
instance2.getFavorites() // cherry
// 可以看到使用寄生组合式继承时子类原型对象上没有多余的属性和方法
console.log(Child.prorotype) // Parent {constructor: ƒ, getFruits: ƒ}
复制代码
使用寄生组合式继承相比于组合继承,只调用一次父类构造函数,去掉了子类原型对象上冗余的属性和方法,提升了组合继承的性能和效率,同时,寄生组合式继承仍然可以使用instanceof
和isPrototypeOf()
方法来获取实例、子类构造函数和父类构造函数之间的关系。
ES6中的继承
ES6中新增了class和extends关键字,它们其实是语法糖,ES6原生支持单继承,其实现方式和思想仍然是原型链。