setTimeout函数之循环和闭包

setTimeout函数之循环和闭包

前言

之前对于setTimeout的一个经典问题的理解总是感到很迷惑,现在好像清晰一点了,所以把我的理解写下来,我对js的理解也不深入,如果有错误,请务必指出。以免误导其他看到这篇文章的小白。-.

先来点开胃菜

先看看这种很常见的问题吧:

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

上面这个例子来自《你不知道的JavaScript》,相信这种类似的问题也很常见,我最早见到这个例子是在TypeScript的文档里面,当时就不是很理解,对于输出的结果也就是强行记忆为“console.log(i)执行的时候i变为6了”,但对于这中间的大致流程却是十分模糊,以至于我当时错误的以为for循环和同步异步有什么关系。

正篇

先说下上面代码的运行结果:运行时会以每秒一次的频率输出五次6.
先抛开为什么结果是五次6这个问题,为什么这个频率会是每秒一次呢?可能大家刚开始的时候会有这种想法:“setTimeout函数的作用不是推迟执行里面的回调函数吗?那结果就应该是for循环第一次时延迟一秒输出1,然后是for循环第二次,延迟两秒输出2然后以此类推或者到最后i的值为6所以应该是以6秒为周期循环打印6?
这里就遇到了第一个坑,对setTimeout函数理解有偏差。

为什么是每秒一次呢?

SF来帮忙

这是我在segmentfault上看到的一个问题。原问题链接。请参考第二个回答。

setTimeout的延迟不是绝对精确的;
setTimeout的意思是传递一个函数,延迟一段时候把该函数添加到队列当中,并不是立即执行;
所以说如果当前正在运行的代码没有运行完,即使延迟的时间已经过完,该函数会等待到函数队列中前面所有的函数运行完毕之后才会运行;也就是说所有传递给setTimeout的回调方法都会在整个环境下的所有代码运行完毕之后执行;

观察下面的代码:

setTimeout(function(){
        console.log("here");
    }, 0);
    var i = 0;
    //具体数值根据你的计算机CPU来决定,达到延迟效果就好
    while (i < 3000000000) {
        i ++;
    }
    console.log("test");

试着将上面的代码运行了遍下,结果为在过了一段时间之后,先打印了test,然后才是here。而且需要注意的是,上面的代码写的是setTimeout(..,0),如果按照之前错误地将setTimeout函数理解为延迟一段时间执行,那这里把时间赋为0岂不是马上执行了?而实验结论则印证了上面"setTimeout的意思是传递一个函数,延迟一段时间把该函数添加到队列中,并不是立即执行“的结论。(涉及到线程,异步,事件循环的知识我现在理解得还不到位,所以暂且不表)

现在再来想想为什么是每秒一次

再回到最初的那个问题,刚进入for循环的时候,i为1,所以相对于现在延迟一秒将timer函数添加到队列当中,然后for循环还要继续啊,并没有等一秒再继续循环啊,然后进行第二次循环,这时候i为2,所以相对于现在延迟两秒将timer函数送进队列。以此类推。for循环的时间忽略不计的话,timer函数就以每秒一次的频率执行啦。

为什么每次都显示6呢?

这个问题我个人觉得与异步和闭包都有关系。
首先和异步的关系上文已经说了。

和闭包的关系

先要清楚,什么是闭包?过去我也把闭包和立即执行函数错误的混为一谈,看着立即执行函数表达式的括号我就天真地以为:用括号把函数包裹起来,这不就是”闭“包吗?

《你不知道的JavaScript》书中,对闭包的解释大概是这样的:对函数类型的值进行传递时,保留对它被声明的位置所处的作用域的引用。
也许上面这句话我总结得比较晦涩,但原书对这个问题解释得要清晰一些,可以看看原书47页。

那timer函数是在setTimeout函数中被声明的吧?在执行timer函数中的console.log(i)的时候,这个i是多少呢?在timer函数中没有i的声明啊。那就继续向外层的作用域找,终于在全局作用域下找到了i为多少了。

