跳到主要内容

封装与私有字段

为什么要划分内部接口与外部接口?​

  • 术语: 这种划分在 OOP 中称为封装 (encapsulation);
  • 保护使用者: 外部代码改不到不该改的内部状态, 避免「好心改坏」;
  • 便于维护: 类的作者可自由重命名、改签名、删除内部成员而不影响使用者;
  • 隐藏复杂度: 对外只暴露简单稳定的接口, 细节可以完全换掉;

_ 前缀如何模拟 protected 字段?​

  • 约定: 下划线开头表示内部成员, 语言不强制, 只靠团队自觉;
  • 控制读写: 内部真名用 _waterAmount, 对外用 getter/setter 做校验;
  • 只读属性: 只写 getter 不写 setter, 赋值时静默失败 (严格模式抛错);
class CoffeeMachine {
_waterAmount = 0;
set waterAmount(v) {
this._waterAmount = v < 0 ? 0 : v;
}
get waterAmount() {
return this._waterAmount;
}
constructor(power) {
this._power = power;
}
get power() {
return this._power;
}
}
  • 可继承: 子类方法里能直接读写 this._waterAmount, 这正是 protected 相对 private 的价值;
  • 风格选择: get/set 语法更短, getX()/setX() 函数更灵活 (可接收多参数);

# 私有字段与 _ 保护字段有何区别?​

  • 强制力: # 由语言强制, 类外访问 obj.#x 直接语法错误; _ 只是命名约定;
  • 作用域: # 只在本类体内可见, 子类写 this.#waterAmount 也报错, 只能走公开的 getter/setter;
  • 命名: 私有字段与同名公开字段互不冲突, #waterAmount 与 waterAmount 可共存;
  • 不可动态访问: this["#name"] 不成立, 这是为隐私保留的语法限制;
  • 品牌检查: #x in obj 可判断对象是否带该私有字段, 但只能在声明该私有名的类体内使用;
class CoffeeMachine {
#waterLimit = 200;
#fixWaterAmount(v) {
return v < 0 ? 0 : Math.min(v, this.#waterLimit);
}
setWaterAmount(v) {
this.#waterLimit = this.#fixWaterAmount(v);
}
}
const m = new CoffeeMachine();
m.setWaterAmount(300);
m.#waterLimit = 1000; // SyntaxError: 类外不可访问
  • 私有访问器: 也可用 get #x() / set #x(v) 定义私有 getter/setter, 做到受控读写;
  • 代价: 子类拿不到父类私有字段, 很多场景下手太紧, 因此 protected 仍然常见;
  • 典型用法: 公开的 get/set waterAmount 读写私有的 #waterAmount, 对外看起来就是普通属性;

用 WeakMap 如何模拟私有字段?​

  • 思路: 数据存在模块作用域的 WeakMap 里, 以实例为键, 外部拿不到这张表;
  • 优点: 真正不可从外部访问, 且键是弱引用, 实例被回收时数据自动消失;
  • 代价: 私有数据不在实例自身上, 每个私有字段都要一次 get/set;
const water = new WeakMap(); // 模块外不可见
class CoffeeMachine {
constructor() {
water.set(this, 0);
}
getWaterAmount() {
return water.get(this);
}
setWaterAmount(v) {
water.set(this, v < 0 ? 0 : v);
}
}