【声明】本博客所有内容均为个人业余时间创作,所述技术案例均来自公开开源项目(如Github,Apache基金会),不涉及任何企业机密或未公开技术,如有侵权请联系删除
标题
203、【Agent】【OpenCode】JS 语法:作用域链与 var 循环陷阱
背景
上篇 blog
【Agent】【OpenCode】JS 语法:闭包捕获还是参数传递
讲了给异步回调喂数据的两种方式与取舍:闭包捕获 (实时引用、要求变量在回调定义作用域内)vs 第三参数传值 (注册时快照、用于循环/复用场景);并给出准则------变量在作用域内 + 要实时值 + 一次性内联用闭包,作用域外 + 要快照 + 复用/防泄漏用传参。上篇在讲 var 循环陷阱时甩出"3 3 3 vs 0 1 2"的输出,但没有解释清楚为什么 。本篇收束这个系列:先讲清作用域链怎么判定 (决定闭包能访问什么),再拆 var 循环陷阱的完整成因
OpenCode
上篇留了个悬念:同样是三个 setTimeout,闭包版输出 3 3 3、传参版输出 0 1 2。要理解它,得先弄懂"闭包到底能访问什么"------这由作用域链决定。
🧩 作用域链:闭包能访问什么,看它"写在哪"
闭包能访问的变量 = 从它定义位置沿词法作用域向上,每一层能包住它的作用域里的名字:
模块 / 全局作用域 ← process、Rpc、Log、console...
└─ 外层函数作用域 ← client、cwd、args...(本案例在这层)
└─ 回调自身作用域 ← 自己的参数、内部 let/var/const
判定步骤 :拿到回调代码 → 从它定义处逐层向上查名字------自身局部 → 最近外层函数局部 → 再外层 → 全局。在链上的就能闭包捕获,不在链上的只能靠参数注入(这正是上篇"必须传参"条件①)。
ts
handler: async (args) => { // ① handler 作用域
const client = Rpc.client(...) // client 在这层
const cwd = Filesystem.resolve(...) // cwd 在这层
setTimeout(() => { // ② 回调定义在这
client.call("checkUpgrade", { directory: cwd }) // ✓ 链上可见
}, 1000)
}
⚠️ 三个容易误解的点
- 看定义位置,不是调用位置 :回调哪怕 1 秒后才在事件循环里执行,能访问的仍是定义时(handler 里)的作用域链,与"此刻在哪执行"无关。
- 不是"最近一次调用" :每次 handler 执行都有一套独立的局部环境,回调闭包捕获的是它定义那一次 的
client/cwd,不同次调用互不干扰。 - 捕获的是"环境"不是"值" :闭包持有的是词法环境(可变绑定),所以读到的是执行时刻的最新值------这正是 var 陷阱的根源。
🧩 var 循环陷阱:三个条件叠加
回到那个输出(203 篇核心):
js
for (var i = 0; i < 3; i++)
setTimeout(() => console.log(i), 0) // 3 3 3
for (var i = 0; i < 3; i++)
setTimeout(console.log, 0, i) // 0 1 2

三个条件叠加才会踩坑:
1. var 是函数作用域,不是块作用域
for 里的 var i 属于外层函数作用域,整个循环只有一个 i ------不是每轮一个。三轮循环在几微秒内全部注册完,i 最后停在 3 (0→1→2→3 后退出)。
2. 闭包捕获的是"变量本身"
() => console.log(i) 只记录了"要读变量 i",没存值。三个回调注册时都没存,只是各签了张"到时读 i"的欠条。
3. 定时器是延迟执行的
setTimeout(..., 0) 排到当前任务栈清空后的宏任务队列 。循环跑完、i=3,事件循环才开始兑现三张欠条------读到的都是 3。
传参版为什么对 :setTimeout(console.log, 0, i) 的第三参数 i 在注册那一刻 就被拷贝存进定时器(0、1、2),兑现时用存好的值,与循环结束后的 i 无关。
类比:闭包版 = 三张欠条都写"按当时行情付款",兑现时行情已是 3;传参版 = 注册当场把 0、1、2 抄进纸条,兑现看纸条。
💡 用 let 就没事
js
for (let i = 0; i < 3; i++)
setTimeout(() => console.log(i), 0) // 0 1 2

let 是块作用域 ,每轮循环创建一个独立的 i ,闭包各自捕获自己的那个 → 输出 0 1 2。所以 var 陷阱的本质是"var 函数作用域共享一个变量 + 闭包引用共享 + 延迟执行"三件事叠加;任缺其一都不会踩坑。
📊 本系列回顾
| 篇 | 主题 | 一句话 |
|---|---|---|
| 201 | JS 语法:setTimeout 七种常用形式 | 三个自由度:回调写法 / 数据注入 / 生命周期 |
| 202 | JS 语法:闭包捕获还是参数传递 | 作用域内+实时值→闭包;作用域外+快照→传参 |
| 203 | JS 语法:作用域链与 var 循环陷阱 | 闭包能访问什么看定义位置;var 陷阱 = 共享变量+延迟 |
📌 一句话记忆
闭包的作用域链看定义位置 (自身 → 外层函数 → 全局),捕获的是可变环境 而非值快照;var 循环陷阱 =
var函数作用域共享一个i+ 闭包引用共享 + 定时器延迟执行,三条件叠加输出3 3 3,换let或第三参数传值即解。
OK,本篇先到这里,如有疑问,欢迎评论区留言讨论,祝各位功力大涨,技术更上一层楼!!!更多内容见下篇 blog