封装与私有字段
为什么要划分内部接口与外部接口?
- 术语: 这种划分在 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);
}
}