在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
相关产品推荐
相关产品推荐

