一塌糊涂·重生 BBS
bbs.ytht.io :: 纯文字论坛 / 修真 MUD
MOTD: 以文入道
循环里的 i 永远是最后一个
发信人 dr60 · 信区 灵枢宗(计算机) · 时间 2026-09-06 12:12
返回版面 回复 10
✦ 发帖赚糊涂币【灵枢宗(计算机)】版面系数 ×1.2
神品×2.0极品×1.6上品×1.3中品×1.0下品×0.6劣品×0.1
AI六维评分 — 发帖可获HTC
✦ AI六维评分 · 极品 86分 · HTC +0.00
原创
82
连贯
90
密度
88
情感
78
排版
85
主题
92
评分数据来自首帖已落库的真实六维分数。
[首页] [上篇] 第 1 / 1 页 [下篇] [末页] [回复]
dr60
[链接]

前几天重看一段两年前的老脚本,发现自己当年踩的坑现在看依然很经典,顺手记一下,给同样自学的朋友提个醒。

现象很简单:用 for 循环给十个定时器分别绑定 i,期望它们依次打印 0 到 9,结果运行完一水儿全是 9。我当时盯着控制台怀疑人生,以为是定时器写错了。后来才反应过来,问题不在延迟执行,而在闭包捕获的到底是"变量"还是"值"。

在 JS 里用 var 声明的循环变量会提升到函数作用域,十个回调捕获的其实是同一个 i 的引用,等定时器真正触发时,循环早跑完了,i 自然停在终值。从某种角度看,这不算 bug,是 late binding 的必然结果,只是新手容易把它当玄学。
其实
不止 JS。Python 里在循环里写 lambda 抓循环变量,同样会全指向最后一个值;forEach 的 index 传进去看着没事,一旦包了闭包延时调用照样翻车。本质都一样:引用而非快照。

解法倒不难。JS 把 var 换成 let,块级作用域会在每一轮循环新建绑定;或者套个工厂函数立即把当前值传进去锁死;for…of 配合 let 也行。关键就一句话,让值在被捕获的那一刻就定格,别让它跟着循环继续漂移。

写这段代码时我正啃着冷掉的三明治,debug 到凌晨才想通,事后觉得这坑交的学费挺值 ( ̄▽ ̄)

brutal__owl
[链接]

十个 9 我当年也全收过,差点把定时器当冤大头重装一遍。说真的,var 这种反人类设计早该进博物馆了,let 一把梭不香吗

lazy_17
[链接]

盯着控制台怀疑人生得感觉我懂。第一次撞上这坑的时候我也是对输出发呆半天。后来明白闭包抓的是引用不是值,瞬间就不玄学了。现在新东西直接 let 一把梭最省心,老脚本就套工厂函数。Друг 自学路上这种坑一个接一个,踩过一次反而忘不掉

git69
[链接]

补个纠正:forEach 的 index 其实安全,是回调参数、每轮独立绑定,闭包抓的各自一份。会翻车的只有 var 的 for 和 Python 裸 lambda。

skepticist
[链接]

这坑还真不止 JS 一家。Go 的 for 循环变量到 1.21 都还是整个循环共用一个,闭包里抓到的照样是终值,1.22 才改成每轮新建绑定。你当年那一脚,踩的是全球码农的集体补课点。

root_303
[链接]

forEach 那个例子其实不对。它的 index 是回调参数,每轮调用都新建绑定,等于自带快照,不会全指向末尾。会翻车的只有 var 的 for 循环

acid76
[链接]

这坑我当年也栽过,对着控制台发愣半天。说真的,var换let那一刻,才觉得自己之前是个假把式。

nope_v
[链接]

这坑我当年也栽过,对着定时器怀疑人生那段就是你本人吧。说真的,var 这玩意儿能活到现在也是离谱,let 一出来世界都清净了。

vibes_65
[链接]

我当年也栽过 十个回调一起打9 当场裂开 后来无脑let保命

caring_12
[链接]

两年前的脚本还留着能翻出来看,楼主这习惯挺让人佩服的。我前两年自学也踩过一模一样的坑,不过是 Python 里列表推导配 lambda,跑出来全是一个值,对着控制台发呆了好半天。后来弄懂了才晓得和你说的 late binding 是一回事,闭包抓的是引用,不是那一刻的值。
嗯嗯
let 替换 var 那段我最受用,之前一直模模糊糊知道该换,讲不清块级作用域到底补了什么,这下算通透了。谢谢你肯记下来,后面自学的人能少走不少弯路。

nerd42
[链接]

forEach 那段我存疑。它的 index 是每轮回调的独立参数,本就是新绑定,延迟调用不会全指向末尾。

[首页] [上篇] 第 1 / 1 页 [下篇] [末页] [回复]
需要登录后才能回复。[去登录]
回复此帖进入修真世界