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的同学来说, 会比较陌生, 有些难以理解, 理解起来非常模糊. 今天就和大家一起来探讨一下这个玩意. 相信大家在看完后, 心中的迷惑会迎然而解. 闭包概念: 闭包就是有权 ...
随机推荐
- Java历程-初学篇 Day09 冒泡排序
冒泡排序 冒泡排序(Bubble Sort)是一种简单的排序算法.它重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来.走访数列的工作是重复地进行直到没有再需要交换,也就是 ...
- rem绝对自适应方案
rem css3新增的rem是现在非常受欢迎的单位.看一下MDN上的说明: 这个单位代表根元素的 font-size 大小(例如 <html> 元素的font-size). 使用这个单位可 ...
- FPGA IN 金融领域
何为金融: 金融指货币的发行.流通和回笼,贷款的发放和收回,存款的存入和提取,汇兑的往来等经济活动.金融(FIN)就是对现有资源进行重新整合之后,实现价值和利润的等效流通. 金融主要包括银行.证券.基 ...
- 从实践的角度理解cookie的几个属性
cookie的处理流程大致分为以下几步: 1.浏览器初次请求服务器. 2.服务器认为有必要设置cookie,通过响应报文首部:Set-Cookie告知浏览器,cookie的内容. 3.浏览器本地保存( ...
- java.security.InvalidKeyException: Illegal key size
今天遇到一个奇怪的问题. 自己做的加签验签功能已经没有问题了,本地测试通过,同事放到服务器上测试也没问题. 然后我将包放到自己搭建的环境上,会报这样一个错误: java.security.Invali ...
- win7下安装Ubuntu后进不去win7的解决方法
win7下安装Ubuntu后进不去win7的解决方法 刚刚给同学在win7下安装了Ubuntu16.04,结果在安装完后竟然无法在电脑重启后,找到win7的进入选项. 在网上找了找,都不行!就差点重装 ...
- Python基础3切片,字符串的方法
切片:截取字符串某一段字符,并不改变原字符串.结构:[起始位置:终止位置:步长] 但不包括终止位置.所谓:顾头不顾尾 索引:序列中每个元素都是有编号的,都是从0开始编号的.使用负数索引时,Pytho ...
- 【持续更新】.Net 开发中给自己埋下的坑!
1.文件“XXX”正在由另一进程使用,因此该进程无法访问此文件. 原因剖析:文件在主线程操作,在子线程中读写操作文件,刚开始没有意识到程序的问题所在,总是在FileStream中报错,google后常 ...
- MySQL比like语句更高效的写法locate position instr find_in_set
使用内部函数instr,可代替传统的like方式查询,并且速度更快. instr函数,第一个参数是字段,第二个参数是要查询的串,返回串的位置,第一个是1,如果没找到就是0. 例如, select na ...
- Java基础总结--异常处理机制
----异常的概述-----1.异常,就是不正常的现象,可能发生在编译期间也可能发生在运行期间2.可能会出现不同的异常,进而在Java中对其描述封装为类--在这些异常类中抽取其共性的东西(异常发生的位 ...