JavaScript 的对象模型与传统面向对象语言存在明显差别,它并不依赖类作为创建对象的蓝图,而是通过原型机制组织对象之间的关系。每一个对象在诞生时都会与另一个对象建立关联,这个被关联的对象就是它的原型。当访问一个对象的属性或方法时,如果对象本身没有定义该成员,查找过程就会沿着这种关联继续向原型上延伸,从而形成一条可以逐级追溯的路径。理解这条路径的构成方式、查找顺序以及它如何支撑继承,是掌握 JavaScript 对象体系的关键。

原型相关的基础概念
在 JavaScript 中,函数和普通对象分别扮演着不同角色,因此出现了两个容易混淆但又极为重要的属性:prototype 与 __proto__。前者只属于函数对象,后者则属于所有普通对象。二者从不同侧面描述了原型关系,共同构成了原型链的静态配置与动态查找机制。
prototype 是函数在创建时自动获得的一个普通对象,通常称为原型对象。这个对象主要用于存放将来由该函数创建的实例所共享的属性和方法。默认情况下,prototype 对象内部会带有一个 constructor 属性,该属性指回函数自身。这一设计让实例可以通过原型间接找到自己的构造函数,从而保持类型信息不丢失。
__proto__ 则存在于每一个对象中,它记录了该对象在创建时所关联的原型对象。对于通过构造函数生成的实例来说,实例的 __proto__ 指向构造函数的 prototype。虽然在浏览器环境中可以直接访问 __proto__,但更规范的做法是使用 Object.getPrototypeOf() 来获取对象的原型,这样能够避免不同宿主环境对非标准属性支持不一致带来的问题。
原型链的构建过程
当通过 new 运算符调用构造函数创建实例时,新生成的实例对象会自动获得一个内部关联,指向构造函数的原型对象。这个关联就是 __proto__。由于原型对象本身也是一个对象,它同样拥有自己的 __proto__,因此这种指向关系会继续向上延伸,直到某个对象的原型为 null。这样一级一级串联起来的链式结构,就是原型链。
以自定义构造函数 Person 为例,当执行 new Person() 后,会生成一个新的实例对象。该实例的 __proto__ 指向 Person.prototype,而 Person.prototype 本身是对象,它的 __proto__ 指向内置的 Object.prototype。最后,Object.prototype 的原型指向 null,表示原型链到达终点。整个查找路径可以简单描述为:实例对象、Person.prototype、Object.prototype、null。
需要特别说明的是,Object.prototype 是所有普通对象原型链的最终公共节点。它不仅终结了默认的原型链,还提供了 toString、hasOwnProperty 等基础方法。正因为所有普通对象最终都能找到它,这些方法才能被任何普通对象调用。而 null 之后没有任何对象,因此属性查找会在此处自然结束。
// 定义构造函数
function Person(name) {
this.name = name;
}
// 为原型对象添加共享方法
Person.prototype.sayName = function() {
console.log(this.name);
};
// 创建实例对象
const person1 = new Person('张三');
// 实例的原型指向 Person.prototype
console.log(person1.__proto__ === Person.prototype); // 输出 true
// Person.prototype 的原型指向 Object.prototype
console.log(Person.prototype.__proto__ === Object.prototype); // 输出 true
// Object.prototype 的原型指向 null
console.log(Object.prototype.__proto__ === null); // 输出 true
原型链的属性查找规则
当读取对象的某个属性时,JavaScript 引擎并不会只在对象自身进行查找,而是按照固定的路径逐级向上搜索。查找过程可以概括为:先搜索对象自身的自有属性;如果找不到,就搜索该对象的直接原型;如果还是找不到,就继续搜索原型的原型,直到找到属性或者原型为 null。一旦在某层找到了目标属性,查找立即停止,即使后续层级中存在同名属性也不会继续访问。
这种规则也带来了属性遮蔽现象。如果对象自身定义了一个与原型链上层同名的属性,那么在读取该属性时,对象自身的属性会优先返回,上层的同名属性被暂时遮蔽。只有当自身的属性被删除后,上层的属性才会重新暴露出来。理解这一点对于调试多层继承关系和对象结构非常有帮助,也能避免误判属性来源。
如果整个原型链上都没有找到目标属性,读取结果就是 undefined。需要注意的是,undefined 不一定表示属性不存在,也可能属性本身就显式赋值为 undefined。因此更准确的存在性判断应该使用 hasOwnProperty 或 in 运算符:前者只检查对象自身是否拥有该属性,后者则会沿着原型链检查属性是否存在。
// 父类构造函数
function Animal() {
this.type = '动物';
}
// 在父类原型上添加方法
Animal.prototype.eat = function() {
console.log('吃东西');
};
// 子类构造函数
function Dog(name) {
this.name = name;
}
// 将 Dog 的原型替换为 Animal 的实例
Dog.prototype = new Animal();
const dog1 = new Dog('小黑');
// 访问实例自身属性
console.log(dog1.name); // 输出:小黑
// 访问父类实例属性,位于原型链上一层
console.log(dog1.type); // 输出:动物
// 调用父类原型上的方法
dog1.eat(); // 输出:吃东西
// 访问不存在的属性
console.log(dog1.age); // 输出:undefined
原型链与继承的关系
JavaScript 中传统的继承方式主要依赖原型链。实现思路相对直接:让子类构造函数的 prototype 指向父类构造函数的实例。这样一来,子类创建的实例对象在查找属性时,会先经过子类自己的原型,再经过父类实例,最后到达父类原型以及更上层的对象。前面代码中的 Dog.prototype = new Animal() 就是这种思想的直接体现。
这种继承方式虽然简单,但也存在明显不足。由于父类实例被放在子类原型上,父类实例中定义的属性会被所有子类实例共享。如果这些属性是引用类型,例如数组或普通对象,那么一个实例修改了属性内容后,其他实例也会受到影响。此外,这种方式无法在创建子类实例时向父类构造函数传递参数,限制了继承的灵活性。
为了解决这些问题,实际开发中通常会把原型链继承与其他手段结合起来使用。常见做法包括在子类构造函数内部通过 call 或 apply 调用父类构造函数,从而让每个子类实例都拥有一份独立的父类实例属性,同时仍然把父类原型方法保留在原型链上供共享。这种组合方式既保证了实例属性的独立性,又保留了方法复用,而原型链始终是整个继承体系的底层基础。
常见误区与最佳实践
一个常见的混淆点是把 prototype 和 __proto__ 理解为同一个东西。实际上,prototype 只属于函数对象,是函数用来配置实例原型的一个属性;而 __proto__ 属于所有普通对象,表示对象自身的原型指向。函数本身也是对象,因此函数也有 __proto__,并且函数的 __proto__ 通常指向内置的 Function.prototype。这两者分别回答了两个不同的问题:函数的实例原型是什么,以及函数自身的原型是什么。
虽然 __proto__ 在主流浏览器中都能访问,但它并不是 ECMAScript 标准中推荐的属性。规范中更倾向于使用 Object.getPrototypeOf() 来读取对象的原型,使用 Object.setPrototypeOf() 来设置原型。这些 API 语义更清晰,也能避免一些与 __proto__ 相关的古怪行为。下面这段代码展示了两种获取原型方式的结果以及函数自身原型的指向。
// 定义一个空构造函数
function Test() {}
// 创建实例对象
const testInstance = new Test();
// 两种获取对象原型的方式
console.log(testInstance.__proto__ === Object.getPrototypeOf(testInstance)); // 输出 true
// 函数对象自身的原型指向 Function.prototype
console.log(Object.getPrototypeOf(Test) === Function.prototype); // 输出 true
// 函数原型的原型最终指向 Object.prototype
console.log(Object.getPrototypeOf(Function.prototype) === Object.prototype); // 输出 true
在修改原型时也要注意维护构造函数引用。如果直接覆盖构造函数的 prototype 对象,原有 constructor 属性会丢失,可能导致类型判断错误。因此在实际编码时,通常会在覆盖后手动补上 constructor 属性,或者使用 Object.assign 在既有原型对象上扩展方法,避免直接替换整个原型对象而破坏原有关系。
总的来说,原型链的本质是对象之间通过原型引用形成的链式查找结构。掌握 prototype 与 __proto__ 的区别、理解属性查找的先后顺序,以及清楚原型链在继承中的基础地位,能够帮助开发者在实际项目中更准确地设计对象关系、定位属性来源,并避免因原型遮蔽或共享引用导致的潜在问题。
JavaScript原型链prototype__proto__原型继承修改时间:2026-07-14 10:45:24