一道面试题引发的文章:
用setTimeout实现setInterval
var mySetInterval = function(func, duration){
function interval(){
setTimeout(interval, duration);
func()
}
setTimeout(interval, duration);
}
为什么要用setTimeout实现setInterval?
在日常编码中setTimeout和setInterval的延迟时间经常不准确, 这是因为二者不是立即执行的, JS是单线程, 有一个事件队列机制
setTimeout是延迟delay毫秒后, 将回调函数加入事件队列, 事件什么时候执行到此处不一定, 所以会有延迟;如果delay为0, 就代表立刻插入到事件队列
setInterval是延迟delay毫秒后, 看看事件队列中是否存在还没有执行的回调函数, 如果还存在, 就不要再往事件队列中添加回调函数了.
再次强调,定时器指定的时间间隔,表示的是何时将定时器的代码添加到消息队列,而不是何时执行代码。所以真正何时执行代码的时间是不能保证的,取决于何时被主线程的事件循环取到,并执行。
因此setTimeout是确定可以向事件队列添加事件的, 但是setInterval不确定.
一道经典的面试题, 输出是什么
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
众所周知, 答案是在1s后同时输出5个5;
因为for循环先执行完(宏任务, 不知道的去 事件循环机制), i = 5; 1s后5个setTimeout同时被添加宏任务队列,由于没有其他的宏任务和微任务, 此时, 5个5被同时打印出来.
比如:
var testSetTimeout = function(){
for(var i = 0;i < 5;i++){
setTimeout(() => {
console.log(i);
}, 1000);
}
console.log(11111);
}
testSetTimeout()
就会先打印出11111, 一秒后打印出5个5
若想依次打印出0,1,2,3,4的解决办法呢?
依次打印出0,1,2,3,4的解决办法
解决办法一: 立即执行函数
for (var i = 0; i < 5; i++) {
(function(i){ //立即执行函数
setTimeout(function (){
console.log(i);
},1000);
})(i);
}
那你有没有想过, 为什么立即执行函数中的setTimeout也是一秒后统一发送到事件队列中,为什么它就可以获取到每次循环的变量呢? 这是因为立即执行函数会创建一个独立的作用域,来保存每次循环的变量, setTimeout中的函数被调用时, 会去找自己的作用域链, 因此才能获取到循环当次的变量.
第二种方法, let
很多人都知道, 最简单的方法就是将var改为let
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
那原因是什么呢? 下一个问题
在for循环中var和let的作用域究竟是什么样子的?
在for循环中, 执行顺序是这样的
设置循环变量(var i = 0) -> 循环判断(i<3) -> 满足执行循环体 -> 循环变量自增(i++)
所以一个循环可以被解析成下面这种格式(以3个为例), 最后调用函数的时候, i的值统一已经变成3了
{
//我是父作用域
var i = 0;
if (0 < 3) {
setTimeout(function(){
console.log(i)
},1000)
};
i++; //为1
if (1 < 3) {
setTimeout(function(){
console.log(i)
},1000)
};
i++; //为2
if (2 < 3) {
setTimeout(function(){
console.log(i)
},1000)
};
i++; //为3
// 跳出循环
}
但是如果将上面代码的var改成let,也是只声明一次, 为什么就相互独立了呢?
这篇博客讲的很深入 有兴趣可以看看
简单说, 就是ES6中, let也只是被声明了一次, 但是每次进入循环内, 都相当于进入了一个新的作用域. 函数会拷贝父作用域到[[scoped]]属性上, 此时由于i是基本数据类型, 所以被直接拷贝进来, 每次函数被执行时, 会将每个函数的[[scoped]]属性放到当前执行环境的作用域链上, 因此输出的是不同的值
如果将let i 中的i变成对象 let i = {y: 1}, 就会发现, 输出的不是依次为0,1,2,3,4 了
for(let y = {i: 0};y.i < 5;y.i++){
setTimeout(() => {
console.log(y.i);
}, 1000);
}
这样, 输出的就是1s后一次性输出5个5了, 因为y={i:0}
是对象, 每次传进函数作用域的都是一个对象的引用, 因此最后取的都是同一个对象.
那么, 立即执行函数呢? 是不是也是这样, 因此, 笔者又去试了下用立即执行函数的方法. 你猜猜结果是什么?
for(var y = {i: 0};y.i < 5;y.i++){
(function(y){
setTimeout(() => {
console.log(y.i);
}, 1000);
})(y)
}
哈哈哈, 果不其然, 也是一秒后一次性输出5个5
如果传给立即执行函数的参数是i
for(var y = {i: 0};y.i < 5;y.i++){
(function(i){
setTimeout(() => {
console.log(i);
}, 1000);
})(y.i)
}
这个结果就是1s后输出0,1,2,3,4
你懂了吗?
最后
感谢你能看到这, 我知道向上爬的人很多, 但是愿意向下扎根的人很少.
如果你能看到这, 祝福你~~