从“祖传”构造函数到 `class`

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 个变体场景思路

  1. Mixin 混入

    class AuditMixin { /*...*/ } + Object.assign(Role.prototype, AuditMixin.prototype) 给不同类无痛加日志。

  2. 装饰器模式

    结合 @frozen @log 装饰器,在 class 声明上直接加横切逻辑,比 ES5 手写代理简洁 80%。

  3. 多继承方案

    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 是不是语法糖",你可以直接把这篇重构记录甩给他:糖不糖无所谓,能少加班才是硬道理。

相关推荐
崔庆才丨静觅5 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60616 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了6 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅6 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅7 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅7 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment7 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅7 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊7 小时前
jwt介绍
前端
爱敲代码的小鱼8 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax