ES6 class与传统JavaScript构造函数有什么区别

作者:代码拾荒

时间:2026-10-30

浏览:241

深入对比ES6 class与传统JavaScript构造函数在变量提升、实例化调用、方法枚举性、继承机制与原型链等核心层面的本质区别与底层实现。

文章目录

很多开发者在初学 ECMAScript 2015(ES6)时,常听到这样一句话:“class 只是基于原型继承的语法糖”。这种说法在宏观抽象层面上确实指出了它没有脱离 JavaScript 原型链的本质,但在微观语义与底层机制上,class 并非简单地将原有构造函数打包变形,两者在提升机制、调用限制、方法枚举性、严格模式以及继承链的构建顺序上有着根本性差异。本文将从具体表现、底层原理与代码行为入手,逐一解析两者的关键区别。

一、核心维度对比总览

为了直观建立全貌认知,我们可以先从声明方式、执行限制以及内部属性等多个核心维度,对 ES6 class 与传统构造函数进行对比:

ES6 class与传统JavaScript构造函数有什么区别
图 1:ES6 class 与传统构造函数在5个关键维度的对比矩阵

二、声明与提升机制(Hoisting)

在传统 JavaScript 中,函数声明会被完整提升到当前作用域顶部,这意味着在声明函数之前就可以直接调用该函数进行实例化:

// 传统构造函数声明:正常执行,不会报错
const p1 = new PersonES5("张三");
console.log(p1.name); // 输出:张三

function PersonES5(name) {
  this.name = name;
}

然而,ES6 class 的表现行为与 let 和 const 完全一致。尽管引擎在编译阶段能够感知到类的存在,但它处于暂时性死区(Temporal Dead Zone,TDZ)中。如果在 class 声明完成之前尝试访问或实例化它,引擎会直接抛出引用错误:

// ES6 class:触发 ReferenceError
const p2 = new PersonES6("李四"); // Uncaught ReferenceError: Cannot access 'PersonES6' before initialization

class PersonES6 {
  constructor(name) {
    this.name = name;
  }
}
设计初衷

类的声明不提升,是为了解决继承场景下的逻辑一致性。如果允许 class 提升,那么当一个类继承另一个由表达式生成的父类时,父类未就绪就会导致子类实例化行为不确定,因此 TC39 委员会将其严格限制在暂时性死区内。

三、实例化与调用限制:[[Construct]] 与 [[Call]]

在 JavaScript 引擎内部,函数对象拥有内部方法 [[Call]](代表作为常规函数调用)和可选的 [[Construct]](代表通过 new 运算符作为构造函数调用)。

传统的构造函数本质上只是一个普通函数,同时具有 [[Call]] 和 [[Construct]]。如果开发者不小心遗漏了 new 关键字,它就会退化为普通函数调用,此时函数体内的 this 会意外绑定到全局对象(非严格模式)或 undefined(严格模式),从而引发全局变量污染:

function UserES5(name) {
  this.name = name;
}

// 错误调用:忘记写 new
const u1 = UserES5("王五");
console.log(u1); // undefined
console.log(window.name); // 非严格模式下意外污染全局: "王五"

为了规避上述问题,ES6 规范规定:通过 class 关键字声明的类,其内部方法只允许被 [[Construct]] 调用。如果在调用时没有使用 new,JavaScript 引擎会立即抛出 TypeError:

class UserES6 {
  constructor(name) {
    this.name = name;
  }
}

// 尝试直接作为函数调用
UserES6("赵六"); 
// 抛出异常:TypeError: Class constructor UserES6 cannot be invoked without 'new'

四、原型方法与属性枚举性

另一个重要的细节是原型上方法的**可枚举性(enumerable)**。在传统构造函数中,直接挂载到 prototype 上的方法默认是可枚举的;而在 ES6 class 内部定义的方法,引擎会自动设置其描述符为 enumerable: false。

// 传统方式
function AnimalES5(name) {
  this.name = name;
}
AnimalES5.prototype.speak = function() {
  return this.name;
};

// ES6 方式
class AnimalES6 {
  constructor(name) {
    this.name = name;
  }
  speak() {
    return this.name;
  }
}

const a1 = new AnimalES5("旺财");
const a2 = new AnimalES6("咪咪");

// 查看原型方法描述符
console.log(Object.getOwnPropertyDescriptor(AnimalES5.prototype, "speak").enumerable); // true
console.log(Object.getOwnPropertyDescriptor(AnimalES6.prototype, "speak").enumerable); // false

