JS原型链与继承机制详解:prototype、__proto__和constructor的关系

作者:代码拾荒

时间:2026-11-05

浏览:345

深入解析 JavaScript 原型链与对象继承机制,系统剖析 prototype、__proto__ 与 constructor 的本质、三者三角拓扑关系、属性沿链查找算法、顶层对象函数全景以及从 ES5 寄生组合继承到 ES6 class 的工业级演进方案。

文章目录
核心概念:原型与继承 语言标准:ECMAScript 阅读时长:约 15 分钟 难度层级:进阶底层原理
JS原型链与继承机制详解:prototype、__proto__和constructor的关系
图 0:JavaScript 原型拓扑架构微缩概念图
JavaScript 并没有基于类的传统静态内存模型,而是基于对象委托(Object Delegation)的原型机制构建对象体系。理解这一机制的核心在于理清三者的权责边界:函数专属的显式原型 prototype、对象实例的隐式原型指针 __proto__(规范内部插槽为 [[Prototype]]),以及定义在原型上指向构造函数本身的 constructor 属性。本文从微观三角关系切入,结合属性沿链查找算法、顶层全景拓扑图,以及从 ES5 寄生组合继承到 ES6 class 的底层演进,完整拆解原型链与继承机制的运作细节。

一、核心三者关系透视:prototype、__proto__ 与 constructor 的本质

在 ECMAScript 规范中,对象之间共享行为并非通过将父级方法拷贝到子级,而是通过指针引用串联起一张网状的原型拓扑。很多开发者感到混淆,根源在于将“函数自身的属性”与“实例对象的属性”混为一谈。

1.1 概念定义与权责划分

  • prototype(显式原型属性):仅存在于常规函数对象(具备 [[Construct]] 内部方法的构造函数)上。当普通函数被声明时,JS 引擎会自动为它创建一个 prototype 属性,指向一个空的原型对象。该对象的作用是:集中存放所有由该构造函数创建的实例所共享的属性和方法。箭头函数与通过某些原生手段构建的对象没有此属性。
  • [[Prototype]] 与 __proto__(隐式原型引用):所有对象(包括函数对象、普通字面量对象以及数组等)都具备一个隐藏的内部插槽(Internal Slot)[[Prototype]]。在历史实现中,主流浏览器通过 __proto__ 访问器属性将其暴露给开发者。它始终指向该对象的原型(即创建该实例的构造函数的 prototype)。现代规范已标准化 Object.getPrototypeOf() 和 Object.setPrototypeOf(),并将 __proto__ 列为附录 B 兼容特性。
  • constructor(构造器回溯属性):默认情况下,每个原型对象(Foo.prototype)都会自动生成一个 constructor 属性,反向指向该原型所属的构造函数 Foo。实例对象自身通常并不拥有 constructor 属性,当我们在实例上访问 instance.constructor 时,实际上是通过隐式原型链回溯到了 Foo.prototype.constructor。

1.2 三角闭环代码验证

通过一个最基础的构造函数与实例化代码,验证三者之间的物理指向:

JavaScript 代码示例:三者指向验证
// 1. 声明构造函数
function Person(name) {
  this.name = name;
}

// 2. 在显式原型上挂载公共方法
Person.prototype.greet = function() {
  return `Hello, I'm ${this.name}`;
};

// 3. 实例化对象
const alice = new Person("Alice");

// 4. 验证核心三者关系
console.log(alice.__proto__ === Person.prototype); // true:实例隐式原型指向构造函数显式原型
console.log(Person.prototype.constructor === Person); // true:原型对象的构造器指针指向构造函数
console.log(alice.constructor === Person); // true:实例沿原型链读取到 Person.prototype.constructor
console.log(Object.hasOwn(alice, "constructor")); // false:验证实例自身无 constructor 属性
console.log(Object.hasOwn(Person.prototype, "constructor")); // true:constructor 位于原型对象上

