JavaScript原型与原型链深度解析
本文深入解析JavaScript中的原型(prototype)与原型链机制。详细阐述所有对象均源自Object的概念,区分构造函数、实例与函数之间的关系。通过代码示例说明prototype与__proto__的区别,解释原型覆盖导致的constructor丢失问题及其修复方法,并梳理从Function到Object的原型链指向规则,帮助开发者理解JS基于原型的对象创建与继承逻辑。
JavaScript原型基础概念
要理解JS中的prototype,首先必须弄清楚以下几个概念 1. JS中所有的东西都是对象
在JavaScript中,万物皆对象。这意味着无论是基本数据类型包装后的对象,还是复杂的自定义结构,在JS的世界里都被视为对象。
// ["constructor", "toString", "toLocaleString", "valueOf", "hasOwnProperty", "isPrototypeOf",
// "propertyIsEnumerable", "__defineGetter__", "__lookupGetter__", "__defineSetter__",
// "__lookupSetter__"]
console.log(Object.getOwnPropertyNames(Object.prototype));
进一步而言,JavaScript中所有的对象最终都由Object衍生而来,即所有对象的原型链终点都指向Object.prototype。此外,还需要理解构造函数与实例(对象)之间紧密且微妙的关系:构造函数通过定义prototype属性来规定其实例的规范,并通过new关键字生成实例,其核心作用是创建对象。值得注意的是,构造函数本身也是Function类的实例,因此它同样拥有__proto__(原型链)属性。
例如Object或function F() {}这样的结构属于构造函数,前者是JS原生API提供的,后者为自定义构造函数。而new Object()或new F()生成的则是实例。对于实例而言,它“只能”通过查看__proto__来追溯自己是基于哪个prototype构建的,而“不能”直接重新定义实例的prototype属性去创造所谓的“实例的实例”。
理论结合实践,只有亲自观察和思考,才能真正领悟原型机制的运作方式。
// 先来看看构造函数到底是什么
// function Empty() {} function Empty() {}
console.log(Function.prototype, Function.__proto__);
// Object {} function Empty() {}
console.log(Object.prototype, Object.__proto__);
function F() {}
// F {} function Empty() {}
console.log(F.prototype, F.__proto__);
如果上述概念让你感到困惑,我们可以逐步分解来看。
关于prototype,其输出格式通常表现为“构造函数名 原型”。首先观察Object.prototype的输出结果:Object {},其中前面的Object代表构造函数名称,后面的{}表示原型,这里是一个Object对象的实例(即空对象)。同理,对于F {},F是构造函数名称,其原型也是一个空对象。
// 再来看看由构造函数构造出来的实例
var o = new Object(); // var o = {};
// undefined Object {}
console.log(o.prototype, o.__proto__);
function F() {}
var i = new F();
// undefined F {}
console.log(i.prototype, i.__proto__);
接下来我们深入一步,尝试定义F的原型,观察具体会发生什么变化。
function F() {}
F.prototype.a = function() {};
var i = new F();
// undefined F {a: function}
console.log(i.prototype, i.__proto__);
通过观察结果可以清晰地看到,实例i是由F构造出来的,其原型变为{a: function}。这意味着在原本的空对象原型基础上,新增了一个名为a的方法。
我们再换一种情况,如果完全覆盖F的原型,结果会如何?
function F() {}
F.prototype = {
a: function() {}
};
var i = new F();
// undefined Object {a: function}
console.log(i.prototype, i.__proto__);
这里会出现一个看似矛盾的现象:实例i似乎是由Object构造出来的。这并非错误,而是因为我们完全覆盖了F的prototype,将其指定为对象{a: function}。这一操作导致原本的constructor信息丢失,转而使用对象{a: function}所指定的constructor。那么,对象{a: function}的constructor究竟是谁呢?
var o = {a: function() {}} // new了一个Object
由于对象{a: function}本质上是Object的实例,因此其constructor自然指向Object。
为了解决这个问题,我们需要对代码进行修正。
function F() {}
F.prototype = {
a: function() {}
}
// 重新指定正确的构造函数
F.prototype.constructor = F;
var i = new F();
// undefined F {a: function, constructor: function}
console.log(i.prototype, i.__proto__);
经过修正后,我们可以再次获得正确的原型信息,确保constructor指向正确的构造函数。
原型链机制解析
接下来探讨原型链的概念。
原型链在概念上与面向对象编程(OOP)中的继承关系(链)相似,即一层一层向上查找,直至最终到达Object.prototype。

