本文通过 setTimeout 定时打印数值的示例,分析直接使用循环变量导致的执行时机与值共享问题。对比全局变量方案后,重点讲解利用闭包为每次定时器调用创建独立作用域的方法,确保每个定时任务能获取正确的循环变量值,并提供了封装函数的实用技巧。
setTimeout 定时打印示例分析
本文通过 setTimeout 实现一个按时间间隔依次打印数值的示例。早期常有人觉得用 JS 实现这种能力比较牵强,这其实是对 JS 特性理解不足。虽然使用 setInterval 实现相对简单,但本文旨在探讨如何使用 setTimeout 达成相同效果。我们从最基础的思路开始分析,通常会写出如下代码。
for(var i = 0; i < 5; i++)
{
setTimeout(console.log(i),i*1000);
}
这段代码虽然依次打印了每个 i 的值 0, 1, 2, 3, 4,但执行时间并未如预期那样产生间隔。原因在于 console.log() 是立即执行的方法调用,在调用该方法时,参数 i 的值已经被确定并立即输出,因此没有达到预期的延时效果。
接下来我们查看另一种写法。
for(var i = 0; i< 5; i++ ){ setTimeout(function(){ console.log(i); },i*1000); }
变量共享与全局变量方案
这里使用一个匿名函数包裹 console.log 来打印 i,但由于 i 是共享的变量,在第一个 setTimeout 触发之前,for 循环已经执行完毕,此时 i 的值已变为 5。因此,所有定时器最终都会打印 5,共打印四次。针对此问题,我们先看第一种解决方案:
var j = 0; function abc(){ console.log("j = "+j); j++; }
for(var i = 0; i < 10; i++ ){
setTimeout(abc,i*1000) }
这里引入另一个全局变量来存储值,每执行一次函数 abc,j 就加一。当执行到 setTimeout 时,调用 abc 函数,似乎能达到预期效果。然而,使用全局变量容易导致值被意外修改或产生命名冲突等问题。第二种方案则再次引入闭包函数,因为闭包在每次创建时都会拥有独立的空间来存储唯一的值。基于此思路,我们将代码改写如下。
for(var i = 0; i < 10; i++ )
{
(function(x){ setTimeout(function()
{
console.log(x) },x*1000) })(i)
}
利用闭包解决作用域问题
我们将 for 循环中 i 的每一次执行值,传递给不同创建的闭包函数。这样,每个闭包函数内部存储的 i 值互不相同,从而达到了我们预期的定时打印不同数值的结果。
补充说明:使用闭包对 setTimeout 进行简单封装。
闭包封装与字符串拼接陷阱
在编写 JS 脚本时,经常会遇到需要拼接字符串来调用函数的情况,例如调用 setTimeout。
var msgalert="test";
function TestAlert(msg)
{
alert(msg)
}
$(document).ready(function () {
$("#btnCancel").click(function (e) {
setTimeout("TestAlert("+msgalert+")",1000);
});
})
经过长时间排查,发现无法弹出对话框的原因是少了一对单引号。
$(document).ready(function () {
$("#btnCancel").click(function (e) {
setTimeout("TestAlert('"+msgalert+"')",1000);
});
})
这种字符串拼接的写法容易出错,且难以检查。如果使用闭包则可以完全避免此类问题,改写方式如下。
var msgalert="test";
function dalayAlert(msg ,time){
setTimeout(
TestAlert(msg),
time
);
}
function TestAlert(msg)
{
alert(msg)
}
$(document).ready(function () {
$("#btnCancel").click(function (e) {
dalayAlert(msgalert,1000)
});
由于使用了闭包,代码逻辑变得简单,且检查错误也变得更加容易。