深入理解 JavaScript 原型与继承机制
本文通过赛车原型的类比,深入解析 JavaScript 中对象、原型与继承的核心机制。详细阐述原型链的工作流程、__proto__ 与 prototype 的区别,以及利用构造函数和 new 操作符实现对象共享方法的最佳实践,帮助开发者掌握 JS 面向对象编程的基础。
暂时放下对传统面向对象编程的固有认知,我们仅从赛车运动的视角来审视这一概念。没错,讨论的核心正是赛车本身。
近期我关注了勒芒 24 小时耐力赛,这是法国极具影响力的赛事。其中速度最快的车型被归类为勒芒原型车。尽管这些车辆由奥迪或标致等厂商制造,但它们并非日常街道或高速公路上常见的汽车类型,而是专为高速耐力竞技专门打造的赛车。
制造商投入巨额资金用于研发、设计及制造这些原型车,工程师们致力于将工程技术推向极致。他们在合金材料、生物燃料、制动系统、轮胎化合物及安全特性等方面进行了大量实验。随着时间推移,部分实验技术经过反复优化后,逐渐应用于主流量产车型中。你日常驾驶的车辆,其部分技术可能最初正是在赛车原型车上首次应用。
换言之,我们可以认为主流量产车继承了源自赛车原型的技术成果。
至此,我们已具备讨论 JavaScript 中原型与继承概念的基础。虽然这种机制不同于 C++、Java 或 C# 中的经典继承模式,但它同样强大,且具备更高的灵活性。
关于对象与类的基本概念
JavaScript 中万物皆对象,这里指的是传统意义上的对象,即“包含状态和行为单一实体”。例如,JavaScript 中的数组是一个包含多个值,并拥有 push、reverse 和 pop 等方法的对象。
var myArray = [1, 2];
myArray.push(3);
myArray.reverse();
myArray.pop();
var length = myArray.length;
现在的问题是,push 等方法从何而来?前文提到的静态语言使用“类语法”来定义对象结构,但 JavaScript 是一种没有“类语法”的语言,无法通过 Array“类”的语法来定义每个数组对象。由于 JavaScript 是动态语言,我们可以在需要时将方法任意添加到对象上。例如,以下代码在二维空间中定义了一个表示点的对象,并添加了一个 add 方法。
var point = {
x : 10,
y : 5,
add: function(otherPoint) {
this.x += otherPoint.x;
this.y += otherPoint.y;
}
};
然而,上述做法的可扩展性较差。我们需要确保每个点对象都包含一个 add 方法,同时希望所有点对象共享同一个 add 方法的实现,而不是手动为每个点对象添加该方法。这正是原型发挥作用的地方。
关于原型的概念
在 JavaScript 中,每个对象都持有一个隐藏的状态——即对另一个对象的引用,称为原型。我们之前创建的数组和自定义的点对象都引用了原型对象。虽然原型引用通常是隐藏的,但部分 ECMAScript(JavaScript 的正式名称)实现允许通过对象的 __proto__ 属性(如 Chrome 浏览器)访问该原型引用。从概念上讲,我们可以将对象与原型的关系理解为图 1 所示的结构。

图 1
展望未来,开发者将使用 Object.getPrototypeOf 函数替代 __proto__ 属性来获取对象原型的引用。在本文撰写时,该函数已在 Google Chrome、Firefox 和 IE9 浏览器中得到支持。随着 ECMAScript 标准的普及,更多浏览器未来将实现此功能。我们可以使用以下代码证明,我们创建的 myArray 和 point 对象引用的是两个不同的原型对象。
- Object.getPrototypeOf(point) != Object.getPrototypeOf(myArray);
在本文后续部分,我将交替使用 __proto__ 和 Object.getPrototypeOf 函数,主要是因为 __proto__ 在图表和文字描述中更易于识别。需要牢记的是,__proto__ 并非标准,而 Object.getPrototypeOf 函数才是查看对象原型的推荐方式。
原型为何如此特殊?
我们尚未回答前文的问题:数组中的 push 等方法从何而来?答案是:它们来源于 myArray 的原型对象。图 2 展示了 Chrome 浏览器脚本调试器的屏幕截图,我们通过调用 Object.getPrototypeOf 方法查看了 myArray 的原型对象。

