js经典闭包
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经典闭包的更多相关文章
- js经典试题之闭包
js经典试题之闭包 1:以下代码输出的结果是? function Foo(){ var i=0; return function(){ document.write(i++); } } var f1= ...
- 「JavaScript」同步、异步、回调执行顺序之经典闭包setTimeout分析
聊聊同步.异步和回调 同步,异步,回调,我们傻傻分不清楚, 有一天,你找到公司刚来的程序员小T,跟他说:“我们要加个需求,你放下手里的事情优先支持,我会一直等你做完再离开”.小T微笑着答应了,眼角却滑 ...
- 理解运用JS的闭包、高阶函数、柯里化
JS的闭包,是一个谈论得比较多的话题了,不过细细想来,有些人还是理不清闭包的概念定义以及相关的特性. 这里就整理一些,做个总结. 一.闭包 1. 闭包的概念 闭包与执行上下文.环境.作用域息息相关 执 ...
- 同步、异步、回调执行顺序之经典闭包setTimeout分析
聊聊同步.异步和回调 同步,异步,回调,我们傻傻分不清楚, 有一天,你找到公司刚来的程序员小T,跟他说:“我们要加个需求,你放下手里的事情优先支持,我会一直等你做完再离开”.小T微笑着答应了,眼角却滑 ...
- JS的闭包、高阶函数、柯里化
本文原链接:https://cloud.tencent.com/developer/article/1326958 https://cloud.tencent.com/developer/articl ...
- 关于js中闭包的理解
1.以前很不理解js中闭包的概念及使用,下面来看一下 function foo() { var a = 123; var b = 456; return function () { return a; ...
- Js经典相册
Js经典相册 点击下载
- js的闭包
一,关于js闭包的只是感觉很高大上似乎,对于学弱来说任何问题都是这样的,值得去钻研和提高. 资料上理解的都是关于js的闭包其实就是js的变量的作用域的灵活使用. 函数内部定义变量的时候,一定要用 va ...
- 彻底搞清js中闭包(Closure)的概念
js中闭包这个概念对于初学js的同学来说, 会比较陌生, 有些难以理解, 理解起来非常模糊. 今天就和大家一起来探讨一下这个玩意. 相信大家在看完后, 心中的迷惑会迎然而解. 闭包概念: 闭包就是有权 ...
随机推荐
- 在ASP.NET Core中如何支持每个租户数据存储策略的数据库
在ASP.NET Core中如何支持每个租户数据存储策略的数据库 不定时更新翻译系列,此系列更新毫无时间规律,文笔菜翻译菜求各位看官老爷们轻喷,如觉得我翻译有问题请挪步原博客地址 本博文翻译自: ht ...
- 技术领导(Technical Leader)画像
程序员都讨厌被管理,而乐于被领导.管理的角色由PM(project manager)扮演,具体来说,PM负责提需求.改改改.大多数情况,PM是不懂技术的,这也是程序员觉得PM难以沟通的原因.而后者由技 ...
- C# JAVA成员访问修饰符比较
在面向对象的访问修饰符中常用的有public ,private ,protected C# 访问修饰符: private < protected internal < internal/p ...
- web应用程序 前段部分调优
1. 使用瀑布图初步诊断网站性能瓶颈 一般来说,打开一个网页的速度会受到以下几项的影响: 1) 服务器花了太长的时间将.aspx页面的内容转化为html. 2) .aspx页面花了太长的时间从服务器端 ...
- 替代PhotoShop:GIMP图形编辑器的使用
GIMP最早是linux环境下用于图形编辑的一款开源软件,目前的功能很已经很丰富,如果使用得当,在很多的图形编辑操作上完全可以替代收费的Photoshop(PS).目前GIMP已经发展成了多平台的开源 ...
- MIRO做发票校验时实现替代功能的多种方式
http://blog.sina.com.cn/s/blog_3f2c03e30100ngje.html MIRO做发票校验时,如果需要对产生的会计凭证做某些字段的替代,可有多种方法. 1.GGB1替 ...
- Mysql [Err] 1293 there can be only one TIMESTAMP column with CURRENT_TIMESTAMP in DEFAULT or ON UPDATE clause
问题: mysql数据 导入数据出错 [Err] 1293 - Incorrect table definition; there can be only one TIMESTAMP column w ...
- 转载:C#特性-表达式树
原文地址:http://www.cnblogs.com/tianfan/ 表达式树基础 刚接触LINQ的人往往觉得表达式树很不容易理解.通过这篇文章我希望大家看到它其实并不像想象中那么难.您只要有普通 ...
- linux 下查找图片文件方法
通常是通过文件后缀名查找图片文件,如果没有文件后缀的图片或者伪造的图片文件,则这种判定方法将达不到要求.我们可以根据读取文件头进行图片文件类型的判定. 比较流行的图片文件类型有:jpg png bmp ...
- Java 集合框架之set用法
Java 集合框架之set 一个简单的例子 创建一个Customer类,类中的属性有姓名(name).年龄(age).性别(gender),每个属性分别有get/set 方法.然后创建两个Custom ...