这一改动让自定义类与 JavaScript 原生内置对象(如 Array.prototype.push、Object.prototype.toString)的行为完全保持一致。当使用 for...in 循环遍历对象属性时,不会意外把原型方法遍历出来,减少了 hasOwnProperty 的防御性检查。

ES6 class与传统JavaScript构造函数有什么区别
图 2:ES6 class 实例、原型对象与构造函数在内存中的关联关系

五、继承机制的底层反转:this 的创建顺序

这是 ES6 class 与传统构造函数最本质的区别。在继承机制的实现路径上,两者完全颠倒了实例对象 this 的生成时序:

1. 传统继承(以寄生组合式继承为例)

在 ES5 中,继承是通过“子类先创建自己的 this 实例,然后借用父类的构造函数进行增强”来实现的:

function Parent(name) {
  this.name = name;
}

function Child(name, age) {
  // 1. 先由 new Child() 创建子类 this 实例
  // 2. 调用父类构造函数修饰当前 this
  Parent.call(this, name);
  this.age = age;
}

// 3. 手动关联原型链
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

2. ES6 extends 继承机制

在 ES6 继承中,子类没有自己的独立 this 绑定。子类的 this 是通过父类构造函数生成后,再传递给子类的:

  • 在子类构造函数中,必须先调用 super(),否则无法访问 this;
  • 只有执行了 super(),父类的构造函数才会创建出实例对象,并返回给子类;
  • 子类的构造函数再进一步对其进行加工和修饰。
class Parent {
  constructor(name) {
    this.name = name;
  }
}

class Child extends Parent {
  constructor(name, age) {
    // 必须首先执行 super(),否则在此行之前使用 this 会抛出 ReferenceError
    super(name);
    this.age = age; // 此时 this 才有效
  }
}
ES6 class与传统JavaScript构造函数有什么区别
图 3:ES5 寄生组合式继承与 ES6 super() 机制推导步骤对比

这种时序上的颠倒带来了一个极其深远的结果:ES6 class 可以原生继承 JavaScript 内置对象(如 Array、Error、Map 等)。在 ES5 时代,内置对象的内部槽点(Internal Slots,如 [[ArrayData]])只能由内置构造函数自己初始化,普通对象无法通过 Array.apply(this) 挂载,因此无法完整继承 Array.prototype 的长度自动更新行为。而在 ES6 中,由于是父类先实例化,内置槽点得以完整保留。

六、静态属性与原型链的双重继承

在传统构造函数中,给构造函数添加静态方法(如 Parent.create = function() {}),子类是无法直接访问的,必须手动将静态方法通过 Object.assign() 拷贝到子类上。

而 ES6 的 extends 在语言层面实现了**双重原型链**:

  1. 子类的原型对象的 __proto__ 指向父类的原型对象(Child.prototype.__proto__ === Parent.prototype),负责实例方法的继承;
  2. 子类构造函数本身的 __proto__ 指向父类构造函数本身(Child.__proto__ === Parent),直接实现了静态方法与静态属性的原型链式继承。
class SuperClass {
  static identify() {
    return "SuperClass static method";
  }
}

class SubClass extends SuperClass {}

// 静态方法直接在子类上可用
console.log(SubClass.identify()); // 输出:"SuperClass static method"
console.log(SubClass.__proto__ === SuperClass); // true

七、决策与使用建议

在现代 JavaScript 与 TypeScript 开发中,传统构造函数与 ES6 class 应该如何选择?

ES6 class与传统JavaScript构造函数有什么区别
图 4:面向对象方案选型、设计模式与遗留迁移决策路径

八、总结

回顾全文,ES6 class 绝不仅是把 prototype 书写方式换了个皮囊。它在语言运行时层面上强化了诸多规则:

  • 提升机制:从过去的自由提升变为具备暂时性死区(TDZ),保证执行安全;
  • 调用方式:彻底关闭 [[Call]],强制 new 实例化,杜绝全局变量污染;
  • 属性特性:类内方法默认 enumerable: false,保持对象遍历清洁;
  • 继承机制:颠倒了 this 创建顺序,通过 super() 先由父类创建底层实例,打通了原生内置类的继承边界,并天然建立双重原型链。

掌握这些底层原理,能够帮助开发者在面对复杂架构设计、框架源码剖析以及排查继承原型链疑难问题时做到游刃有余。

实用工具推荐

电子包浆

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

使用 ›
图片压缩

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

使用 ›
相关文章