图 2
请注意,myArray 的原型对象中包含许多方法,包括代码示例中调用的 push、pop 和 reverse 方法。因此,原型对象中确实包含 push 方法,但 myArray 对象如何引用到它呢?
myArray.push(3);
理解其工作原理的第一步是认识到原型并非特殊存在,它只是普通的对象。我们可以给原型添加方法和属性,并将其视为其他 JavaScript 对象一样处理。然而,套用乔治·奥威尔《动物农场》中“所有动物一律平等,但有些动物比其他动物更加平等”的说法——所有对象应当是平等的,但某些对象(遵循特定规则的)比其他对象更加平等。
JavaScript 中的原型对象之所以特殊,是因为它们遵循以下规则:当我们尝试调用对象的 push 方法或读取对象的 x 属性时,运行时环境首先查找对象本身。如果未找到所需成员,它会沿着 __proto__ 引用在原型对象中查找该成员。当我们调用 myArray 的 push 方法时,JavaScript 未在 myArray 对象上发现 push 方法,而是在其原型对象上找到并调用该方法(见图 3)。

图 3
上述行为描述了一个对象如何继承原型上的任何方法或属性。在 JavaScript 中,无需使用类语法即可实现继承。正如从赛车原型继承技术的量产车,JavaScript 对象也可以从原型对象继承功能特性。
图 3 还展示了每个数组对象同时维护自身的状态和成员。在请求 myArray 的 length 属性时,JavaScript 会获取 myArray 中 length 属性的值,而不会读取原型中的对应值。我们可以通过向对象添加 push 等方法来“重写”原型中的 push 方法,从而有效地隐藏原型中的实现。
共享原型
JavaScript 中原型的真正神奇之处在于多个对象如何维持对同一个原型对象的引用。例如,如果我们创建如下两个数组:
var myArray = [1, 2];
var yourArray = [4, 5, 6];
那么这两个数组将共享同一个原型对象,且以下代码计算结果为 true:
Object.getPrototypeOf(myArray) === Object.getPrototypeOf(yourArray);
如果我们引用两个数组对象上的 push 方法,JavaScript 会去寻找原型上共享的 push 方法。

图 4
JavaScript 中的原型对象既提供继承功能,又实现了方法实现的共享。原型也是链式的。换言之,因为原型对象只是一个对象,所以一个原型对象可以维持对另一个原型对象的引用。如果你重新审视图 2,可以看到原型的 __proto__ 属性是一个指向另一个原型的非空值。当 JavaScript 查找像 push 方法这样的成员时,它会沿着原型引用链检查每一个对象,直到找到该成员或抵达原型链末端。原型链为继承和共享开辟了一条灵活的途径。
你可能会问的下一个问题是:如何设置自定义对象的原型引用?例如前文使用的点对象,如何将 add 方法添加到原型对象中,并从多个点对象中继承方法?在回答此问题之前,我们需要先了解函数。
关于函数的概念
JavaScript 中的函数也是对象。这一表述带来了几个重要结果,虽然本文不会涉及所有事项,但其中将函数赋值给变量以及将函数作为参数传递给其他函数的能力,构成了现代 JavaScript 编程表达的基本范式。
我们需要关注的是,函数本身就是对象,因此函数可以拥有自身的方法、属性,并引用一个原型对象。让我们讨论以下代码的含义。
// 这将返回 true:
typeof (Array) === "function"
// 这样的表达式也是:
Object.getPrototypeOf(Array) === Object.getPrototypeOf(function () { })
// 这样的表达式同样:
Array.prototype != null
代码第一行证明,JavaScript 中的数组是函数。稍后我们将看到如何调用 Array 函数创建新数组对象。下一行代码证明,Array 对象使用与任何其他函数对象相同的原型,正如我们看到数组对象间共享相同原型一样。最后一行代码证明,Array 函数都有一个 prototype 属性,该属性指向一个有效对象。这个 prototype 属性至关重要。
JavaScript 中的每个函数对象都有 prototype 属性。切勿将此 prototype 属性与 __proto__ 属性混淆。它们的用途不同,也不指向同一个对象。
// 返回 true
Object.getPrototypeOf(Array) != Array.prototype
Array.__proto__ 提供的是数组原型——可将其视为 Array 函数所继承的对象。
而 Array.prototype 提供的是所有数组的原型对象。也就是说,它提供的是像 myArray 这样数组对象的原型对象,包含了所有数组将会继承的方法。我们可以编写代码来证明这一事实。
// true
Array.prototype == Object.getPrototypeOf(myArray)
// 也是 true
Array.prototype == Object.getPrototypeOf(yourArray);
我们也可以利用这项新知识重绘之前的示意图。

