【声明】本博客所有内容均为个人业余时间创作,所述技术案例均来自公开开源项目(如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判定步骤:拿到回调代码 → 从它定义处逐层向上查名字——自身局部 → 最近外层函数局部 → 再外层 → 全局。在链上的就能闭包捕获,不在链上的只能靠参数注入(这正是上篇"必须传参"条件①)。
handler:async(args)=>{// ① handler 作用域constclient=Rpc.client(...)// client 在这层constcwd=Filesystem.resolve(...)// cwd 在这层setTimeout(()=>{// ② 回调定义在这client.call("checkUpgrade",{directory:cwd})// ✓ 链上可见},1000)}⚠️三个容易误解的点
- 看定义位置,不是调用位置:回调哪怕 1 秒后才在事件循环里执行,能访问的仍是定义时(handler 里)的作用域链,与"此刻在哪执行"无关。
- 不是"最近一次调用":每次 handler 执行都有一套独立的局部环境,回调闭包捕获的是它定义那一次的
client/cwd,不同次调用互不干扰。 - 捕获的是"环境"不是"值":闭包持有的是词法环境(可变绑定),所以读到的是执行时刻的最新值——这正是 var 陷阱的根源。
🧩var 循环陷阱:三个条件叠加
回到那个输出(203 篇核心):
for(vari=0;i<3;i++)setTimeout(()=>console.log(i),0)// 3 3 3for(vari=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 就没事
for(leti=0;i<3;i++)setTimeout(()=>console.log(i),0)// 0 1 2let是块作用域,每轮循环创建一个独立的i,闭包各自捕获自己的那个 → 输出 0 1 2。所以 var 陷阱的本质是"var 函数作用域共享一个变量 + 闭包引用共享 + 延迟执行"三件事叠加;任缺其一都不会踩坑。
📊本系列回顾
| 篇 | 主题 | 一句话 |
|---|---|---|
| 201 | JS 语法:setTimeout 七种常用形式 | 三个自由度:回调写法 / 数据注入 / 生命周期 |
| 202 | JS 语法:闭包捕获还是参数传递 | 作用域内+实时值→闭包;作用域外+快照→传参 |
| 203 | JS 语法:作用域链与 var 循环陷阱 | 闭包能访问什么看定义位置;var 陷阱 = 共享变量+延迟 |
📌一句话记忆
闭包的作用域链看定义位置(自身 → 外层函数 → 全局),捕获的是可变环境而非值快照;var 循环陷阱 =
var函数作用域共享一个i+ 闭包引用共享 + 定时器延迟执行,三条件叠加输出3 3 3,换let或第三参数传值即解。
OK,本篇先到这里,如有疑问,欢迎评论区留言讨论,祝各位功力大涨,技术更上一层楼!!!更多内容见下篇 blog
【Agent】【OpenCode】TUI 内部:SDK 客户端与事件流