JavaScript 9种继承模式详解与实现
本文系统梳理了JavaScript中9种继承实现方式,涵盖原型链、构造函数借用、属性拷贝及多重继承等核心机制。通过对比各方法的优缺点,解析constructor修正、原型链查找及深拷贝等技术细节,帮助开发者根据场景选择最优继承方案。
JavaScript继承机制概述
与C++和Java等基于类的语言不同,JavaScript采用原型机制实现继承。鉴于JavaScript语言的高度灵活性,开发者可以使用多种不同的模式来实现对象间的继承关系。
理解继承的基础在于掌握构造函数与原型链的概念。通常,父类使用Parent作为构造函数名,子类使用Child作为构造函数名,对应的实例分别命名为parent和child。
每个对象内部都包含一个隐藏属性[[prototype]](在Chrome浏览器中可见为__proto__,部分环境不可直接访问),该属性指向对象的原型。当访问对象的属性或方法时,引擎会先在当前对象中查找;若未找到,则沿[[prototype]]指向的原型链向上搜索,直至找到对应属性或返回undefined。
原型链继承与构造函数修正
1. 原型链继承
原型链是JavaScript实现继承的默认机制。若要使子对象继承父对象,最直观的方法是将子对象构造函数的prototype属性指向父对象的一个实例。
代码如下:
function Parent() {}
function Child() {}
Child.prototype = new Parent()此时,Child的prototype属性被重新赋值指向一个新对象,但该新对象的constructor属性并未自动指向Child。由于JS引擎不会自动修复这一指向,需要手动将Child原型对象的constructor重新指向Child本身。
代码如下:
Child.prototype.constructor = Child上述过程构成了JavaScript默认的继承机制:将可复用的属性和方法移至原型对象,而不可复用的部分设为实例自身的属性。然而,这种方式需要创建一个实例作为原型对象,因此在执行效率上略低。
非原型链与临时构造器继承
2. 非原型链的原型继承
为避免前一种方法中重复创建原型实例的问题,可以直接将子对象构造函数的prototype指向父对象构造函数的prototype。这样,Parent.prototype中的所有属性和方法均可被复用,且无需重复创建原型实例。
代码如下:
Child.prototype = Parent.prototype
Child.prototype.constructor = Child然而,在JavaScript中对象属于引用类型。此方法实际上使Child.prototype和Parent.prototype指向同一个内存地址。因此,若在子对象原型中扩展属性以支持后续继承,父对象的原型也会随之被修改,因为两者共享同一个原型实例。这是该继承方式的主要缺陷。
3. 临时构造器继承
为解决上述问题,可以引入一个临时构造函数作为中间层。所有对子对象原型的操作均在临时构造器的实例上执行,从而避免影响父对象的原型。
代码如下:
var F = function() {}
F.prototype = Parent.prototype
Child.prototype = new F()
Child.prototype.constructor = Child此外,为了在子对象中访问父类原型中的属性,可在子对象构造函数中添加一个指向父对象原型的属性(如uber)。这样,可通过child.constructor.uber直接访问父级原型对象。
可将上述逻辑封装为一个函数,以便后续复用:
代码如下:
function extend(Child, Parent) {
var F = function() {}
F.prototype = Parent.prototype
Child.prototype = new F()
Child.prototype.constructor = Child
Child.uber = Parent.prototype
}调用方式如下:
代码如下:
extend(Dog, Animal)4. 属性拷贝
该继承方式未改变原型链关系,而是将父级原型中的属性直接复制到子对象原型中。需注意,此处的复制仅适用于基本数据类型,对象类型仍遵循引用传递。
代码如下:
function extend2(Child, Parent) {
var p = Parent.prototype
var c = Child.prototype
for (var i in p) {
c[i] = p[i]
}
c.uber = p
}这种方式部分重建了原型属性,虽然构建对象时效率较低,但能减少原型链的查找次数。不过,其优势并不显著。
对象间浅拷贝与深拷贝
5. 对象间继承
除了基于构造器的继承,还可以跳过构造器直接进行对象间的继承,即直接拷贝对象属性,包括浅拷贝和深拷贝两种形式。
浅拷贝: 接收目标对象,创建一个新的空对象,将源对象的属性拷贝至新对象并返回:
代码如下:
function extendCopy(p) {
var c = {}
for (var i in p) {
c[i] = p[i]
}
c.uber = p
return c
}拷贝完成后,若需修改新对象中的属性,可进行手动调整。
深拷贝: 浅拷贝无法处理对象类型属性,仅传递引用。为解决此问题,需使用深拷贝。深拷贝的核心在于递归调用:当检测到对象类型属性时,创建对应的对象或数组,并逐一复制其中的基本类型值。
代码如下:
function deepCopy(p, c) {
c = c || {}
for (var i in p) {
if (p.hasOwnProperty(i)) {
if (typeof p[i] === 'object') {
c[i] = Array.isArray(p[i]) ? [] : {}
deepCopy(p[i], c[i])
} else {
c[i] = p[i]
}
}
}
return c
}此处使用了ES5的Array.isArray()方法判断参数是否为数组。若环境不支持,需手动封装shim。
代码如下:
Array.isArray = function(p) {
return p instanceof Array
}使用instanceof操作符无法判断来自不同框架的数组变量,但这种情况较为少见。
原型继承与Object.create
6. 原型继承
借助父级对象,通过构造函数创建一个以父级对象为原型的新对象:
代码如下:
function object(o) {
var n
function F() {}
F.prototype = o
n = new F()
n.uber = o
return n
}此处直接将父对象设为子对象的原型,ES5中的Object.create()方法即基于此实现。
原型继承与属性拷贝混用
7. 原型继承与属性拷贝混用
在原型继承中,以传入的父对象为原型构建子对象,同时可额外传入需拷贝属性的对象:
代码如下:
function ojbectPlus(o, stuff) {
var n
function F() {}
F.prototype = o
n = new F()
n.uber = o
for (var i in stuff) {
n[i] = stuff[i]
}
return n
}8. 多重继承
此方式不涉及原型链操作,传入多个需拷贝属性的对象,依次进行全量属性拷贝:
代码如下:
function multi() {
var n = {}, stuff, i = 0,
len = arguments.length
for (i = 0; i < len; i++) {
stuff = arguments[i]
for (var key in stuff) {
n[i] = stuff[i]
}
}
return n
}根据传入对象的顺序依次拷贝,若后传入的对象包含与前面对象相同的属性,后者将覆盖前者。
构造器借用与多重继承
9. 构造器借用
JavaScript中的call()和apply()方法常用于改变执行上下文,在继承实现中同样有效。构造器借用指在子对象构造函数中借用父对象的构造函数对this进行操作:
代码如下:
function Parent() {}
Parent.prototype.name = 'parent'
function Child() {
Parent.apply(this, arguments)
}
var child = new Child()
console.log(child.name)该方法的最大优势在于,子对象构造函数中完全重建了自身属性,引用类型变量会生成新值而非引用,因此对子对象的操作不会影响父对象。
然而,该方法在构建过程中未使用new操作符,导致子对象无法继承父级原型上的属性。如上述代码所示,child的name属性将为undefined。
为解决此问题,可手动将子对象构造器的原型设为父对象的实例:
代码如下:
Child.prototype = new Parent()但这会引发父对象构造函数被调用两次:一次在借用过程中,另一次在继承原型时。
为解决此问题,需去除一次父对象构造函数的调用。由于构造器借用不可省略,只能去除后一次调用。实现继承原型的另一种方法是迭代复制:
代码如下:
extend2(Child, Parent)使用之前实现的extend2()方法即可。
JS动态构建复杂嵌套JSON对象的方法
2026-11-05 15:41:52
JS负数取余为什么是负数?详解余数与模运算区别
2026-11-05 13:34:41
JS 解压出现乱码或报错怎么解决?
2026-11-05 12:32:38
JS 中 startsWith、indexOf 与正则判断字符串前缀的对比
2026-11-04 22:19:43
JS解析Excel后怎么将数据转换为JSON格式
2026-11-04 20:58:28