图 5
基于所学知识,请想象创建一个新对象,并让该对象表现得像数组的过程。一种方法是使用以下代码。
// 创建一个新的空对象
var o = {};
// 继承自同一个原型,一个数组对象
o.__proto__ = Array.prototype;
// 现在我们可以调用数组的任何方法...
o.push(3);
虽然这段代码有趣且能运行,但问题在于,并非所有 JavaScript 环境都支持可写的 __proto__ 对象属性。幸运的是,JavaScript 确实有一个创建对象内建的标准机制,只需一个操作符即可创建新对象并设置新对象的 __proto__ 引用——那就是“new”操作符。
var o = new Array();
o.push(3);
JavaScript 中的 new 操作符执行三个基本任务。首先,它创建新的空对象。接下来,它将设置新对象的 __proto__ 属性,以匹配所调用函数的原型属性。最后,操作符调用函数,将新对象作为“this”引用传递。如果要扩展最后两行代码,情况如下:
var o = {};
o.__proto__ = Array.prototype;
Array.call(o);
o.push(3);
函数的 call 方法允许你在调用函数时指定函数内部“this”所引用的对象。当然,函数的作者需要在此情况下实现这样的函数。一旦作者创建了这样的函数,即可称之为构造函数。
构造函数
构造函数与普通函数类似,但具有以下两个特殊性质。
- 通常构造函数的首字母是大写的(这有助于识别构造函数)。
- 构造函数通常与 new 操作符结合使用,用于构造新对象。
Array 就是一个构造函数的例子。Array 函数需要与 new 操作符一起使用,且 Array 的首字母是大写的。JavaScript 将 Array 作为内置函数提供,任何人都可以编写自己的构造函数。事实上,我们可以为前文创建的点对象编写构造函数。
var Point = function (x, y) {
this.x = x;
this.y = y;
this.add = function (otherPoint) {
this.x += otherPoint.x;
this.y += otherPoint.y;
}
}
var p1 = new Point(3, 4);
var p2 = new Point(8, 6);
p1.add(p2);
在上述代码中,我们使用了 new 操作符和 Point 函数来构造点对象,该对象带有 x 属性和 y 属性以及一个 add 方法。你可以将最终结果想象为图 6 所示的结构。

图 6
现在的问题是,我们的每个点对象中仍然有单独的 add 方法。利用我们学到的原型和继承知识,我们更希望将点对象的 add 方法从每个点实例转移到 Point.prototype 中。要实现继承 add 方法的效果,我们所需要做的,就是修改 Point.prototype 对象。
var Point = function (x, y) {
this.x = x;
this.y = y;
}
Point.prototype.add = function (otherPoint) {
this.x += otherPoint.x;
this.y += otherPoint.y;
}
var p1 = new Point(3, 4);
var p2 = new Point(8, 6);
p1.add(p2);
大功告成!我们刚刚在 JavaScript 中完成了原型式的继承模式!

图 7
总结
我希望这篇文章能帮助你揭开 JavaScript 原型概念的神秘面纱。我们从原型如何让一个对象从其他对象中继承功能开始,接着看到如何结合 new 操作符和构造函数来构建对象。这里提到的内容只是开启对象原型力量和灵活性的第一步。本文鼓励你自己发现学习有关原型和 JavaScript 语言的新信息。
同时,请小心驾驶。你永远不会知道这些行驶在路上的车辆会从他们的原型继承到什么(有缺陷)的技术。
原文链接: Script Junkie 翻译: 伯乐在线 - 埃姆杰
JS split切割后出现空字符串怎么过滤
2026-11-05 16:00:30
在 React 或 Vue 项目中封装使用 JS 节流防抖
2026-11-05 15:27:29
JavaScript中怎么将带小数的字符串转为整数?
2026-11-05 14:32:38
怎么用JavaScript策略模式重构电商促销计算逻辑?
2026-11-05 14:01:09
JS使用constructor判断数据类型可靠吗
2026-11-05 13:49:41
