You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在JavaScript类原型上使用defineProperty时的变量作用域问题

JavaScript中Object.defineProperty捕获变量快照的问题解释

我尝试使用Object.defineProperty在类上动态创建getter,希望这些getter能捕获调用defineProperty时外部变量的快照值——也就是后续调用getter时,能返回定义时的变量值,而非变量后续修改后的值。但嵌套循环场景下出现了不符合预期的行为:外层循环定义的计数器cA,被内部循环的getter引用后,最终getter返回的是cA的最终值,而非defineProperty调用时的即时值。

问题复现(TestA)

代码:

class TestA {}

for (let i = 0; i < 2; i++) {
  let cA = 0;
  for (let j = 0; j < 2; j++) {
    Object.defineProperty(TestA.prototype, 'prop_' + i + "_" + j, {
      get() { return "val_" + i + "_" + j + ":" + cA; }
    });
    cA++;
  }
}

tA = new TestA();

for (let i = 0; i < 2; i++) {
  let cA = 0;
  for (let j = 0; j < 2; j++) {
    console.log(
      "not-really-but-lets-say-expected", "val_" + i + "_" + j + ":" + cA,
      "got", tA["prop_" + i + "_" + j]
    );
    cA++;
  }
}

输出:

"not-really-but-lets-say-expected", "val_0_0:0", "got", "val_0_0:2"
"not-really-but-lets-say-expected", "val_0_1:1", "got", "val_0_1:2"
"not-really-but-lets-say-expected", "val_1_0:0", "got", "val_1_0:2"
"not-really-but-lets-say-expected", "val_1_1:1", "got", "val_1_1:2"

可以看到,所有getter返回的cA值都是外层循环结束后的最终值2,而非定义时的0或1。

解决方案(TestB)

通过在内部循环中新增变量复制cA的即时值,getter可以正确捕获快照:

代码:

class TestB {}

for (let i = 0; i < 2; i++) {
  let cA = 0;
  for (let j = 0; j < 2; j++) {
    let cB = cA;
    Object.defineProperty(TestB.prototype, 'prop_' + i + "_" + j, {
      get() { return "val_" + i + "_" + j + ":" + cB; }
    });
    cA++;
  }
}

tB = new TestB();

for (let i = 0; i < 2; i++) {
  let cB = 0;
  for (let j = 0; j < 2; j++) {
    console.log(
      "not-really-but-lets-say-expected", "val_" + i + "_" + j + ":" + cB,
      "got", tB["prop_" + i + "_" + j]
    );
    cB++;
  }
}

输出:

"not-really-but-lets-say-expected", "val_0_0:0", "got", "val_0_0:0"
"not-really-but-lets-say-expected", "val_0_1:1", "got", "val_0_1:1"
"not-really-but-lets-say-expected", "val_1_0:0", "got", "val_1_0:0"
"not-really-but-lets-say-expected", "val_1_1:1", "got", "val_1_1:1"

原理解释

这本质是JavaScript闭包的变量引用特性导致的:

  • 闭包捕获的是变量的引用,而非值的副本。在TestA中,外层循环的cA属于外层迭代的作用域,内部循环的所有getter函数作为闭包,共享同一个cA变量的引用。当后续cA++执行到最终值2,调用getter时会读取这个变量当前的引用值,也就是2。
  • 在TestB中,内部循环里用let cB = cA创建了一个内部迭代作用域的变量,每次内部循环都会生成一个新的cB,并把当前cA的即时值赋值给它。getter捕获的是这个当前迭代的cB变量的引用,后续cA的修改不会影响已经创建的cB,因此能保留定义时的快照值。
  • 补充:let在循环中每次迭代都会创建独立的作用域,所以内部循环的j和cB每次都是全新的变量,而外层循环的cA是每个外层迭代的作用域变量,内部循环的所有getter都绑定到同一个外层cA的引用上。

内容的提问来源于stack exchange,提问作者Paljas

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 09:23:05