ES6 class,真的只是语法糖吗

0 阅读7分钟

很多前端第一次学 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 时代,你很难真正继承 ArrayError 这种内置对象。你可以“看起来像在继承”,但经常会在关键行为上露馅。

比如你想搞一个自己的数组子类:

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

qrcode_for_gh_6a9e7f3719d6_344.jpg