题目如下:
for(var i=0;i<5;i++){
setTimeout(function(){
console.log('b',new Date,i)
},1000)
}
console.log('a',new Date,i)
上述代码打印出什么?
因为涉及到执行时间,所以结果跟执行的时机有关,下面是我执行的结果:
a Sat May 26 2018 16:32:21 GMT+0800 (中国标准时间) 5
b Sat May 26 2018 16:32:22 GMT+0800 (中国标准时间) 5
b Sat May 26 2018 16:32:22 GMT+0800 (中国标准时间) 5
b Sat May 26 2018 16:32:22 GMT+0800 (中国标准时间) 5
b Sat May 26 2018 16:32:22 GMT+0800 (中国标准时间) 5
b Sat May 26 2018 16:32:22 GMT+0800 (中国标准时间) 5
那么从上面的结果开始我的js之旅。
理解基本概念
1 定时器
定时器的工作方式:在特定时间过去后将代码插入队列(代码队列)。
这句话怎么理解呢?首先要知道除了主js执行进程外,还有一个需要在进程下一次空闲时执行的代码队列。其次,插入到队列的代码不一定马上执行,而只能表示它会尽快执行。
所以面试题中的代码会在执行完for循环后,创建5个定时器对象,然后接着执行下面那行代码,这个时候i已经是5了,所以打印出a Sat May 26 2018 16:32:21 GMT+0800 (中国标准时间) 5
,而时间可能也就过去1毫秒。将定时器里面的代码加入队列的时机是从创建定时器开始计时的,经过1000毫秒后将它里面的代码插入到执行队列。由于上述定时器是在for循环中创建的,所以它们的执行代码几乎同时插入队列,而此时主进程又是空闲的,所以后面5条结果几乎同时输出。
除了setTimeout还有setInterval重复定时器,也就是每隔指定时间将代码插入队列,这会导致两个问题:(1)某些间隔会被跳过;(2)多个定时器代码执行之间的间隔可能会比预期的小,导致回调堆积。
解决方案:使用链式setTimeout调用,代码如下:
setTimeout(function(){
//处理中
setTimeout(arguments.callee,interval)
},interval)
此外,代码队列不仅可以插入定时器中的代码,还有事件处理函数(如用户点击了某个按钮,那么onClick对应的事件处理程序也会插入到队列中)、ajax回调函数等。
2 闭包
定时器的第一个参数是一个匿名函数,匿名函数里面引用了外部函数的变量i,这就形成了闭包。其实就是匿名函数的作用域链引用了外部函数的变量对象。关于怎么理解函数执行环境、作用域链、变量对象可以参考这篇博文:js作用域内存模型和图解对象内存模型。由于匿名函数执行时,外部函数的变量对象i的值已经是5了,所以后续5条结果中i的值都是5。
那么这个时候面试官可能会继续追问:有什么办法能让i的值按for循环遍历时的值打印输出呢?
答案是再创建一层内部函数实现对i的闭包。代码如下:
for(var i=0;i<5;i++){
var g = function(){
setTimeout(function(){
console.log('b',new Date,i)
},1000)
};
g(i);
}
或者
for(var i=0;i<5;i++){
(function(){
setTimeout(function(){
console.log('b',new Date,i)
},1000)
})(i);
}
3 new Date = new Date()
Date是js的引用类型,创建一个日期对象,使用new操作符和调用的Date的构造函数即可,如果不传参,新创建的对象默认获得当前日期和时间。不传参的时候可以不用后面的括号,但一般不建议将括号去掉。
总结
1 js中异步执行代码的方式有:定时器、事件处理函数、ajax
2 闭包会维持父级作用域的变量对象,所以会占用多一些内存,使用需慎重。