1. 问题场景:老代码的"祖传味道"
老系统里有一个 Role
模块,负责给不同岗位分配权限。最早的实现长这样:
js
// role.es5.js
function Role(name) {
this.name = name; // 🔍 公有属性
this._privileges = []; // 🔍 约定"私有",但完全靠自觉
}
Role.prototype.addPrivilege = function (p) {
this._privileges.push(p);
};
Role.prototype.hasPrivilege = function (p) {
return this._privileges.includes(p);
};
上线 8 年,需求不断膨胀:
- 需要静态方法
Role.createAdmin()
快速创建管理员 - 需要真正的私有字段,防止外部篡改
_privileges
- 需要继承出
SuperRole
并混入审计日志
用 ES5 写下去,原型链层层嵌套,代码越来越像"意大利面"。
2. 解决方案:一把梭升级到 class
我们决定用 ES6 class
重构,目标:语义化 + 真私有 + 静态方法 + 继承链。
js
// role.es6.js
const Privilege = Symbol('priv'); // 🔍 唯一 key,避免命名冲突
class Role {
#privileges = []; // 🔍 真私有字段,外部无法访问
constructor(name) {
this.name = name;
}
addPrivilege(p) {
this.#privileges.push(p);
}
hasPrivilege(p) {
return this.#privileges.includes(p);
}
static createAdmin() {
const admin = new Role('admin');
admin.addPrivilege('all');
return admin;
}
}
class SuperRole extends Role {
constructor(name, audit = true) {
super(name);
this.audit = audit;
}
addPrivilege(p) {
super.addPrivilege(p);
if (this.audit) console.log(`[audit] add ${p}`);
}
}
逐行解析:
#privileges
是 TC39 提案的「私有字段」,浏览器直接拦截外部访问,比 Symbol 更安全。static createAdmin()
挂在类本身,而不是实例,调用时Role.createAdmin()
。extends
+super()
让继承像 Java/C# 一样直观,不再手写Object.create()
。
3. 原理剖析:语法糖到底糖在哪?
3.1 表面用法对比
特性 | ES5 写法 | ES6 class 写法 |
---|---|---|
构造函数 | function Foo(){} |
class Foo { constructor() } |
实例方法 | Foo.prototype.bar = fn |
bar() {} |
静态方法 | Foo.baz = fn |
static baz() {} |
私有属性 | 约定 _prop 或闭包 |
#prop |
继承 | Child.prototype = new Parent() |
class Child extends Parent |
3.2 底层机制
我们用 DevTools 的 Memory 面板拍了一张快照,发现:
- ES5 的"类"本质上是构造函数对象 + 原型链对象 两张表。
- ES6 的
class
在引擎内部仍然是同样的两张表,但在声明阶段就被标记为不可枚举、不可随意修改,从而避免开发者手滑把原型链打断。
lua
new Role('admin')
├─ 引擎检查:Role [[IsClassConstructor]] = true
├─ 分配实例对象
├─ 执行 Role#constructor
└─ 返回实例(原型固定指向 Role.prototype)
3.3 设计哲学
ES5 的"构造函数"是命令式 :你自己管原型、管继承、管私有。
ES6 的 class
是声明式 :告诉引擎"我要一个类",底层帮你把原型链、静态方法、私有字段全部摆好。
一句话总结:class
不是新功能,而是把最佳实践固化成语法。
4. 应用扩展:迁移中的 3 个高频坑
坑 1:原型链属性被意外枚举
老代码里有人用 for...in
遍历实例,结果把原型方法也扫出来。
迁移后,在 class
中定义的方法默认 enumerable: false
,老逻辑直接失效。
解决:加一行防御代码:
js
if (!instance.hasOwnProperty(key)) continue;
坑 2:静态属性丢失
ES5 时代我们习惯 Role.version = '1.0';
class
声明体里只能放方法,不能直接写 static version = '1.0';
(Stage 3 提案已支持,但老构建工具不认)。
解决:类外补一句 Role.version = '1.0';
坑 3:私有字段调试困难
#privileges
在控制台直接 role.#privileges
会抛语法错误。
解决:用 DevTools 的 Scope 面板查看闭包变量,或者临时加 get debug() { return this.#privileges; }
。
5. 举一反三:3 个变体场景思路
-
Mixin 混入
用
class AuditMixin { /*...*/ }
+Object.assign(Role.prototype, AuditMixin.prototype)
给不同类无痛加日志。 -
装饰器模式
结合
@frozen
@log
装饰器,在class
声明上直接加横切逻辑,比 ES5 手写代理简洁 80%。 -
多继承方案
用
class A extends mix(B, C) {}
,其中mix
是一个函数,内部用Object.setPrototypeOf
把 B、C 的方法扁平化到 A 的原型链。
6. 可复用配置片段
如果你用 Babel + Webpack 老项目,记得在 .babelrc
加:
json
{
"plugins": [
["@babel/plugin-proposal-private-methods", { "loose": true }],
["@babel/plugin-proposal-class-properties", { "loose": true }]
]
}
环境适配:Node ≥ 14 原生支持
#
私有字段,浏览器侧需 Chrome ≥ 74。
小结
把 ES5 的"构造函数"升级到 class
,最大的收益不是语法变短,而是把容易出错的底层操作收拢到引擎层面 。
下次再有人争论"class
是不是语法糖",你可以直接把这篇重构记录甩给他:糖不糖无所谓,能少加班才是硬道理。