最关键的是要弄清楚JavaScript中哪些东西是(实例)对象。这很简单,因为JS中所有东西都是对象。同时需要明确,任何一个对象都拥有一个原型。
我们可以通过以下代码来证明这一点。
Object // 这是一个函数, 函数是 Function 的实例对象, 那么就是由 Function 构造出来的
Object.__proto__ == Function.prototype // 那么Object的原型, true
// 这个是一个普通对象了, 因此属于 Object 的实例
Function.prototype.__proto__ == Object.prototype // true
// 这已经是原型链的最顶层了, 因此最终的指向 null
Object.prototype.__proto__ == null // true
Function // 这也是一个函数, 没错吧!
Function.__proto__ == Function.prototype // true
function A() {} // 这是一个自定义的函数, 终归还是一个函数, 没错吧!
A.__proto__ == Function.prototype // 任何函数都是 Function 的实例, 因此A的原型是?
var a = new A()
a.__proto__ == A.prototype // 实例a是由A构造函数构造出来的, 因此a的原型是由A的prototype属性定义的
A.prototype.__proto__ == Object.prototype // 普通对象都是 Object 的示例
Prototype与__proto__的关系
关于Prototype和__proto__的区别:每一个对象都包含一个__proto__属性,它指向该对象的原型。类似地,每一个函数都包含一个prototype属性,这个prototype对象的作用是什么?
让我们看下面的代码,这里使用构造函数来创建一个对象(此前使用的是字面量形式创建对象)。
function Foo(){};
var foo = new Foo();
console.log(foo.__proto__);
试想一下,这个foo对象的__proto__会指向哪里?

这是一个包含constructor属性的对象吗?如果看不太懂,不妨将函数Foo的prototype属性打印出来,进行对比即可明白。
function Foo(){};
var foo = new Foo();
console.log(foo.__proto__);
console.log(Foo.prototype);
console.log(foo.__proto__ === Foo.prototype);

结果显示,new出来的对象foo的__proto__确实指向函数Foo的prototype。
foo.__proto__ --> Foo.prototype
JavaScript这样设计的意义何在?回顾之前提到的观点,在JS的世界中,对象不是根据类(模具)创建的,而是从原型(另一个对象)衍生出来的。
当我们执行new操作创建新对象时,虽然不深入new的具体实现细节,但可以肯定一点:新对象的__proto__会指向一个原型对象。
针对刚才这段代码进行分析。
function Foo(){};
var foo = new Foo();
foo.__proto__到底应该指向谁?它不能指向Foo这个函数本身,尽管函数也是对象,但这会在后续详细讨论。如果foo.__proto__直接指向Foo函数,虽然技术上可行,但并不合适。因为Foo是一个包含大量逻辑代码的函数,而foo作为一个对象,继承函数的逻辑处理没有意义,它真正需要继承的是“原型对象”的属性。
因此,每个函数会自动生成一个prototype对象,由该函数new出来的对象的__proto__就指向这个函数的prototype。
foo.__proto__ --> Foo.prototype
原型机制总结与图示
总结部分,虽然文字描述可能仍显抽象,但配合图表会更清晰。我曾参考过其他资料中的图表,但觉得解释不够透彻,因此自己绘制了一张图。如果觉得这张图有帮助,请给予认可(绘制此图确实耗费了大量精力)。

结合这张图,我们需要记住以下几个关键事实:
1. 每个对象中都包含一个__proto__属性。
在JS世界中,不存在类(模具)的概念,对象是从另一个对象(原型)衍生出来的。因此,每个对象中会有一个__proto__属性指向它的原型对象。(参考左上角用字面量形式定义的对象obj,它在内存中开辟空间存放自身属性的同时,生成一个__proto__指向其原型——顶层原型对象。)
2. 每个函数都有一个prototype属性。
“构造函数”之所以叫构造函数,是因为它负责构造对象。根据上述第一条事实,构造出的新对象的__proto__属性指向哪里?总不能指向构造函数自身,尽管它也是对象,但你并不希望新对象继承函数的属性与方法。因此,在每个构造函数中都有一个prototype属性,指向一个对象,作为该构造函数创建的新对象的原型。
3. 函数也是对象。
每个函数都有一些通用的属性和方法,例如apply()/call()等。但这些通用方法是如何继承的?函数又是如何创建出来的?试想,一切皆对象,包括函数也是对象,且是通过构造函数创建的对象。根据上述第二条事实,每个函数也会有__proto__指向其构造函数的prototype。而这个构造函数的函数就是Function,JS中的所有函数都是由Function构造出来的。函数的通用属性与方法就存放在Function.prototype这个原型对象上。
JS语法错误会阻止加载吗?区分SyntaxError与资源加载失败
2026-11-05 16:08:46
JavaScript常见SyntaxError语法错误排查与修复方法
2026-11-05 13:58:44
JS中slice、substring和substr截取字符串的区别
2026-11-05 13:43:47
JS 转 TS 手动迁移最佳实践:从配置 tsconfig 到消除 any 类型
2026-11-05 11:53:32
为什么JS数组push后得到的是数字而不是数组?
2026-11-05 09:59:18