上述代码呈现出经典的“三角关系”:Person 通过 new 生产出 alice;Person.prototype 提供共享方法池;alice.__proto__ 向上锚定该原型;而 Person.prototype.constructor 将控制权交还给构造函数。如下拓扑图直观展示了该结构的内存分布:

JS原型链与继承机制详解:prototype、__proto__和constructor的关系
图 1:构造函数、原型对象与实例对象的三角拓扑关系

二、原型链查找机制:属性解析、遮蔽与终止边界

在理解了单一实例与原型的绑定之后,原型链(Prototype Chain)的概念便顺理成章:原型对象本身也是一个对象,因此原型对象也有它自己的 [[Prototype]]。这种通过内部指针层层递进的单向链接结构,就构成了原型链。

2.1 属性读取算法的推导步骤

当执行表达式 obj.property 时,JavaScript 引擎并非直接报错,而是严格执行以下推导步骤:

  1. 检查自有属性(Own Properties):引擎首先检查对象 obj 自身的属性表中是否存在 property。若存在且可访问,则直接返回其值,检索立即终止。
  2. 读取隐式指针 [[Prototype]]:若自身无此属性,引擎读取 obj 内部的 [[Prototype]] 插槽(即 Object.getPrototypeOf(obj))。
  3. 原型终点校验:如果获取到的原型对象为 null,表明已经跨越了整条原型链的最顶端仍未找到匹配项,引擎正式判定属性不存在,返回 undefined。
  4. 逐层回溯迭代:若原型对象非 null,则将当前游标指针切换为该原型对象,返回步骤 1 重新执行自有属性判定,直到找到属性或命中 null 为止。
JS原型链与继承机制详解:prototype、__proto__和constructor的关系
图 2:JavaScript 原型链属性检索与决策步骤流程图

2.2 属性遮蔽(Property Shadowing)与读写差异

原型链的查找算法仅适用于读取(RHS 查找)操作。在进行写入(LHS 赋值)时,行为完全不同:

JavaScript 代码示例:属性遮蔽与原型不可变性
function Car(brand) {
  this.brand = brand;
}
Car.prototype.wheels = 4;

const myCar = new Car("Tesla");

// 读操作:自身无 wheels,沿原型链读取到 Car.prototype.wheels
console.log(myCar.wheels); // 示例输出: 4

// 写操作:直接在 myCar 实例自身创建 wheels 属性,并不修改 Car.prototype
myCar.wheels = 6; 

// 再次读取:命中自身自有属性,发生“属性遮蔽”
console.log(myCar.wheels); // 示例输出: 6
console.log(Car.prototype.wheels); // 示例输出: 4 (原型不受任何污染)

// 删除自有属性后,原型属性再次显现
delete myCar.wheels;
console.log(myCar.wheels); // 示例输出: 4
注意事项:如果原型链上层的同名属性被标记为只读(writable: false),或者定义了只有 getter 没有 setter 的访问器属性,在严格模式('use strict')下直接对实例赋值将抛出 TypeError: Cannot assign to read only property,在非严格模式下则会静默失败,此时不会在实例上创建遮蔽属性。

三、原型顶层结构全景:函数与对象的双重身份

初学者在探索 JavaScript 原型时,最容易在 Function 与 Object 的互引中迷失。理解顶层全景的关键在于确立两个基本事实:在 JavaScript 中,函数也是对象(一等公民);所有构造函数都是通过 Function 构造出来的实例。

3.1 拆解“万物皆对象”与“函数是构建者”

