JavaScript六种继承方式详解:原型链、构造函数与寄生组合式

作者:纸飞机

时间:2026-10-22

浏览:338

本文详细解析JavaScript中的六种继承方式。从基于原型链的原型继承和借用构造函数的构造函数继承入手,分析其优缺点。进而介绍结合两者优势的组合继承,以及ES5规范的原型式继承和Object.create方法。最后探讨寄生式与寄生组合式继承,重点解决调用两次父类构造函数的问题,提供清晰的技术实现路径。

文章目录

JavaScript继承概述

继承是面向对象编程的核心机制之一。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的六种继承方式,希望对大家有所帮助。如有疑问请留言,小编会及时回复。感谢大家对本站的支持!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章