私有字段与esm模块协同构成不可绕过的封装边界:#字段在引擎层禁止访问,esm通过静态导出控制引用可见性,二者结合实现“看不见且碰不到”的强封装。

私有字段(#field)不是语法糖,而是 JavaScript 在语言层面对“谁可以碰这个值”的硬性约束;ESM 模块化则定义了“谁可以拿到这个类/对象的引用”。二者叠加,才真正划出一条可执行、不可绕过的封装边界。
私有字段:运行时不可穿透的数据护城河
ES2022 引入的 # 前缀私有字段,在 JS 引擎层面禁止外部访问——哪怕你拿到实例引用,也完全无法读写、枚举、代理拦截该字段:
-
obj.#id直接报SyntaxError(编译期拦截) -
Reflect.ownKeys(obj)不返回#id -
Object.getOwnPropertyDescriptors(obj)不包含它 - Proxy 的
get/settrap 对私有字段完全失效
这意味着:只要状态藏在 # 后面,外部代码连“尝试绕过”的机会都没有。这不是靠文档约定,而是引擎强制。
ESM 模块:静态可见性的契约容器
ESM 的 import 是静态声明,模块接口在解析阶段就锁定。一个模块导出什么、不导出什么,决定了外部能拿到什么引用:
- 只
export class User,不暴露构造器内部细节(如#cache或#validator) - 不导出辅助函数(如
normalizeEmail()),即使它在同一文件里 - 用
export default封装实例工厂,而非裸类,进一步隐藏实现
这种静态边界让 IDE 能准确提示、打包工具能安全摇树(tree-shake),更重要的是:模块使用者根本看不到私有字段的存在,自然不会产生“我试试能不能 hack”的念头。
二者协同:从“看不见”到“碰不到”
单独用私有字段,若类被随意 export,别人仍可 new 实例、调用公有方法、甚至 monkey patch 原型;单独用 ESM,若导出的类全是 public 字段,数据照样裸奔。只有两者配合,才形成完整防线:
- 模块内定义
class AuthSession { #token; #expiresAt; constructor(t) { this.#token = t; } } - 仅
export { login, logout }函数,不导出AuthSession类本身 - 对外只暴露
login(email, pw)返回一个带有限公有方法(如.isValid())的对象,其内部状态全由#字段承载
此时,调用方既无法 import 到原始类,也无法通过任何方式触达 token 字符串——边界清晰、无歧义、无例外。
警惕常见越界操作
实际开发中容易无意破坏这层边界:
- 在模块顶层导出
export { AuthSession },等于把私有字段的“容器”直接交出去 - 公有方法返回
this或原始对象(如getRawData() { return { token: this.#token } }),造成防御性复制缺失 - 用
export default class并鼓励用户new,实则应导出工厂函数或单例实例 - 在 ESM 中混用 CommonJS 风格的
module.exports,导致私有字段语义失效
边界一旦松动,后续重构、权限收拢、审计追踪都会失去立足点。










