如果让我一句话概括 ES6 的 class,我会说:它是在 ES5 的函数、原型和构造函数这套老机制外面,套了一层更像 Java/C# 的语法糖。这不是贬义,因为“糖”不等于“没用”。它规范了很多原本靠约定才能完成的事:方法定义不可枚举、构造函数必须用 new 调用、继承链条统一、错误信息更明确。反过来想,既然本质还是函数和原型,那就完全可以用 ES5 把 ES6 的类重新“翻译”出来。
这个需求现在听起来有点像考古,但我实际遇到过几种需要手写的情况:老项目的 WebView 内核太旧,Babel 补丁又不能随便升级;内网组件库必须保持零构建依赖;还有不少面试题让你“不用 class 实现一个类”。更重要的是,当你亲手把 class 从语法糖剥回 function 之后,再看 Babel 输出的那些 helper,就不会再觉得像天书了。这篇文章我会从最基础的 constructor 和 prototype 开始,依次还原原型方法、静态方法、访问器、继承、super 调用,最后给一个可以直接抄走的迷你实现。
这篇内容适合两类人:一类是正在维护 ES5 老代码、想复用新语法设计模式的前端;另一类是把 ES6 类写得很熟,但每次看到 prototype、Object.create 就心虚的基础薄弱者。看完之后你会发现,类不是玄学,它就是一个函数加几条原型链规则。
1. 先拆开 class 的糖衣:ES6 类到底比 ES5 多做了哪些事
1.1 从一段最普通的类开始
先看一段谁都能写出来的 ES6 代码:
javascript复制class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
distance() {
return Math.sqrt(this.x * this.x + this.y * this.y);
}
static origin() {
return new Point(0, 0);
}
get description() {
return '(' + this.x + ', ' + this.y + ')';
}
}
这段代码运行时,Point 其实就是一个普通函数。你写类名、写 constructor,本质上是在创建一个将来可以被 new 调用的函数。distance 会挂到 Point.prototype 上,origin 会挂到 Point 函数自身身上,description 则会以访问器属性的形式出现在 Point.prototype 上。
只看到这一层还不够。ES6 语法里藏了很多规范细节:类方法默认不可枚举;类体内所有代码默认处于严格模式;类不能像普通函数那样不带 new 直接调用;extends 不仅连接了实例和父类原型,还会把子类构造函数和父类构造函数连接起来,让静态方法也能被继承。这些细节,用手写 ES5 时如果漏掉,就会在某个角落埋下坑。
1.2 把语法糖翻译成一张语义清单
为了不越改越乱,我习惯先列一张对照表。所谓“用 ES5 实现 ES6 的类”,就是去实现这张表,而不是只实现长得像的代码。
| ES6 类语法 | ES5 里的真实落点 |
|---|---|
constructor(name) {} |
普通构造函数 function Foo(name) {} |
method() {} |
Foo.prototype 上的不可枚举属性 |
static method() {} |
Foo 函数自身上的不可枚举属性 |
get xxx() / set xxx(v) |
Object.defineProperty(Foo.prototype, 'xxx', { get, set }) |
class Child extends Parent {} |
Child.prototype = Object.create(Parent.prototype) 并修正 constructor |
super(...args) |
在子类构造器里调用 Parent.call(this, ...args) |
super.method() |
Parent.prototype.method.call(this, ...args) |
| 不能用 new 之外的方式调用 | 构造函数内部加 this instanceof Foo 守卫 |
| 类内严格模式 | 所有函数体外层或内部写 "use strict" |
这张表基本就是手写工作的全部范围。接下来每一节,我们逐行落实。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 手写最基础的一层:constructor 加 prototype 方法
2.1 最直接的一一对应
还是用前面的 Point。最朴素的 ES5 写法如下:
javascript复制function Point(x, y) {
if (!(this instanceof Point)) {
throw new TypeError("Class constructor Point cannot be invoked without 'new'");
}
this.x = x;
this.y = y;
}
这个函数承担了 ES6 中 constructor 的角色。函数本身有一个 prototype 属性,一开始里面就有 constructor 指向自身。但这还不够,因为
