JavaScript 9种继承模式详解与实现

作者:纸飞机

时间:2026-10-30

浏览:661

本文系统梳理了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()方法即可。

实用工具推荐

电子包浆

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

使用 ›
图片压缩

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

使用 ›
相关文章