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是多少以及怎样改写这段代码。
总觉得这篇文章还有一些欠缺,希望大家能指正。


js经典闭包的更多相关文章

  1. js经典试题之闭包

    js经典试题之闭包 1:以下代码输出的结果是? function Foo(){ var i=0; return function(){ document.write(i++); } } var f1= ...

  2. 「JavaScript」同步、异步、回调执行顺序之经典闭包setTimeout分析

    聊聊同步.异步和回调 同步,异步,回调,我们傻傻分不清楚, 有一天,你找到公司刚来的程序员小T,跟他说:“我们要加个需求,你放下手里的事情优先支持,我会一直等你做完再离开”.小T微笑着答应了,眼角却滑 ...

  3. 理解运用JS的闭包、高阶函数、柯里化

    JS的闭包,是一个谈论得比较多的话题了,不过细细想来,有些人还是理不清闭包的概念定义以及相关的特性. 这里就整理一些,做个总结. 一.闭包 1. 闭包的概念 闭包与执行上下文.环境.作用域息息相关 执 ...

  4. 同步、异步、回调执行顺序之经典闭包setTimeout分析

    聊聊同步.异步和回调 同步,异步,回调,我们傻傻分不清楚, 有一天,你找到公司刚来的程序员小T,跟他说:“我们要加个需求,你放下手里的事情优先支持,我会一直等你做完再离开”.小T微笑着答应了,眼角却滑 ...

  5. JS的闭包、高阶函数、柯里化

    本文原链接:https://cloud.tencent.com/developer/article/1326958 https://cloud.tencent.com/developer/articl ...

  6. 关于js中闭包的理解

    1.以前很不理解js中闭包的概念及使用,下面来看一下 function foo() { var a = 123; var b = 456; return function () { return a; ...

  7. Js经典相册

    Js经典相册 点击下载

  8. js的闭包

    一,关于js闭包的只是感觉很高大上似乎,对于学弱来说任何问题都是这样的,值得去钻研和提高. 资料上理解的都是关于js的闭包其实就是js的变量的作用域的灵活使用. 函数内部定义变量的时候,一定要用 va ...

  9. 彻底搞清js中闭包(Closure)的概念

    js中闭包这个概念对于初学js的同学来说, 会比较陌生, 有些难以理解, 理解起来非常模糊. 今天就和大家一起来探讨一下这个玩意. 相信大家在看完后, 心中的迷惑会迎然而解. 闭包概念: 闭包就是有权 ...

随机推荐

  1. SQL语句表名或者字段名和保留字冲突解决方法

    最近开发遇到一个很奇葩的问题,简单做一下笔记 select * from Add ... 以上SQL语句会报错. 原因Add是表名,SQL语句保留字中又有Add 解决方法: select * from ...

  2. Python自学笔记-sorted()函数(来自廖雪峰的官网Python3)

    感觉廖雪峰的官网http://www.liaoxuefeng.com/里面的教程不错,所以学习一下,把需要复习的摘抄一下. 以下内容主要为了自己复习用,详细内容请登录廖雪峰的官网查看. 排序算法 排序 ...

  3. ZOJ-3933-Team Formation【二分图最佳匹配】【KM】

    http://blog.csdn.net/loy_184548/article/details/51154195    一开始对不同组合得不同分数(mm1,mg2,gg3),想用sap来写,但是保证了 ...

  4. 用C#实现字符串相似度算法(编辑距离算法 Levenshtein Distance)

    在搞验证码识别的时候需要比较字符代码的相似度用到"编辑距离算法",关于原理和C#实现做个记录. 据百度百科介绍: 编辑距离,又称Levenshtein距离(也叫做Edit Dist ...

  5. asp.net中利用Jquery+Ajax+Json实现无刷新分页(二)

    <%@ Page Language="C#" AutoEventWireup="true" CodeFile="PageTest.aspx.cs ...

  6. Struts2 02--通配符

       在以前没有使用Struts时,web与前台的数据交互通过Servlet+jsp页面.一个增删改查往往需要写四个Servlet来处理数据:在使用struts之后,Servlet不再被使用,而是通过 ...

  7. 解题笔记-洛谷-P1010 幂次方

    0 题面 题目描述 任何一个正整数都可以用2的幂次方表示.例如 137=2^7+2^3+2^0 同时约定方次用括号来表示,即a^b 可表示为a(b). 由此可知,137可表示为: 2(7)+2(3)+ ...

  8. ROS命令

    rospack find [package_name]功能:获取软件包的路径 例:运行 rospack find roscpp ,会返回 /opt/ros/indigo/share/roscppros ...

  9. 新型勒索软件Magniber正瞄准韩国、亚太地区开展攻击

    近期,有国外研究人员发现了一种新型的勒索软件,并将其命名为Magniber,值得注意的是,这款勒索软只针对韩国及亚太地区的用户开展攻击.该勒索软件是基于Magnitude exploit kit(简称 ...

  10. Spring 3 AOP 概念及完整示例

    AOP概念 AOP(Aspect Oriented Programming),即面向切面编程(也叫面向方面编程,面向方法编程).其主要作用是,在不修改源代码的情况下给某个或者一组操作添加额外的功能.像 ...