通过控制台直接打印各顶层核心实体的原型关系,可以归纳出五条不可动摇的黄金定律:

  • 定律 1:原型链的终点是 null。
    Object.prototype.__proto__ === null。作为万物原型的 Object.prototype 没有上一级原型。
  • 定律 2:所有常规原型对象直接继承自 Object.prototype。
    例如 Person.prototype.__proto__ === Object.prototype,因为 Person.prototype 本身只是一个通过 Object 创建的普通对象。
  • 定律 3:所有函数(包括内置构造函数)的隐式原型都指向 Function.prototype。
    Person.__proto__ === Function.prototype,同时作为函数的 Object 也服从该规则:Object.__proto__ === Function.prototype。
  • 定律 4:Function 是自身构造函数的实例(自指闭环)。
    Function.__proto__ === Function.prototype,并且 Function.prototype.constructor === Function。
  • 定律 5:Function.prototype 也是一个对象,因此最终归流于 Object.prototype。
    Function.prototype.__proto__ === Object.prototype。
JavaScript 代码示例:顶层全景拓扑验证
// 验证构造函数的隐式原型
console.log(Object.__proto__ === Function.prototype); // true
console.log(Function.__proto__ === Function.prototype); // true
console.log(Array.__proto__ === Function.prototype); // true

// 验证原型对象的顶层归宿
console.log(Function.prototype.__proto__ === Object.prototype); // true
console.log(Object.prototype.__proto__ === null); // true (链条终点)

// 验证 instanceof 的底层逻辑
console.log(Object instanceof Function); // true:Object.__proto__ -> Function.prototype
console.log(Function instanceof Object); // true:Function.__proto__ -> Function.prototype.__proto__ -> Object.prototype
JS原型链与继承机制详解:prototype、__proto__和constructor的关系
图 3:JavaScript 顶层 Function 与 Object 双闭环原型全景拓扑架构图

四、继承机制演进:从原型链切片到 ES6 Class

理解了原型链的传递后,JavaScript 继承机制的本质即可概括为:如何让子类构造函数的实例,既能拥有父类构造函数中初始化的自有属性,又能通过原型链透明访问父类原型上定义的公共方法。

4.1 传统方式的缺陷分析

在 ECMAScript 发展早期,社区尝试过多种继承方案,均存在明显的结构缺陷:

继承方案 典型实现方式 核心缺陷与边界限制
纯原型链继承 Child.prototype = new Parent() 父类构造函数中的引用类型属性会被所有子类实例全局共享;子类实例化时无法向父类构造函数传参。
借用构造函数继承 在 Child 内部执行 Parent.call(this, args) 仅实现了实例属性的拷贝,完全没有建立原型链连接,子类实例无法复用父类原型上的方法。
伪经典组合继承 结合 call 与 Child.prototype = new Parent() 调用了两次父构造函数,导致子类的原型对象(Child.prototype)中残留了一份无用的父类实例属性,造成内存冗余。

4.2 工业级标准:寄生组合式继承(Parasitic Combination)

为了解决组合继承中两次调用构造函数并污染原型的问题,Douglas Crockford 等人提出了“寄生组合式继承”。其核心是:不要通过 new Parent() 来获取原型,而是利用一个干净的空对象切片(Object.create)直接连接原型链。

JavaScript 代码示例:寄生组合式继承标准实现
// 1. 父类构造函数
function SuperType(name) {
  this.name = name;
  this.colors = ["red", "blue"];
}

SuperType.prototype.sayName = function() {
  return this.name;
};

// 2. 子类构造函数
function SubType(name, age) {
  // 核心步骤 1:借用构造函数继承实例属性(仅调用一次父构造函数)
  SuperType.call(this, name);
  this.age = age;
}

// 核心步骤 2:通过 Object.create 创建一个干净的原型副本,切断与父类实例属性的耦合
SubType.prototype = Object.create(SuperType.prototype);

// 核心步骤 3:修复因为原型被重写而丢失的 constructor 指针
SubType.prototype.constructor = SubType;

// 4. 在子类原型上添加自身特有方法
SubType.prototype.sayAge = function() {
  return this.age;
};

