在 JavaScript 的发展历程中,ES6(ECMAScript 2015)引入的 class 关键字无疑是最具里程碑意义的更新之一。它让习惯了 Java、C++ 等传统面向对象语言的开发者感到亲切,仿佛 JavaScript 终于拥有了“真正”的类。然而,很多资深开发者会告诉你:“ES6 的 class 只是语法糖。” 这句话既对也不对。说它对,是因为底层依然是基于原型链的;说它不对,是因为 ES6 的 class 在行为约束、继承机制和安全性上,与 ES5 的构造函数有着本质的区别。理解这些差异,不仅是应对面试的必备技能,更是编写高质量、可维护现代 JavaScript 代码的基础。

本质与表象:语法糖下的原型继承
从本质上讲,ES6 的 class 并没有引入一种全新的面向对象底层机制,它依然是建立在 JavaScript 原有的原型继承之上的。如果你使用 Babel 将 ES6 代码编译为 ES5,你会发现 class 最终被转译成了构造函数和原型方法的组合。
在 ES5 中,我们通常通过构造函数配合原型链来模拟“类”:
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
return 'Hi, ' + this.name;
};
而在 ES6 中,代码变得更加清晰和语义化:
class Person {
constructor(name) {
this.name = name;
}
sayHello() {
return 'Hi, ' + this.name;
}
}
虽然写法不同,但 typeof Person 在两种情况下都返回 'function'。这说明 ES6 的类本质上仍然是一个函数。但是,这种“糖衣”并非仅仅是为了好看,它封装了许多复杂的底层细节,使得代码行为更加严谨。
核心区别深度解析
ES6 的 class 并非简单的语法重写,它在多个维度上对 ES5 的构造函数进行了“升级”和“约束”。
1. 调用方式的强制约束
在 ES5 中,构造函数本质上只是一个普通函数。虽然约定俗成我们用 new 来调用它,但如果你忘记了 new 直接执行 Person('zhangsan'),代码依然能运行。但这会导致 this 指向全局对象(浏览器中是 window),从而污染全局作用域,这是极其隐蔽且危险的 Bug。
ES6 的 class 则强制要求必须使用 new 命令调用。如果你尝试直接调用 Person('zhangsan'),JavaScript 引擎会直接抛出一个 TypeError,提示类构造函数不能在没有 new 的情况下被调用。这种强制性的检查机制,从语言层面杜绝了忘记 new 导致的低级错误。
2. 变量提升的差异
在 ES5 中,函数声明会被提升。这意味着你可以在函数定义之前就调用它。
var p = new Person('Tom'); // 可以执行
function Person(name) { this.name = name; }
但在 ES6 中,class 声明不存在变量提升。类必须先声明后使用,否则在运行时会抛出 ReferenceError。这种机制类似于 let 和 const,它强制要求代码具有更好的组织顺序,避免了因变量提升带来的逻辑混乱。这也意味着在继承时,子类必须定义在父类之后,保证了继承关系的清晰性。
3. 原型方法的可枚举性
这是一个容易被忽视但影响深远的区别。在 ES5 中,通过 Person.prototype.sayHello = function(){} 定义的方法,是可枚举的。这意味着当你使用 for...in 循环遍历实例时,这些方法会被打印出来,这通常不是我们想要的。
而在 ES6 的 class 中,定义在类体中的方法(如 sayHello())默认是不可枚举的。当你遍历实例属性时,只会得到实例自身的属性(如 name),而不会得到原型上的方法。这更符合传统面向对象语言中“方法不属于对象自有属性”的认知,也让对象序列化(如 JSON.stringify)和属性遍历更加干净。
4. 继承机制与 this 的绑定
ES6 的继承机制(extends)比 ES5 更加严格和强大。
在 ES5 中,继承通常是“先有子类 this,再借用父类构造函数”。而在 ES6 中,子类的 this 必须先通过调用 super() 来初始化(即先有父类的实例),然后才能在子类中使用。如果子类构造函数中没有调用 super() 就直接使用 this,会直接报错。
这种机制使得 ES6 可以更容易地继承原生数据结构(如 Array、Map),这是 ES5 很难做到的。此外,ES6 还提供了 super 关键字,可以方便地访问父类的构造函数和方法,大大简化了继承代码的编写。
5. 严格模式与私有成员
ES6 的 class 内部默认运行在严格模式下。这意味着你不需要在文件顶部写 'use strict',类内部的代码就会自动受到严格模式的约束(如禁止未声明的变量、禁止重复参数名等)。
此外,ES6(及后续版本)引入了私有属性的语法(使用 # 前缀,如 #count)。这提供了真正的封装能力,外部无法直接访问或修改这些属性,而 ES5 只能通过闭包或 WeakMap 来模拟私有属性,不仅繁琐且不够直观。
总结对比表
为了让你更直观地理解两者的差异,我整理了以下对比表:
| 特性 | ES5 构造函数 | ES6 Class |
|---|---|---|
| 本质 | 普通函数 | 特殊的函数(语法糖) |
| 调用方式 | 可直接调用,也可 new 调用 |
必须使用 new 调用 |
| 变量提升 | 函数声明会被提升 | 不存在变量提升 |
| 原型方法 | 可枚举 | 不可枚举 |
| 继承机制 | 借用构造函数,先有子类 this | 使用 extends,先调用 super() |
| 运行模式 | 默认非严格模式 | 默认严格模式 |
| 私有属性 | 需通过闭包模拟 | 支持 # 私有字段 |
结语
ES6 的 class 并不是为了取代原型,而是为了让原型继承变得更加优雅、安全和易用。它通过强制性的规则(如必须 new、必须 super)和自动化的处理(如严格模式、不可枚举),帮助开发者规避了 ES5 时代的许多“坑”。在现代前端开发中,除非是为了兼容极古老的浏览器,否则应始终优先使用 ES6 的 class 语法来构建面向对象的应用。