JS 之 for 循环的那些事儿
新的一年开始,是投简历,找工作,跳槽,升职加薪的黄金时期,当然也要有高操的技能和扎实的基本功才能行。
今天我们就来聊一聊 JS 中for循环的那些事儿。
首先来看一个非常常见的面试题:
下列代码会打印出什么结果?
for (var i = 0; i < 10; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}
对于一个“行走江湖”多年的JS老鸟而言,这个问题真的是太基础、太入门、太没什么挑战性可言了,哪怕就是一个新手,答案也是张口就来:10个10。
没错,结果百分百正确。但是如果面试官问你为什么呢?可能JS基本功扎实的“老鸟”能给出相当让人们以的答复,但是对于一些“菜鸟”而言,可就没那么简单了。
下面就让我们一行行的分析一下这段小小的代码,彻彻底底的搞明白这个for循环到底是怎么执行的,怎么就一蹴而就的打印了:10个10的。
for(var i = 0;i<10;i++){
}
这段代码是创建循环体,var 声明一个临时变量 i ,赋值 = 0;i的值 小于10,每次执行i++;
setTimeout(() => {
console.log(i);
}, 1000);
这段代码是循环体内部的逻辑,调用setTimeout函数,没隔1000毫秒执行一次它的第一个参数(一个箭头函数或者说是匿名函数),在这个箭头函数内部执行打印语句。
好的,代码我们分析完毕了,下面我们来分析一下代码的执行逻辑。
for循环是一个同步执行的过程,也就是说,for循环体内部的业务逻辑会没有阻塞的一步步的执行下去;花括号{}包括的循环体是一个for循环作用域,var声明的变量i的作用域就是for循环体内部。
换言之,i在执行第一次循环的时候被创建,在堆内存中占有一个位置用来存放它的引用,赋初值 = 0,i的值被存放于栈内存中的某个地方;以后的每次循环执行,i这个变量的引用不会发生改变,改变(++操作)的只是栈内存中他的值而已(加了10次变成了10)。
好的,我们分析完了i的变化,下面我们再来看循环体内部的变化。
首先setTimeout函数是一个异步函数,它是在多少时间间隔之后执行某个操作的意思。每次(执行10次)执行setTimeout函数,都会往时间循环队列(Event loop queue)里push一个匿名(箭头)函数,函数的逻辑很简单,就是一行打印操作,打印的内容是i,这个i就是for循环体定义的那个i,同一个引用,指向同一块栈内存地址,对应着同样的一个值。
当同步的for循环执行完毕之后,调用堆栈(call stack)开始对事件循环队列执行出队操作,本着队列先进先出的原则,里面的匿名函数被一个个的出队执行,进行打印i,同一个引用,相同的值(每次++之后),所以10个匿名函数打印的结果都是10。
可能用了这么多的语言描述让人实在感到乏味,下面我们来看一张简单的图:
看完分析是不是觉得,这么一个小小的for循环其实内容还是挺丰富的,涉及到了var的声明变量,引用和值,作用域,堆/栈内存,事件循环队列,堆栈调用,setTimeout,同步/异步等。
想想看,如果被问及这个问题,如果你能细致入微的把这些都回答上来,是不是会让面试官耳目一新、眼前一亮,觉得你是一个不可多得的对JS有着很深刻研究的人才呢,不仅仅会给你的面试加分,甚至还有可能给你加薪呢!
当然这些都还只是我们一厢情愿而已了。说不定还有更变态的问题在后面,比如,面试官又问:
如果我想打印出0,1,2,3,……怎么做呢?
你可能想,这个问题我早就考虑到了,当下手写除了你认为完美的答案:
for (let i = 0; i < 10; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}
对于写惯了ES6的你来说,这样简单易行的改写还不是手到擒来嘛。面试官看了看,面无表情的点了点头说,嗯,可以是可以,但是为什么呢?
你心想,这就有点不按套路出牌了,给了你正确答案就行了,怎么一个劲儿的不停的问为什么呢?
或许你也会在面试之前扪心自问过:为什么呢?为什么只是单纯的把var改成了let,就可以顺序打印出0,1,2,3……了呢?
然后你不由自主的上网搜索,终于在权威的MDN上找到了答案。
let声明的变量只在其声明的块或子块中可用,这一点,与var相似。二者之间最主要的区别在于var声明的变量的作用域是整个封闭函数。 —— MDN
什么意思呢?
由于 let 声明的变量只在其块或者子块中起作用,也就可以理解为,这种for循环里会执行10次的let i;并且每一个 i 都有各自的内存空间地址,每个 i 都有其不同的值,且被绑定到了10个分别不同的匿名函数中,因此最终当事件循环队列里的匿名函数出队列执行时,打印的结果自然也就是其绑定的不同的 i 的值:0,1,2,3……了。
如果你还能把这个问题圆满的回答上来,估计面试官又会对你刮目相看三分了,当然还有更变态的会继续问你:
嗯,你回答的不错,那还有其他方式改写这段代码吗?
都到这份上了,你就干脆一下又把闭包的改写方法写了出来,并且又是细致入微的将闭包的原理给面试官讲了一遍,面试官估计在这个问题上是彻底的被你征服了。
好吧,碍于时间和主题划分问题,我们今天不讨论闭包的实现逻辑,只是把代码贴在这里了,有兴趣的小伙伴可以自行Google研究。等哪天有时间整理闭包的问题时,我们再把这个问题拿出来“鞭尸”!
//闭包实现
for (var i = 0; i < 10; i++) {
void function (j) {
setTimeout(() => {
console.log(j);
}, 1000)
}(i);
}