js 循环和闭包

要说明闭包,for 循环是最常见的例子。

for (var i=1; i<=5; i++) {
 setTimeout( function timer() {
 console.log( i );
 }, i*1000 );
}

正常情况下,我们对这段代码行为的预期是分别输出数字 1~5,每秒一次,每次一个。但实际上,这段代码在运行时会以每秒一次的频率输出五次 6。这是为什么?首先解释 6 是从哪里来的。这个循环的终止条件是 i 不再 <=5。条件首次成立时 i 的值是6。因此,输出显示的是循环结束时 i 的最终值。仔细想一下,这好像又是显而易见的,延迟函数的回调会在循环结束时才执行。事实上,当定时器运行时即使每个迭代中执行的是 setTimeout(…, 0),所有的回调函数依然是在循
环结束后才会被执行,因此会每次输出一个 6 出来。这里引伸出一个更深入的问题,代码中到底有什么缺陷导致它的行为同语义所暗示的不一致呢?缺陷是我们试图假设循环中的每个迭代在运行时都会给自己“捕获”一个 i 的副本。但是根据作用域的工作原理,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个 i。
这样说的话,当然所有函数共享一个 i 的引用。循环结构让我们误以为背后还有更复杂的机制在起作用,但实际上没有。如果将延迟函数的回调重复定义五次,完全不使用循环,那它同这段代码是完全等价的。下面回到正题。缺陷是什么?我们需要更多的闭包作用域,特别是在循环的过程中每个迭代都需要一个闭包作用域。IIFE 会通过声明并立即执行一个函数来创建作用域。
我们来试一下:

for (var i=1; i<=5; i++) {
 (function() {
 setTimeout( function timer() {
 console.log( i );
 }, i*1000 );
 })();
}

这样能行吗?试试吧,我等着你。
我不卖关子了。这样不行。但是为什么呢?我们现在显然拥有更多的词法作用域了。的确每个延迟函数都会将 IIFE 在每次迭代中创建的作用域封闭起来。如果作用域是空的,那么仅仅将它们进行封闭是不够的。仔细看一下,我们的 IIFE 只是一个什么都没有的空作用域。它需要包含一点实质内容才能为我们所用。它需要有自己的变量,用来在每个迭代中储存 i 的值:

for (var i=1; i<=5; i++) {
 (function() {
 var j = i;
 setTimeout( function timer() {
 console.log( j );
 }, j*1000 );
 })();
}

行了!它能正常工作了!。
可以对这段代码进行一些改进:

for (var i=1; i<=5; i++) {
 (function(j) {
 setTimeout( function timer() {
 console.log( j );
 }, j*1000 );
 })( i );
}

当然,这些 IIFE 也不过就是函数,因此我们可以将 i 传递进去,如果愿意的话可以将变量名定为 j,当然也可以还叫作 i。无论如何这段代码现在可以工作了。在迭代内使用 IIFE 会为每个迭代都生成一个新的作用域,使得延迟函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。问题解决啦!重返块作用域仔细思考我们对前面的解决方案的分析。我们使用 IIFE 在每次迭代时都创建一个新的作用域。换句话说,每次迭代我们都需要一个块作用域。 let 声明,可以用来劫持块作用域,并且在这个块作用域中声明一个变量。本质上这是将一个块转换成一个可以被关闭的作用域。因此,下面这些看起来很酷的代码就可以正常运行了:

for (var i=1; i<=5; i++) {
 let j = i; // 是的,闭包的块作用域!
 setTimeout( function timer() {
 console.log( j );
 }, j*1000 );
}

但是,这还不是全部!for 循环头部的 let 声明还会有一个特殊的行为。这个行为指出变量在循环过程中不止被声明一次,每次迭代都会声明。随后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。

for (let i=1; i<=5; i++) {
 setTimeout( function timer() {
 console.log( i );
 }, i*1000 );
}
  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值