JavaScript函数内变量赋值为何是引用而非复制?闭包循环问题
问题本质:赋值操作的执行时机错了
你这个写法失效的核心原因非常直接:let j = i这行代码的执行时间,从「循环迭代阶段」延后到了「shooter函数被调用阶段」,而闭包捕获的是变量引用,不是变量的瞬时值。
我们把三个版本的执行逻辑拆开看就很清楚:
- 第一个原始版本:所有shooter函数内部直接引用外层
makeArmy作用域的同一个i变量。等你循环走完调用shooter的时候,i已经自增到10了,所以所有输出都是10。 - 第二个能正常运行的版本:
let j = i写在while循环的块级作用域里,这行代码会在每一轮循环跑的时候立刻执行。每一轮循环都会生成一个独立的块级作用域、一个独立的j变量,当场把当前轮次的i值存进去。后面i再怎么自增,都和已经存好值的各个j没关系。每个shooter闭包持有的是对应轮次的独立j的引用,调用的时候自然能拿到正确的序号。 - 第三个出问题的版本:你把
let j = i挪到了shooter函数内部,这行代码在循环阶段根本不会执行——循环阶段只是定义了shooter函数、把函数引用塞进数组,完全不会跑函数内部的逻辑。等你后面手动调用army[0]()、army[1]()的时候,整个makeArmy函数早就执行完毕了,外层的i已经走完循环变成10。这时候你才在shooter内部执行let j = i,拿到的当然是已经变成10的i,所有输出自然全是10。
你对相关机制的认知偏差点
你踩坑的核心是混淆了三个关键逻辑:
- 赋值时机决定取值结果:不是只要写了
j = i就能固定住i的值。如果赋值发生在循环执行阶段,拿到的是循环当时的i值;如果赋值发生在shooter调用阶段,拿到的就是调用那一刻的i值。 - 闭包不会做值快照:闭包持有的是对外部变量的引用,不是代码编写那一刻变量的值。不管你在函数内部怎么转存变量,只要转存的动作发生在函数调用时,你读到的永远是变量最终的状态。
- 块级作用域的绑定时机是块执行时:while循环每一轮的独立块级作用域,是在循环迭代执行那一轮代码的时候生效的。如果你把变量声明、赋值都挪到了内部的shooter函数里,那这个变量完全属于shooter的函数作用域,和循环每一轮的独立块级作用域没有任何关系,自然存不下每一轮的i值。
你可以加几行日志跑一遍,能非常直观看到执行顺序的差异:
function makeArmy() { let shooters = []; let i = 0; while (i < 10) { console.log('循环迭代中,当前i值:', i); let shooter = function () { console.log('shooter被调用时,当前i值:', i); let j = i; console.log('输出j值:', j); }; shooters.push(shooter); i++; } return shooters; } let army = makeArmy(); // 上面这行执行完,会先打印10次「循环迭代中」的日志,i从0到9 army[0](); // 这时候才会打印shooter内部的日志,此时i已经是10
除了把j声明在循环块里,更常见的写法是用for循环的let声明——for循环的let i会自动在每一轮迭代生成独立的i绑定,不用额外声明j变量:
function makeArmy() { let shooters = []; for (let i = 0; i < 10; i++) { let shooter = function () { console.log(i); }; shooters.push(shooter); } return shooters; }
内容的提问来源于stack exchange,提问作者Elbasel
相关产品推荐
相关产品推荐

