无需登录 数据私有 本地保存

ES 私有字段演示 - # 语法与封装

14
0
0
0

常见问题解答

以下整理了开发者在学习和使用 JavaScript ES 私有字段时最常遇到的问题,每个问题都提供了详细的解答。

这是一个非常重要的区别,很多开发者在从 TypeScript 转向使用 ES 原生私有字段时都会遇到这个困惑。

TypeScript 的 private 是编译时约束。当 TypeScript 代码被编译为 JavaScript 时,private 关键字会被完全移除,私有成员变成普通的公有属性。这意味着在运行时,任何人都可以通过点号或方括号语法访问这些"私有"成员。TypeScript 的 private 更多是一种设计意图的表达,而非强制执行的访问控制。

ES # 私有字段 是运行时强制执行的。使用 # 前缀声明的字段在 JavaScript 引擎层面就被保护,类外部的代码无法访问。即使将代码编译为低版本 JavaScript,# 私有字段的保护机制仍然存在(虽然可能通过其他方式模拟实现)。

在实际项目中,如果您需要真正的封装和数据保护,应该使用 ES 原生私有字段。如果您只需要开发时的类型检查和代码提示,TypeScript private 就足够了。许多项目会同时使用两者:用 TypeScript private 做类型检查,用 ES # 字段做运行时保护。

不可以。私有字段在子类中完全不可访问,这是 ES2022 私有字段的一个重要设计决策。

class Parent {
  #secret = "hidden";
}

class Child extends Parent {
  check() {
    return this.#secret;  // SyntaxError!
  }
}

即使是同一个实例对象,子类也无法访问父类的私有字段。这意味着私有字段是真正的"类级别"私有,而不是"实例级别"私有。如果您需要在子类中访问父类的某些状态,应该使用 protected 模式的约定(如使用下划线前缀 _field)或通过公有方法暴露。

这个设计决策背后的考虑是:私有字段的内部实现是类的实现细节,子类不应该依赖这些细节。如果允许子类访问父类的私有字段,那么父类在重构内部实现时就需要考虑子类的影响,这违背了封装的原则。

#field in object 是 ES2022 引入的新语法,用于检查一个对象是否拥有指定的私有字段。它的主要用途包括:

  • 品牌检测:验证一个对象是否是由特定类创建的,类似于 instanceof 但更可靠。
  • 安全检查:在不触发异常的情况下判断对象是否具有某个私有字段。
  • 鸭子类型:在多态场景中,判断对象是否实现了特定的私有接口。

限制条件:

  • 只能在定义该私有字段的类内部使用,在其他类或全局作用域中使用会导致语法错误。
  • 不能用于检查公有字段,只能用于检查以 # 开头的私有字段。
  • 对于未初始化的私有字段,in 操作符仍然返回 true,因为字段已经"存在"只是未赋值。

私有字段对 JSON 序列化有显著影响:JSON.stringify() 不会序列化私有字段。当您对一个包含私有字段的对象调用 JSON.stringify() 时,输出的 JSON 字符串中只包含公有属性。

class Config {
  #apiKey;
  #secretToken;
  baseUrl;

  constructor(key, token, url) {
    this.#apiKey = key;
    this.#secretToken = token;
    this.baseUrl = url;
  }
}

const config = new Config("key123", "token456", "https://api.example.com");
console.log(JSON.stringify(config));
// 输出: {"baseUrl":"https://api.example.com"}
// #apiKey 和 #secretToken 被排除在外

同样,Object.keys()Object.values()Object.entries() 等方法也不会返回私有字段。这在需要持久化数据时是一个常见的陷阱。解决方案包括:

  • 实现自定义的 toJSON() 方法来显式返回需要序列化的字段
  • 创建一个公有的数据传输对象(DTO)来承载序列化数据
  • 使用 Object.getOwnPropertyNames() 也无法获取私有字段

ES2022 私有字段在所有主流现代浏览器中都已获得支持。具体版本要求如下:

浏览器 最低支持版本 发布时间
Google Chrome91+2021年5月
Mozilla Firefox90+2021年7月
Apple Safari15+2021年9月
Microsoft Edge91+2021年5月
Node.js16.11+2021年10月

如果您需要支持旧版浏览器,可以使用 Babel 等转译工具将私有字段语法转换为 ES5 兼容的代码。Babel 会使用 WeakMap 或其他机制来模拟私有字段的行为。

