Setter中直接用this.prop=value会触发递归调用,必须通过私有存储(如#name、WeakMap或\_name)隔离赋值目标与访问代理,其中WeakMap最干净,私有字段语法简洁但需正确定义。

Setter 中直接用 this.prop = value 会触发自身递归调用
这是最常踩的坑:在 ES6 class 的 setter 里,如果直接写 this.name = value,而 name 又恰好有对应的 setter,就会无限递归,最终抛出 RangeError: Maximum call stack size exceeded。这不是设计缺陷,而是 JavaScript 强制要求你显式区分「赋值目标」和「访问代理」。
解决思路只有一个:把真实值存在一个「私有存储位置」,让 setter 和 getter 都绕过自身、操作这个位置。常见做法包括:
- 用下划线前缀变量(如
_name),约定为内部存储,setter 写入它,getter 读取它 - 用
WeakMap存储私有值,真正隔离作用域(推荐用于需要强封装的场景) - 用私有字段(
#name)——但注意:私有字段不能在 getter/setter 中直接通过this.#name赋值,必须先声明,且仅限类内访问
用 WeakMap 实现真正隔离的 setter
WeakMap 是目前最干净的方案:它不污染实例属性,避免命名冲突,且能随实例自动回收。关键在于把实例作为 key,把值存进 map。
const _valueMap = new WeakMap();
class Person {
constructor(name) {
_valueMap.set(this, { name }); // 初始化私有状态
}
set name(value) {
const state = _valueMap.get(this);
if (state) state.name = value; // 修改私有状态,不触发自身 setter
}
get name() {
return _valueMap.get(this)?.name ?? '';
}
}
注意点:
- 必须在 constructor 中调用
_valueMap.set(this, ...),否则_valueMap.get(this)返回undefined - 不能在 setter 里写
this.name = value,那又绕回去了 -
WeakMap不可枚举,调试时看不到,但这是封装性的代价
私有字段 #name 的 setter 写法限制
ES2022 私有字段语法简洁,但 setter 内部对 #name 的赋值有严格限制:你只能在类定义体顶层声明它,且必须用 this.#name 访问——但这个访问本身不会触发 setter,所以是安全的。
class Person {
#name = '';
constructor(name) {
this.name = name; // 触发 setter,没问题
}
set name(value) {
if (typeof value !== 'string') throw new TypeError('name must be string');
this.#name = value; // ✅ 直接写入私有字段,不触发 setter
}
get name() {
return this.#name;
}
}
容易忽略的细节:
- 私有字段必须在类体中声明(哪怕只写
#name;),否则this.#name报SyntaxError - 不能在 setter 外部(比如原型方法里)访问
this.#name,会报错 - Babel 或旧版 Node.js 可能不支持,需检查运行时环境
为什么不用 Object.defineProperty 手动定义?
手动定义确实可行,但实际项目中几乎没人这么干,因为:
- 每次都要写
Object.defineProperty(this, 'prop', {...}),模板代码多,易出错 - 无法在 class 字段初始化语法(
field = value)中混用,破坏一致性 - 如果类继承自父类,手动 define 可能覆盖父类同名 accessor,且难以调试
- 现代语法(
#field/WeakMap)更明确表达意图,工具链支持更好
除非你在写兼容 IE 的代码,或者需要动态生成大量 accessor,否则没必要退回到 defineProperty。
真正难的不是写对 setter,而是判断该用哪种隔离方式:简单组件用私有字段就够了;跨框架共享逻辑或需运行时动态控制时,WeakMap 更灵活;而下划线约定只适合临时脚本或团队规范极强的场景。

















