很多前端第一次学 class,都会听到一句话: “ES6 class 只是原型链的语法糖。”
这句话不能说错,但它会把人带偏。
因为如果你继续往下读 Dr. Axel Rauschmayer 在《Exploring ES6》里对 class 的解读,会发现一件事:ES6 class 在“表面上”是语法糖,但在一些关键语义上,ES5 根本没法完整模拟。
也就是说,class 不只是把原来难看的构造函数写法,换成了更像 Java / C# 的样子。它还顺手修掉了 ES5 时代很多“看起来能继承,实际不是真继承”的坑。
先说结论:为什么这件事值得理解?
因为很多人对 class 的误解,会直接影响两个判断:
什么时候该把它当“更清晰的语法”
什么时候要意识到它背后已经不是 ES5 那套旧逻辑了
尤其是你在看 Babel 输出、理解继承、处理 super、或者调试内置对象子类时,这种差别非常关键。
一、class 的确延续了原型链,但它不是简单换皮
先承认一件事:class 的实例方法,最后还是挂在 prototype 上;类本身本质上也还是函数。
比如:
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return `(${this.x}, ${this.y})`;
}
}
这段代码背后,依然和原型链有关。它不是凭空发明了一套全新的对象系统。
但问题在于, “底层还是原型链” ,不等于 “语义上完全等价于 ES5 手写构造函数” 。
这两个不是一回事。
二、第一个明显差异:class 不能像普通函数那样调用
在 ES5 里,构造函数本质上也是函数。
function Person(name) {
this.name = name;
}
你可以写 new Person('Axel'),也可以误写成 Person('Axel')。后者通常会出 bug,但语言层面并不阻止你。
而 class 不一样:
class Person {
constructor(name) {
this.name = name;
}
}
Person('Axel'); // TypeError
类必须配合 new 使用。
这不是风格建议,而是语言语义本身。也就是说,ES6 直接替你把一类低级错误封死了。
三、第二个差异:class 不会提升
ES5 函数声明可以提升:
foo();
function foo() {}
但类声明不行:
new Point(); // ReferenceError
class Point {}
为什么?
因为类可能有 extends,而 extends 后面甚至可以跟一个表达式。这个表达式必须在当前位置求值,不能像函数声明那样粗暴提升。
所以这里你要记住一句话:
函数声明更像“提前可用”,类声明更像 let。
四、第三个差异:class 里的代码天然是严格模式
类体内部默认就是 strict mode。
这意味着很多过去在 ES5 里“勉强能跑”的写法,在 class 里会直接报错。它的态度非常明确:既然你已经在写类这种更结构化的东西,就别再混用那些模糊写法了。
这也是为什么 class 给人的感觉不仅仅是“语法更好看”,而是“语义更收紧了”。
五、第四个差异:方法默认不可枚举
这是一个很容易被忽略,但很有价值的改进。
在 ES5 里,如果你通过对象字面量给原型塞方法,这些方法往往是可枚举的;而在 class 中定义的方法,默认是不可枚举的。
这件事看起来小,实际上很重要。
因为它更符合“方法是行为,不是普通数据字段”的直觉,也避免你在遍历对象时,莫名把原型方法一起扫出来。
很多时候,语言设计最厉害的地方,不是让你多做什么,而是默认帮你少踩坑。
六、真正的大区别:ES6 可以正确继承内置对象,ES5 做不到
这才是 Axel 在这章里最值得反复读的地方。
在 ES5 时代,你很难真正继承 Array、Error 这种内置对象。你可以“看起来像在继承”,但经常会在关键行为上露馅。
比如你想搞一个自己的数组子类:
class MyArray extends Array {}
const arr = new MyArray();
arr.push(1);
arr.push(2);
console.log(arr.length); // 2
这在 ES6 里是成立的。
但在 ES5 里,问题是:Array 不是普通对象。它带有一些特殊内部行为,比如 length 会跟着元素变化自动更新。你没法只靠 Array.call(this) 或 Array.apply(this),就把一个普通对象“变成真正的数组”。
这就是 ES5 无法模拟的点:它能模仿表面结构,模仿不了内置对象的内部语义。
Axel 对这个问题的解释特别关键:
• ES5 的思路:最底层子类先创建实例,再逐层调用父类初始化
• ES6 的思路:实例可以由基类构造器来分配,子类通过 super() 把控制权交上去
这就给了内置对象一个机会:由它自己来创建那个“带特殊能力”的实例。
所以 class MyArray extends Array 才真正能工作。
这已经不是“把 ES5 写法包装一下”了,而是对象模型能力本身增强了。
七、super 也不是 ES5 那种简单的“手动 call 一下”
很多人把 super 理解成:
Parent.prototype.method.call(this)
这样理解只对了一半。
因为 ES6 的 super 不只是语法简写,它背后依赖一个规范层面的概念:方法会记住自己定义时所在的“家”——也就是 [[HomeObject]]。
这意味着:
• super() 可以在子类构造函数里调用父类构造器
• super.xxx() 可以在方法里沿着原型链找父类方法
• 这种查找不是你手动写死父类名,而是和方法所属位置绑定的
这也是为什么带 super 的方法,不能像普通函数那样随便挪来挪去。因为它不是纯文本替换,它背后带着自己的语义上下文。
八、为什么子类构造函数里必须先调 super()
这个规则你一定写过,但很多人并不知道它为什么存在。
class A {}
class B extends A {
constructor() {
super();
this.x = 1;
}
}
在派生类里,this 不是一开始就可用的。你必须先通过 super() 完成父类那一侧的实例初始化,this 才会被建立起来。
如果你先碰 this,就会直接报错。
这条规则背后,本质上还是前面那句话:ES6 的实例分配顺序,和 ES5 已经不是一套逻辑了。
九、所以,class 到底是不是语法糖?
我觉得最准确的说法是:
从“对象仍然基于原型链”这个层面看,它是语法糖;但从“语言对类、继承、super、内置对象子类化新增了哪些语义保障”这个层面看,它又明显不只是语法糖。
如果你只盯着 prototype,你会低估它。
如果你完全把它当成 Java 那套 class system,你又会高估它。
真正更准确的理解应该是:
ES6 class 站在 JavaScript 原型系统之上,给出了一套更严格、更安全、也更完整的“类语义”。
十、你真正该记住什么?
读完 Axel 这一章,我觉得最值得带走的是 4 句话:
class 的底层依然和原型链有关,但语义已经比 ES5 构造函数更强。
super、内置对象继承、实例分配顺序,这些都不是 ES5 能完整伪造出来的。
“只是语法糖”这句话太粗糙,容易让你忽略真正重要的规范差异。
理解 class,不能只看语法长什么样,要看语言在运行时到底保证了什么。
很多知识点,学的时候像语法;但理解深了,你会发现它们其实是在教你语言设计。
ES6 的 class 就是这种东西。
参考原文:Dr. Axel Rauschmayer, Exploring ES6: Classes