在使用本工具时,请确保您的浏览器版本满足上述要求。如果遇到语法错误,请检查浏览器版本是否过旧。

# 语法和 WeakMap 都可以实现私有字段的效果,但它们各有优劣:

# 语法的优点:

  • 语法简洁直观,易于理解和维护
  • 语言原生支持,性能更优
  • 在 IDE 中可以获得更好的自动补全和错误提示
  • 调试时更容易识别私有字段
  • 提供品牌检测能力(#field in obj

# 语法的缺点:

  • 需要较新的运行环境(现代浏览器或 Node.js 16.11+)
  • 无法在子类中访问,继承性较差
  • 不能动态添加私有字段

WeakMap 的优点:

  • 兼容性好,可以在任何 ES5 环境中使用
  • 私有数据的生命周期与对象绑定,不会造成内存泄漏
  • 可以在子类中访问父类的"私有"数据

WeakMap 的缺点:

  • 语法繁琐,需要维护额外的数据结构
  • 运行时性能略低
  • IDE 无法识别 WeakMap 中的私有字段
  • 调试时不易查看私有数据

在现代项目中,建议优先使用 # 语法。只有在需要兼容旧环境或需要子类访问的特殊场景中,才考虑使用 WeakMap。

不可以。ES2022 的私有字段必须在类定义时静态声明。不能通过赋值操作动态创建私有字段。例如,以下代码会导致语法错误:

class Example {
  #existingField = 1;
}

const obj = new Example();
obj.#dynamicField = 2;  // SyntaxError: Private field '#dynamicField' must be declared

这个限制的原因是私有字段需要在类定义时就被 JavaScript 引擎识别,以便进行静态分析和访问控制。动态添加字段会破坏这一机制。

替代方案:

  • 在构造函数中初始化所有可能的字段:如果字段名可以在编译时确定,直接在类定义中声明。
  • 使用 Map 或普通对象:将动态字段存储在一个公有的 Map 或对象中,通过键名来区分不同的"私有"数据。
  • 使用 WeakMap:WeakMap 可以在类定义时创建,并在运行时动态添加键值对,模拟动态私有字段的效果。
  • 重新设计数据结构:考虑是否真的需要动态字段,也许可以将动态数据组织为嵌套对象。

由于私有字段不会被 JSON.stringify() 序列化,您需要在类中实现自定义的序列化和反序列化逻辑。以下是推荐的实现方式:

class User {
  #password;
  name;

  constructor(name, password) {
    this.name = name;
    this.#password = this.#hash(password);
  }

  #hash(str) {
    return btoa(str);
  }

  // 自定义序列化
  toJSON() {
    return {
      name: this.name,
      password: this.#password
    };
  }

  // 静态工厂方法用于反序列化
  static fromJSON(json) {
    const data = typeof json === 'string' ? JSON.parse(json) : json;
    const user = Object.create(User.prototype);
    user.name = data.name;
    user.#password = data.password;
    return user;
  }
}

这种模式的关键在于 toJSON() 方法和静态工厂方法 fromJSON()toJSON() 会在 JSON.stringify() 调用时自动执行,fromJSON() 则提供了一个安全的方式来从 JSON 数据重建包含私有字段的对象。这种模式在实际项目中非常实用,尤其是在需要将包含私有字段的对象持久化到数据库或通过网络传输时。

在现代 JavaScript 引擎中,私有字段的性能与公有字段几乎没有差异。主要的性能考量包括:

  • 访问速度:V8 引擎对私有字段进行了专门的优化,访问速度与公有字段相当。在微基准测试中,差异通常在 1-3% 以内,可以忽略不计。
  • 内存占用:私有字段的内存布局与公有字段类似,不会显著增加对象的内存占用。
  • 创建开销:类实例化时,私有字段的初始化开销与公有字段相同。

唯一的性能差异可能出现在品牌检测(#field in obj)场景中,因为这需要遍历对象的私有字段列表。但这种操作通常只在特定的验证场景中使用,不会成为性能瓶颈。

在实际项目中,选择使用私有字段还是公有字段应该基于设计需求(封装性)而非性能考虑。如果您需要真正的数据保护,私有字段的轻微性能开销是完全值得的。现代 JavaScript 引擎的优化使得私有字段在生产环境中的使用完全没有性能顾虑。