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

相关推荐
橙子家2 小时前
浏览器缓存之【身份与会话管理】:Cookies 和 Private state tokens
前端
To_OC3 小时前
LC 49 字母异位词分组:想到哈希表很简单,选对 key 才是精髓
javascript·算法·leetcode
最新资讯动态3 小时前
HDC 2026 | 对话鲸鸿动能:存量时代,品牌如何夺回营销“主动权”?
前端
最新资讯动态3 小时前
游戏出海,从产品走向体系
前端
最新资讯动态3 小时前
20人团队跑出百万DAU、大厂也来抢量:谁在鸿蒙生态跑出加速度
前端
最新资讯动态3 小时前
千万开发者背后,鸿蒙商业化的B面
前端
爱勇宝5 小时前
AI 时代:智商决定起点,情商决定走多远
前端·ai编程
kyriewen5 小时前
用了半年 Claude Code 后,我尝试关掉它写了一周代码——结果比想象中严重
前端·javascript·ai编程
IT_陈寒6 小时前
Vite的静态资源打包让我熬夜到三点,这坑千万别跳
前端·人工智能·后端
山河木马7 小时前
矩阵专题0-webGL中的矩阵
javascript·webgl·计算机图形学