// 验证继承效果
const child = new SubType("Bob", 12);
console.log(child.sayName()); // 示例输出: "Bob" (成功继承父类原型方法)
console.log(child.sayAge());  // 示例输出: 12 (子类特有方法)
console.log(child.constructor === SubType); // 示例输出: true (构造器正确归位)
console.log(child instanceof SuperType);    // 示例输出: true
JS原型链与继承机制详解:prototype、__proto__和constructor的关系
图 4:寄生组合式继承实现步骤与 ES6 Class 原型映射对比

4.3 ES6 Class 的底层本质:语法糖与双原型链

ES6 引入的 class 与 extends 关键字在语义上贴近传统面向对象,但底层依然严格基于原型链运作。相比 ES5 寄生组合式继承,ES6 实现了更完备的双重原型链继承:

  1. 实例方法链:SubType.prototype.__proto__ === SuperType.prototype。保证子类实例可以向上调用父类的方法与 super.method()。
  2. 静态属性/方法链:SubType.__proto__ === SuperType。这使得定义在父类上的静态方法(static foo()),子类可以直接通过 SubType.foo() 调用。在 ES5 手写继承中,如果不手动添加 Object.setPrototypeOf(SubType, SuperType),静态方法是无法被继承的。

五、开发实践与避坑指南:API 选择与核心规则总结

在日常生产代码中,原型机制虽然被 class 良好封装,但涉及底层库设计、插件架构与性能调优时,仍需遵循现代规范并规避经典陷阱。

5.1 现代 API 推荐与弃用清单

  • 避免直接读写 obj.__proto__:在现代 V8 等引擎中,修改已有对象的 [[Prototype]] 会彻底破坏引擎的隐藏类(Hidden Class / Shape)优化与内联缓存(Inline Cache),导致该对象及其所有后续访问降级为低效的哈希表字典查找。
  • 规范推荐 Object.getPrototypeOf(obj):安全高效读取对象的原型指针。
  • 规范推荐 Object.create(proto):在对象创建时一次性确立其原型,避免创建后修改带来的性能惩罚。若需要创建无任何原型负担的纯字典,可使用 Object.create(null)。
  • 规范推荐 Object.hasOwn(obj, prop):ES2022 引入的标准方法,替代过去容易在无原型对象或重写属性时报错的 obj.hasOwnProperty(prop)。

5.2 原型重写时务必修正 constructor

当批量向原型对象添加方法时,开发者常习惯直接赋值一个新对象字面量:

错误与正确实践对照
function Widget() {}

// 危险做法:直接重写整个 prototype 对象
Widget.prototype = {
  render() {},
  destroy() {}
};
// 此时新对象的 constructor 丢失,变成了 Object!
console.log(new Widget().constructor === Widget); // false
console.log(new Widget().constructor === Object); // true

// 正确做法:重写时显式补齐 constructor 并设为不可枚举
Object.defineProperty(Widget.prototype, "constructor", {
  value: Widget,
  enumerable: false, // 遵循原生原型对象的默认不可枚举特征
  writable: true,
  configurable: true
});
console.log(new Widget().constructor === Widget); // true
JS原型链与继承机制详解:prototype、__proto__和constructor的关系
图 5:JavaScript 原型链与继承核心机制决策总结图

六、总结

JavaScript 的原型链与继承体系可以用以下三句话精炼概括:

1. 归属明确:prototype 是构造函数对外的展示窗口,存放共享原型;__proto__ 是实例对内的回溯索链,指向所属原型;constructor 是原型对象反哺构造函数的身份凭证。

2. 链条单向:属性查找沿着 __proto__ 单向递进,终于 null;写入属性默认就地遮蔽,不污染上游。

3. 现代继承:ES6 class 是寄生组合式继承的标准语法糖,并补齐了静态属性构造函数原型链。在现代工程中,应遵循“创建即定型”原则,拥抱 class 与 Object.create(),杜绝动态篡改 __proto__。

实用工具推荐

电子包浆

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

使用 ›
图片压缩

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

使用 ›
相关文章