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

相关推荐
paopaokaka_luck1 小时前
基于SpringBoot+Uniapp的健身饮食小程序(协同过滤算法、地图组件)
前端·javascript·vue.js·spring boot·后端·小程序·uni-app
患得患失9492 小时前
【前端】【vscode】【.vscode/settings.json】为单个项目配置自动格式化和开发环境
前端·vscode·json
飛_2 小时前
解决VSCode无法加载Json架构问题
java·服务器·前端
YGY Webgis糕手之路5 小时前
OpenLayers 综合案例-轨迹回放
前端·经验分享·笔记·vue·web
90后的晨仔5 小时前
🚨XSS 攻击全解:什么是跨站脚本攻击?前端如何防御?
前端·vue.js
Ares-Wang5 小时前
JavaScript》》JS》 Var、Let、Const 大总结
开发语言·前端·javascript
90后的晨仔5 小时前
Vue 模板语法完全指南:从插值表达式到动态指令,彻底搞懂 Vue 模板语言
前端·vue.js
德育处主任5 小时前
p5.js 正方形square的基础用法
前端·数据可视化·canvas
烛阴5 小时前
Mix - Bilinear Interpolation
前端·webgl
90后的晨仔5 小时前
Vue 3 应用实例详解:从 createApp 到 mount,你真正掌握了吗?
前端·vue.js