
本文详解 `maximum call stack size exceeded` 在对象 setter 中的成因:当 setter 内部直接赋值给自身属性时,会无限触发该 setter,引发递归调用直至栈溢出;核心解法是使用私有内部属性(如 `_age`)隔离数据存储与访问逻辑。
在 JavaScript 对象中定义 setter 时,一个常见却极易被忽视的陷阱是:在 setter 函数体内对同一属性名执行赋值操作。这看似自然的操作,实则会触发 setter 的自我调用,形成无限递归。
以原始代码为例:
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 = value;
- 由于 age 是一个 setter 属性,该赋值不会写入自有属性,而是再次调用 set age();
- 如此循环往复,每次调用都压入新的执行上下文到调用栈,最终超出 V8 引擎默认的调用栈深度限制(通常约 10k–20k 层),抛出 RangeError: Maximum call stack size exceeded。
✅ 正确做法是引入一个独立于 setter/getter 名称的内部存储属性(惯例以 _ 开头,如 _age),确保数据落地不触发代理逻辑:
立即学习“Java免费学习笔记(深入)”;
let user = {
name: "Fra",
surname: "Emme",
set age(value) {
if (value < 18) {
console.log(`${value} - You are underage :(`);
} else {
this._age = value; // ✅ 写入私有字段,无副作用
}
},
get age() {
return this._age; // ✅ 读取私有字段
},
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" }? 关键注意事项:
- _age 和 _place 是普通数据属性,不带任何访问器逻辑,因此读写均安全;
- 命名前缀 _ 是社区约定(非语言强制),表示“内部使用”,提醒开发者勿直接依赖;
- 若需真正私有化(ES2022+),可改用 #age 私有字段(但需注意兼容性与语法差异);
- getter 必须配套定义,否则 user.age 将返回 undefined(因 _age 未初始化时 this._age 为 undefined);
- 初始值建议显式初始化(如 this._age = null),避免读取未定义值引发意外行为。
总结:setter 的本质是属性访问的拦截器,而非普通赋值目标。一切对 setter 所代理属性的读/写操作,都会经过其对应的 get/set 函数。规避栈溢出的唯一可靠方式,就是让 setter 的实现逻辑绕过自身代理,直操作底层存储——这是理解 JavaScript 访问器属性设计哲学的关键一步。










