事件监听器回调函数中var变量的作用域问题咨询
为什么用var声明的循环索引在onclick回调里是循环结束后的值?
兄弟,这个问题我当初刚学JavaScript的时候也踩过一模一样的坑!其实核心原因就是var的作用域特性加上回调函数的执行时机,咱们一步步把它说清楚:
1. var的作用域根本不是你以为的「块级作用域」
你觉得{}会把var变量的作用域限制在里面?错啦!var是函数级作用域(如果没有函数包裹就是全局作用域),它不认代码块(比如for循环的{}、if的{})。
举个例子,你写的for循环里用var i,这个i其实是挂载在当前函数(或者全局)上的唯一变量——整个循环过程中,每次迭代都是在修改同一个i的值,而不是每循环一次就创建一个新的i。
2. onclick回调是「异步执行」的
for循环是同步代码,会以极快的速度跑完所有迭代。而按钮的onclick回调函数,只有当你点击按钮的时候才会执行。等你真的去点按钮时,循环早就结束了,这时候i已经变成了循环终止时的那个值(比如循环条件是i < 3,那结束后i就是3)。
回调函数执行时,会去它所在的作用域里找i变量,而这个作用域里从头到尾就只有那一个i,所以自然就输出循环结束后的最终值了。
给你看个典型的错误示例代码:
for (var i = 0; i < 3; i++) { document.getElementById(`btn${i}`).onclick = function() { console.log(i); // 不管点哪个按钮,都会输出3 } }
三个按钮的回调函数都引用了同一个i变量,循环跑完后i已经是3,所以点击任何按钮都会输出3。
怎么解决这个问题?
给你两个常用的方案:
- 用let替代var:let是ES6新增的,支持块级作用域。每次循环迭代时,都会创建一个新的i变量,回调函数引用的就是当前迭代的那个i,不会被后续循环修改:
for (let i = 0; i < 3; i++) { document.getElementById(`btn${i}`).onclick = function() { console.log(i); // 点btn0输出0,btn1输出1,btn2输出2 } } - 用立即执行函数(IIFE)创建闭包:这是var时代常用的解决方案,通过立即执行函数给每次循环的i创建一个独立的作用域,把当前迭代的i值保存下来:
这里每次循环调用IIFE时,会把当前的i作为参数for (var i = 0; i < 3; i++) { (function(currentI) { document.getElementById(`btn${currentI}`).onclick = function() { console.log(currentI); } })(i); }currentI传进去,currentI是函数内部的局部变量,每个回调引用的都是自己的currentI,不会被后续循环覆盖。
简单总结一下:问题根源就是var没有块级作用域,加上回调函数执行时机晚于循环,导致所有回调共享同一个i变量。换成let或者用闭包就能完美解决啦!
内容的提问来源于stack exchange,提问作者Ali Chamas
相关产品推荐
相关产品推荐

