JavaScript原型链与继承:ES5原型 vs ES6 Class底层实现对比
深入对比JavaScript中ES5原型继承与ES6 Class类的底层实现原理,解析原型链查找机制、寄生组合继承、双重原型链拓扑及this初始化顺序等核心技术细节。
很多前端开发者在日常工作中习惯了使用 class Parent extends Child 这种清晰的语法结构,但在浏览器控制台打印类实例时,展开的依然是 [[Prototype]]、prototype 和 constructor 等传统属性。JavaScript 的 class 并没有在引擎底层引入一套类似于 Java 或 C++ 的基于类的面向对象模型,其底层依然完全构建在原型委托机制之上。本文将抛开表层语法糖,从内存拓扑、执行时机以及对象生命周期三个维度,深入对比 ES5 经典继承与 ES6 Class 的底层运作逻辑与关键差异。
一、JavaScript 原型链基石:三角关系与属性查找
要透彻理解面向对象继承,必须先理清构造函数、原型对象(prototype)以及实例三者之间的关系。在 JavaScript 中,除极少数通过 Object.create(null) 创建的无原型对象外,几乎所有对象都持有一个指向其原型对象的内部隐藏插槽 [[Prototype]],在现代浏览器环境中通常可以通过 __proto__ 访问。
当一个函数被声明为普通构造函数时,JavaScript 引擎会自动为其分配一个 prototype 对象,且该对象包含一个指回该函数的 constructor 属性。一旦使用 new 运算符实例化该函数,新生成的实例内部的 [[Prototype]] 便会精准指向该构造函数的 prototype。
当访问 user1.name 时,引擎的执行步骤如下:
- 检查
user1实例本身是否含有name自身属性(自有属性);若有,直接返回; - 若未找到,通过
[[Prototype]]进入User.prototype查找; - 若仍未找到,继续通过其原型对象的
[[Prototype]]深入Object.prototype; - 若最终达到原型链顶端的
null依然未命中,则返回undefined。
这种通过原型链接实现多对象间属性与方法委托的链条,就是所谓的“原型链”。
二、ES5 继承演化:从原型链继承到寄生组合式继承
在 ES6 出现之前,社区经历了多种模拟面向对象继承模式的探索,主要包括:
- 原型链继承:直接令
Child.prototype = new Parent()。缺点是所有Child实例会共享来自Parent的引用类型属性,且无法在不影响其他实例的情况下向父类构造函数传参。 - 借用构造函数继承:在
Child构造函数中执行Parent.call(this, ...args)。虽解决了引用属性独立性和传参问题,但无法复用父类原型上的方法。 - 组合继承:同时使用上述两者。缺点是
Parent构造函数被无意义地调用了两次,一次在创建子类原型时,另一次在子类实例化时,造成内存冗余。
最终成为标准业界典范的是寄生组合式继承。其核心逻辑在于:并不执行 new Parent(),而是使用 Object.create(Parent.prototype) 创建一个纯净的原型对象副本,同时修正 constructor 指向。
以下是 ES5 寄生组合式继承的标准代码实现:
function inheritPrototype(subType, superType) {
// 1. 创建父类原型的一个浅拷贝副本
var prototype = Object.create(superType.prototype);
// 2. 修复 constructor 属性指针
prototype.constructor = subType;
// 3. 将新创建的原型对象赋值给子类的原型
subType.prototype = prototype;
}
// 父类构造函数
function SuperType(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
SuperType.prototype.sayName = function() {
return this.name;
};
// 子类构造函数
function SubType(name, age) {
// 4. 借用构造函数:初始化父类属性
SuperType.call(this, name);
this.age = age;
}
// 继承原型方法
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function() {
return this.age;
};
三、ES6 Class 的底层拓扑:双重继承链与 Babel 还原
ES6 引入了 class 与 extends 关键字。很多初学者误以为 class 仅仅是将上述 ES5 代码原样打包,但事实上,ES6 规范在底层建立了更为严密的双重继承链结构:
- 实例方法继承链:
SubType.prototype.__proto__ === SuperType.prototype。保证子类实例能沿原型链继承父类原型上的公共方法; - 类静态继承链:
SubType.__proto__ === SuperType。保证子类构造函数能够直接访问父类上定义的static静态方法和静态属性。
SubType.prototype.__proto__,而 SubType.__proto__ 默认指向 Function.prototype。因此,ES5 默认无法自动继承静态属性,除非额外执行 Object.setPrototypeOf(SubType, SuperType)。
Babel 如何将 ES6 extends 编译降级?
通过查看 Babel 对 class Child extends Parent 的转译结果,可以清晰看到其底层注入的辅助函数 _inherits:
function _inherits(subClass, superClass) {
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function");
}
// 1. 建立实例原型链 (同 ES5 寄生组合式)
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
writable: true,
configurable: true
}
});
// 2. 建立静态类原型链 (ES6 独有)
if (superClass) {
Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}
}
四、实例化时机与 this 构建顺序:ES5 与 ES6 的核心分歧
除了双重继承链之外,ES5 与 ES6 在执行实例构建时有一个极为深刻的机制差异:this 对象的创建时机完全颠倒。
1. ES5:先子后父(自底向上)
在 ES5 中,执行 new SubType() 时,JavaScript 引擎的操作流程是:
- 首先由子类构造函数内部自动创建一个空对象并绑定为
this; - 在子类构造函数内部通过
SuperType.call(this),把刚刚生成的子类this作为上下文传给父类,借调父类逻辑向该对象上补充属性; - 子类再向该
this追加自有属性; - 最终返回该
this。
2. ES6:先父后子(自顶向下)
而在 ES6 的 class 继承机制中,规范明确要求:
- 派生类(Derived Class)自身并不首先分配
this; - 子类构造函数必须首先调用
super(),这会把构建this对象的控制权交给基类(Base Class)的构造函数; - 基类构造函数分配好底层实例对象并初始化自身属性后,再将此
this返回给派生类; - 此时派生类才获得合法的
this引用,并继续执行自己的构造函数体。
如果在派生类构造函数中,在调用 super() 之前访问 this,JavaScript 引擎会立即抛出 ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor。
为什么这对于内置类继承至关重要?
在 ES5 时代,我们无法真正继承原生类(例如 Array、Error、RegExp):
function MyArray() {
Array.apply(this, arguments); // ES5 借调
}
MyArray.prototype = Object.create(Array.prototype);
MyArray.prototype.constructor = MyArray;
var arr = new MyArray();
arr[0] = 'test';
console.log(arr.length); // 输出 0!原生内部槽 [[ArrayData]] 未能正确绑定
原因在于:原生数组的 length 与内部缓冲区是由 V8 引擎底层 C++ 内核在执行 Array 构造器时绑定的。ES5 是“子类先创建普通空对象 this 再借调 Array”,Array.apply 无法强行将已有的普通空对象改装为底层具备隐藏插槽的原生数组对象。
而 ES6 是“先让 Array 的构造函数生成具备底层槽位的 this,再由 MyArray 承接”,因此 class MyArray extends Array 能够完美继承原生数组的自动变长 length 及切片行为。
五、底层行为特征矩阵与关键差异对比
为了让各维度对比更加一目了然,我们归纳整理如下表格:
| 对比维度 | ES5 原型继承(寄生组合式) | ES6 Class 继承 |
|---|---|---|
| 本质机制 | 函数原型对象手工连接与构造函数借调 | 基于原型委托的语法封装,辅以引擎级内部标记 |
| 调用方式约束 | 可直接执行 Parent()(需手动加 instanceof 防御) |
强制必须通过 new 触发,否则抛出 TypeError |
| 方法可枚举性 | prototype.method 默认 enumerable: true |
类中声明的原型方法默认 enumerable: false |
| this 初始化顺序 | 先在子类生成 this,再借调父类构造函数修饰 |
先在父类构造函数生成 this,再由子类承接加工 |
| 静态属性继承 | 需手动遍历赋值或 Object.setPrototypeOf |
原生具备 SubClass.__proto__ === SuperClass |
| 内置类继承支持 | 无法继承 Array, Error 等原生对象内部槽 |
完美继承原生对象所有内部特性与行为 |
| 变量提升 (Hoisting) | 函数声明存在提升,定义前即可调用 | 存在暂时性死区 (TDZ),定义前访问直接报错 |
六、总结与认知
从 ES5 的原型链手工编排到 ES6 的 class 规范,JavaScript 语言完成了一次工程化与规范化的巨大跃迁。但这并不意味着 ES6 Class 抹去了原型链。在 V8 等现代 JavaScript 引擎内部,对象的属性查找依然严格沿着 [[Prototype]] 链条逐级向上回溯。
ES6 Class 的真正价值,在于它通过引擎层面的底层支持,彻底消除了 ES5 中手工拼装原型链易遗漏静态链、内置对象继承受限、this 绑定时机混乱以及构造器缺乏调用保护等诸多历史暗坑。掌握其底层流转逻辑,不仅能让我们在面对复杂设计模式与框架源码时胸有成竹,也能在排查原型污染与对象遮蔽等疑难问题时迅速定位根本原因。
什么时候该用JS构造器:与工厂模式、字面量的选择对比
2026-11-05 16:18:22
推荐几款轻量级JS照片墙开源库及对比
2026-11-05 15:56:19
JavaScript递归写法有哪些常见陷阱及避免方法
2026-11-05 14:53:38
JS数组浅拷贝与深拷贝怎么选?场景与方法总结
2026-11-05 14:41:02
p5.js 中 setup() 和 draw() 函数的执行机制详解
2026-11-05 12:32:38