var的疑问

再来看看那个for循环,for(var i = 1; i <= 5; i++){...},在这里其实隐含着函数作用域和块作用域的的陷阱。在这段代码中用var声明的变量i的作用域在哪呢?是在当前作用域还是{}所包裹的内部呢?其实我们只要明确刚才这段代码相当于下面的代码就清除i的作用域在哪了。

var i;
for(i = 1; i <= 5; i++)

这就是每次的输出都是6的原因

所以,当timer函数第一次执行的时候,在执行console.log(i)的时候,这个时候的i其实是全局作用域下的i,这个时候循环是已经结束了,这时候i为6.(再次提醒不要错误地认为要等timer函数执行之后才会继续循环,再看看什么是异步);

那么问题来了

那么,怎么改动上面的代码让结果依次为1,2,3,4,5呢?最简单的办法就是将var改为let,原因是let创建了块作用域。(具体是怎么回事暂且不表,可以用babel将ES6转换为ES5查看结果。但是原理和下面要讲的类似)
所以,再想想为什么会每次的输出都是6呢?是因为每次执行到console.log(i)的时候这个i是全局作用域下的i啊,那怎么才能让这个i为每次循环时的i呢?即怎么才能在每次循环时”捕获“到i的副本呢

不要急,先来看看为什么可以用立即执行函数表达式。

所以下面的代码有用吗?

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

上面这个例子同样是来自《你不知道的JavaScript》。我以前错误地认为,立即执行函数表达式,这是立即执行啊,所以里面的timer也立即执行了,所以就能输出1,2,3,4,5了。
先说答案,这样当然是不行的,这里的立即执行也只是立即执行了setTimeout函数,而setTimeout函数的作用也就是将timer函数延迟一段时间添加到队列,所以这个立即执行表达式在这里有没有都一样。我之前错误的想法也是受到了”立即执行“这四个字的误导。先来看看一个正确答案:

for (var i = 1; i <= 5; i++) {
    (function() {
        var j = i;
        setTimeout( function timer() {
            console.log(j);
        },i*1000 ); //这一行将i*1000改为j*1000也行,并不影响
    })();
}

发现这个答案和上面的错误答案的区别了吗?其实我们是用立即执行函数表达式创造了新的函数作用域将timer函数包裹了起来,并用j捕获了每次循环时的i,这样在运行到console.log(j)的时候显示的就是每次循环时的i值啦。
同理还有这样的写法:

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

还有一些其他写法这里就不一一列举了,原理都是和作用域相关。其实上面这个涉及到let的例子和块作用域相关,这里就不展开了。

总结

异步决定了这段代码打印i的频率,闭包和作用域的知识决定了这个i是多少以及怎样改写这段代码。
总觉得这篇文章还有一些欠缺,希望大家能指正。 uuu

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容

  • 特别说明,为便于查阅,文章转自https://github.com/getify/You-Dont-Know-JS...
    杀破狼real阅读 462评论 0 0
  • 问题 一、什么是闭包? 有什么作用? 1.什么是闭包①JavaScript高级程序设计第三版定义闭包是指有权访问另...
    鸿鹄飞天阅读 432评论 0 0
  • 官方中文版原文链接 感谢社区中各位的大力支持,译者再次奉上一点点福利:阿里云产品券,享受所有官网优惠,并抽取幸运大...
    HetfieldJoe阅读 5,558评论 16 88
  • 以前看过好多文档,对于闭包不是很理解,再读《你不知道的JavaScript》上卷之后,终于明白了,感谢这本书,把自...
    晴風無眠阅读 390评论 0 1
  • 1 如何加入 第一步,回答五个问题 A 为什么要加入“爱吃”? B 目标收入是多少? C一天能保证几小时空闲时间发...
    1bf7bab8493a阅读 291评论 1 1