
本文详解 `maximum call stack size exceeded` 在对象 setter 中的成因:当 setter 内部直接赋值给同名属性时,会无限触发自身,引发递归调用直至栈溢出;核心解法是使用私有命名约定(如 `_age`)分离内部存储与访问接口。
在 JavaScript 中,对象的 set 访问器(setter)本质上是一个函数,每当对目标属性执行赋值操作(如 obj.age = 20)时,引擎会自动调用对应的 setter 函数。关键陷阱在于:若该 setter 函数体内再次对同一属性名进行赋值(如 this.age = value),就会形成隐式递归调用——因为 this.age = value 并非简单写入数据,而是再次触发 set age(),如此循环往复,最终耗尽调用栈空间,抛出 RangeError: Maximum call stack size exceeded。
以原始代码为例:
set age(value) {
if (value < 18) {
console.log(`${value} - You are underage :(`);
} else {
this.age = value; // ❌ 危险!此处再次调用 set age()
}
}user.age = 20 → 触发 set age(20) → 执行 this.age = 20 → 再次触发 set age(20) → …… 无限递归。
✅ 正确做法是引入一个内部存储属性(通常以 _ 开头作为命名约定,表示“仅供内部使用”),将数据保存到该属性中,避免触发 setter 自身:
立即学习“Java免费学习笔记(深入)”;
let user = {
name: "Fra",
surname: "Emme",
set age(value) {
if (value < 18) {
console.log(`${value} - You are underage :(`);
} else {
this._age = value; // ✅ 写入私有字段,不触发 setter
}
},
get age() {
return this._age; // ✅ 提供配套 getter,保持属性可读
},
get fullName() {
return `${this.name} ${this.surname}`;
},
set place(value) {
if (value === "" || value === null || value.length <= 2) {
console.log("Invalid place");
return;
}
this._place = value; // ✅ 同理,避免 this.place = value
},
get place() {
return this._place;
}
};
user.age = 20;
user.place = "Rovereto";
console.log(user); // { name: "Fra", surname: "Emme", _age: 20, _place: "Rovereto" }⚠️ 注意事项:
- getter 必须配套实现:仅定义 setter 而无 getter 会导致属性不可读(访问 user.age 返回 undefined),且可能干扰 for...in 或 Object.keys() 等遍历行为。
- 命名约定非强制但强烈推荐:_age 是社区广泛接受的私有字段标识,虽不能真正阻止外部访问,但清晰表达了设计意图;ES2022 引入的 #privateField 语法(需 #age)可提供真正私有性,但目前对象字面量中尚不支持私有字段(仅类中可用)。
- 避免逻辑耦合副作用:setter 中应专注数据校验与存储,避免嵌入复杂业务逻辑或异步操作,否则易引发难以调试的副作用。
总结:setter 的本质是拦截赋值行为,而非普通变量赋值。任何在 setter 内对同名属性的写操作都会重新进入该 setter,这是 JavaScript 访问器机制的固有特性。理解并规避这一“自调用陷阱”,是掌握现代 JavaScript 对象封装能力的关键一步。










