本文详细解析JavaScript中的六种继承方式。从基于原型链的原型继承和借用构造函数的构造函数继承入手,分析其优缺点。进而介绍结合两者优势的组合继承,以及ES5规范的原型式继承和Object.create方法。最后探讨寄生式与寄生组合式继承,重点解决调用两次父类构造函数的问题,提供清晰的技术实现路径。
JavaScript继承概述
继承是面向对象编程的核心机制之一。JavaScript仅支持实现继承,不支持接口继承,其实现主要依赖于原型链机制。

原型链继承
原型链
理解原型链需先掌握与prototype的关系及区别,相关原理在《一篇文章看懂proto和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()
console.log(instance.getSuperValue()) // true
代码中定义了两个类型SuperType和SubType,各自包含属性和方法。SubType通过创建SuperType的实例,并将该实例赋值给SubType.prototype来实现继承。
这种实现的本质是重写原型对象,将其替换为新类型的实例。因此,SuperType实例中所有的属性和方法,现在都存在于SubType.prototype中。
创建实例时,实例对象内部会有一个指针指向其原型。在代码SubType.prototype = new SuperType()中,SubType.prototype也会创建一个内部指针,从而与SuperType关联起来。
因此,instance指向SubType的原型,SubType的原型又指向SuperType的原型。当instance调用getSuperValue()方法时,会沿着这条链向上查找。
添加方法
在SubType原型上添加方法时,若父类存在同名方法,SubType将覆盖该方法以实现重写。但需注意,父类中的该方法依然保留。
切勿使用字面量形式添加方法。因为前文提到原型继承本质是重写,若再使用字面量形式,相当于再次重写,这会切断与父类的关联,导致原型链断裂。
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()
console.log(instance.getSuperValue()) // error
问题
单纯使用原型链继承的主要缺陷在于包含引用类型值的原型属性。
function SuperType() {
this.colors = ['red', 'blue', 'green']
}
function SubType() {
}
SubType.prototype = new SuperType()
var instance1 = new SubType()
var instance2 = new SubType()
instance1.colors.push('black')
console.log(instance1.colors) // ["red", "blue", "green", "black"]
console.log(instance2.colors) // ["red", "blue", "green", "black"]
若SuperType构造函数中定义了colors属性,SubType通过原型链继承后,该属性会出现在SubType.prototype中,如同专门创建了SubType.prototype.colors。这导致所有SubType实例共享该属性。因此,instance1修改colors引用类型值,也会反映到instance2中。
借用构造函数继承
借用构造函数
此方法旨在解决原型中包含引用类型值所带来的共享问题。
其思想是在子类构造函数的内部调用父类构造函数,并可借助apply()和call()方法改变对象的执行上下文。
function SuperType() {
this.colors = ['red', 'blue', 'green']
}
function SubType() {
// 继承SuperType
SuperType.call(this)
}
var instance1 = new SubType()
var instance2 = new SubType()
instance1.colors.push('black')
console.log(instance1.colors) // ["red", "blue", "green", "black"]
console.log(instance2.colors) // ["red", "blue", "green"]
在创建SubType实例时调用SuperType构造函数,使得在SubType对象上执行SuperType函数中定义的所有初始化代码。
结果是,SubType的每个实例都拥有自己独立的colors属性副本。
传递参数
借助构造函数继承还有一个优势,即可以向父类构造函数传递参数。
function SuperType(name) {
this.name = name
}
function SubType() {
// 继承SuperType
SuperType.call(this, 'Jiang')
this.job = 'student'
}
var instance = new SubType()
console.log(instance.name) // Jiang
console.log(instance.job) // student
问题
若仅使用构造函数继承,由于方法都在构造函数内部定义,导致函数无法实现复用。
组合继承
组合继承(原型链+构造函数)
组合继承结合了原型链继承和构造函数继承,旨在发挥两者的优势。
其思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。
这样既通过在原型上定义方法实现了函数复用,又保证了每个实例拥有自己的属性副本。
function SuperType(name) {
this.name = name
this.colors = ['red', 'blue', 'green']
}
SuperType.prototype.sayName = function () {
console.log(this.name)
}
function SubType(name, job) {
// 继承属性
SuperType.call(this, name)
this.job = job
}
// 继承方法
SubType.prototype = new SuperType()
SubType.prototype.constructor = SuperType
SubType.prototype.sayJob = function() {
console.log(this.job)
}
var instance1 = new SubType('Jiang', 'student')
instance1.colors.push('black')
console.log(instance1.colors) //["red", "blue", "green", "black"]
instance1.sayName() // 'Jiang'
instance1.sayJob() // 'student'
var instance2 = new SubType('J', 'doctor')
console.log(instance2.colors) // //["red", "blue", "green"]
instance2.sayName() // 'J'
instance2.sayJob() // 'doctor'
该模式避免了原型链和构造函数继承各自的缺陷,融合了优点,是最常用的继承模式。
原型式与Object.create
原型式继承
借助原型,可以基于已有对象创建新对象,且无需创建自定义类型。
function object(o) {
function F() {}
F.prototype = o
return new F()
}
在object函数内部,先创建一个临时性构造函数,将传入的对象作为该构造函数的原型,最后返回该临时类型的一个新实例。
本质上,object对传入的对象执行了一次浅复制。
var person = {
name: 'Jiang',
friends: ['Shelby', 'Court']
}
var anotherPerson = object(person)
console.log(anotherPerson.friends) // ['Shelby', 'Court']
使用该模式的前提是必须有一个对象作为另一个对象的基础。
例如,将person作为基础传入object函数,该函数返回一个新对象。
新对象以person为原型,因此其原型中包含基本类型和引用类型。
这意味着,若另一个对象anotherPerson也关联了person,当anotherPerson修改数组friends时,该变化也会体现在原对象中。
Object.create()方法
ES5通过Object.create()方法规范了原型式继承。该方法接受两个参数:用作新对象原型的对象,以及可选的为新对象定义额外属性的对象。其行为与前述object函数基本一致,区别在于Object.create()可接受第二个参数。
var person = {
name: 'Jiang',
friends: ['Shelby', 'Court']
}
var anotherPerson = Object.create(person)
console.log(anotherPerson.friends) // ['Shelby', 'Court']
寄生式继承
寄生式继承
寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数。
function createAnother(o) {
var clone = Object.create(o) // 创建一个新对象
clone.sayHi = function() { // 添加方法
console.log('hi')
}
return clone // 返回这个对象
}
var person = {
name: 'Jiang'
}
var anotherPeson = createAnother(person)
anotherPeson.sayHi()
基于person返回新对象anotherPerson,该对象不仅拥有person的属性和方法,还具备自己的sayHi方法。
在主要考虑对象而非自定义类型和构造函数的场景下,这是一种有用的模式。
寄生组合式继承
寄生组合式继承
在前述组合模式(原型链+构造函数)中,继承过程需要调用两次父类构造函数。
父类
function SuperType(name) {
this.name = name
this.colors = ['red', 'blue', 'green']
}
第一次调用发生在子类构造函数内部。
function SubType(name, job) {
// 继承属性
SuperType.call(this, name)
this.job = job
}
第二次调用是将子类的原型指向父类的实例时发生。
// 继承方法
SubType.prototype = new SuperType()
当使用var instance = new SubType()时,会产生两组name和color属性:一组在SubType实例上,另一组在SubType原型上。虽然实例上的属性屏蔽了原型上的属性,但这仍是不必要的开销。
使用寄生式组合模式可以规避此问题。
该模式通过借用构造函数继承属性,通过原型链的混成形式继承方法。
基本思路:无需为了指定子类型的原型而调用父类构造函数,我们只需获取父类原型的一个副本。
本质上,这是使用寄生式继承来继承父类原型,并将结果指定给子类型的原型。
function inheritPrototype(subType, superType) {
var prototype = Object.create(superType.prototype)
prototype.constructor = subType
subType.prototype = prototype
}
该函数实现了寄生组合继承的最简形式。
此函数接受两个参数:子类和父类。
第一步创建父类原型的副本,第二步为创建的副本添加constructor属性,第三步将子类的原型指向该副本。
function SuperType(name) {
this.name = name
this.colors = ['red', 'blue', 'green']
}
SuperType.prototype.sayName = function () {
console.log(this.name)
}
function SubType(name, job) {
// 继承属性
SuperType.call(this, name)
this.job = job
}
// 继承
inheritPrototype(SubType, SuperType)
var instance = new SubType('Jiang', 'student')
instance.sayName()
补充:直接使用Object.create实现,相当于将上述封装函数拆解,这样演示更易于理解。
function SuperType(name) {
this.name = name
this.colors = ['red', 'blue', 'green']
}
SuperType.prototype.sayName = function () {
console.log(this.name)
}
function SubType(name, job) {
// 继承属性
SuperType.call(this, name)
this.job = job
}
// 继承
SubType.prototype = Object.create(SuperType.prototype)
// 修复constructor
SubType.prototype.constructor = SubType
var instance = new SubType('Jiang', 'student')
instance.sayName()
ES6新增的Object.setPrototypeOf方法可直接创建关联,且无需手动添加constructor属性。
// 继承
Object.setPrototypeOf(SubType.prototype, SuperType.prototype)
console.log(SubType.prototype.constructor === SubType) // true
以上介绍了JavaScript的六种继承方式,希望对大家有所帮助。如有疑问请留言,小编会及时回复。感谢大家对本站的支持!