JavaScript原型链与继承:ES5原型 vs ES6 Class底层实现对比

作者:闭包旅人

时间:2026-10-30

浏览:461

深入对比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。

JavaScript原型链与继承:ES5原型 vs ES6 Class底层实现对比
图 1:JavaScript 原型链基石与属性委托查找链路图

当访问 user1.name 时,引擎的执行步骤如下:

  1. 检查 user1 实例本身是否含有 name 自身属性(自有属性);若有,直接返回;
  2. 若未找到,通过 [[Prototype]] 进入 User.prototype 查找;
  3. 若仍未找到,继续通过其原型对象的 [[Prototype]] 深入 Object.prototype;
  4. 若最终达到原型链顶端的 null 依然未命中,则返回 undefined。

这种通过原型链接实现多对象间属性与方法委托的链条,就是所谓的“原型链”。

二、ES5 继承演化:从原型链继承到寄生组合式继承

在 ES6 出现之前,社区经历了多种模拟面向对象继承模式的探索,主要包括:

  • 原型链继承:直接令 Child.prototype = new Parent()。缺点是所有 Child 实例会共享来自 Parent 的引用类型属性,且无法在不影响其他实例的情况下向父类构造函数传参。
  • 借用构造函数继承:在 Child 构造函数中执行 Parent.call(this, ...args)。虽解决了引用属性独立性和传参问题,但无法复用父类原型上的方法。
  • 组合继承:同时使用上述两者。缺点是 Parent 构造函数被无意义地调用了两次,一次在创建子类原型时,另一次在子类实例化时,造成内存冗余。

最终成为标准业界典范的是寄生组合式继承。其核心逻辑在于:并不执行 new Parent(),而是使用 Object.create(Parent.prototype) 创建一个纯净的原型对象副本,同时修正 constructor 指向。

JavaScript原型链与继承:ES5原型 vs ES6 Class底层实现对比
图 2:ES5 寄生组合式继承推导与实现四步流程

以下是 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 规范在底层建立了更为严密的双重继承链结构:

  1. 实例方法继承链:SubType.prototype.__proto__ === SuperType.prototype。保证子类实例能沿原型链继承父类原型上的公共方法;
  2. 类静态继承链:SubType.__proto__ === SuperType。保证子类构造函数能够直接访问父类上定义的 static 静态方法和静态属性。
注意对比:在 ES5 的经典寄生组合继承中,我们只处理了 SubType.prototype.__proto__,而 SubType.__proto__ 默认指向 Function.prototype。因此,ES5 默认无法自动继承静态属性,除非额外执行 Object.setPrototypeOf(SubType, SuperType)。
JavaScript原型链与继承:ES5原型 vs ES6 Class底层实现对比
图 3:ES5 单链与 ES6 双重继承链拓扑结构对比

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 引擎的操作流程是:

  1. 首先由子类构造函数内部自动创建一个空对象并绑定为 this;
  2. 在子类构造函数内部通过 SuperType.call(this),把刚刚生成的子类 this 作为上下文传给父类,借调父类逻辑向该对象上补充属性;
  3. 子类再向该 this 追加自有属性;
  4. 最终返回该 this。

2. ES6:先父后子(自顶向下)

而在 ES6 的 class 继承机制中,规范明确要求:

  1. 派生类(Derived Class)自身并不首先分配 this;
  2. 子类构造函数必须首先调用 super(),这会把构建 this 对象的控制权交给基类(Base Class)的构造函数;
  3. 基类构造函数分配好底层实例对象并初始化自身属性后,再将此 this 返回给派生类;
  4. 此时派生类才获得合法的 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 及切片行为。

五、底层行为特征矩阵与关键差异对比

JavaScript原型链与继承:ES5原型 vs ES6 Class底层实现对比
图 4:ES5 与 ES6 底层核心机制维度全景总结

为了让各维度对比更加一目了然,我们归纳整理如下表格:

对比维度 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 绑定时机混乱以及构造器缺乏调用保护等诸多历史暗坑。掌握其底层流转逻辑,不仅能让我们在面对复杂设计模式与框架源码时胸有成竹,也能在排查原型污染与对象遮蔽等疑难问题时迅速定位根本原因。

实用工具推荐

电子包浆

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

使用 ›
图片压缩

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

使用 ›
相关文章