循环与回调函数的问题,也就是作用域与闭包的问题。个中原因是 JavaScript 的两个特性:
- JavaScript 不支持块作用域
- 只要使用了回调函数就是在使用闭包
第一个特性就是字面的意思,第二个特性是由闭包的特性引申出来的。
从实际代码的角度来讲,闭包是在定义时所在作用域以外执行的函数。而 js 引擎在执行 js 代码,就会将回调函数丢给事件触发线程。当回调函数执行时,回调函数是在定义时所在作用域以外,所以说只要使用了回调函数就是在使用闭包。
弄清楚了概念,来看一下循环与回调函数的问题,以下方代码为例:
var btns = document.querySelectorAll("button");
for(var i = 0; i < btns.length;i++){//假设 btn.length 为 6
btns[i].onclick = function() {
console.log(i);
}
}
这段代码中,打印 i 所在的匿名函数是一个回调函数。当按钮点击时该函数就会触发,打印循环时的 i。然而结果出人意料,每次点击时输出的数字都是 6,为什么会这样呢?
首先是因为 js 没有块作用域,for 循环多次迭代中只是声明了一个 i,所以最后 i 只有一个值,也就是循环结束时的值。
其次是因为回调函数是闭包,所以在作用域外引用作用域中的 i,这个 i 只有一个最终值。
这个例子也可以换一个角度来想。如果 js 支持块作用域,那么上方代码中每个按钮的回调都声明在不同的块作用域中。只要每次迭代保存一下 i 的值,如使用 var j = i,那么每次打印 j,就可以打印 i 的不同值了。
根据上方的原因分析,我们可以得出解决方案——使 i 的值声明或保存在不同的作用域中。具体方法多种多样,下面列举两个解决方法:
var btns = document.querySelectorAll("button"); for(let i = 0; i < btns.length;i++){//假设 btn.length 为 6 btns[i].onclick = function() { console.log(i); } }
let 是 ES6 的关键字,用来声明一个块级作用域的本地变量,并使变量每次迭代时声明一次。
var btns = document.querySelectorAll("button"); for(var i = 0; i < btns.length;i++){//假设 btn.length 为 6 (function(){ var j = i; btns[i].onclick = function() { console.log(j); } })(); }
(function(){})()是一个立即执行函数,每次迭代中都有一个不同的函数作用域,在其中保存 i 的当前值并使用。
James Harris 2017-01-20 08-48-05 .jpg
网友评论