揭开 for 循环背后的神秘面纱(烧脑呀)

509 阅读2分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

感谢 Jake 和 Surma 分享视频的《JavaScript for-loops are… complicated - HTTP203》因为个人技术和英语理解的局限性可能会出入,如果大家对下面难题有好的解释希望大家踊跃讨论。

for (var i = 0; i < 2; i++) {
    console.log(i)
}

今天的分享有点类似猜谜游戏。今天主要想谈一谈在引擎中,循环系统(for)究竟是如何工作的。从表面上看, for 循环有三条指令,初始化、条件和增量器,首先初始化 i 仅一次,然后判断 i 是否满足条件 i < 2 如果满足条件就退出,接下来运行循环体, console.log(i)然后运行 i++ ,在回到条件语句,答案很显然

0
1

看到这里大家可能会想这是不是太简单了,也就意味着今天将空手而归。因为循环真的非常、非常简单。接下来我们来看一个有点难的,虽然难但是对于稍有经验前端程序员是可以脱口而出给出正确答案的。

for (var i = 0; i < 2; i++) {
    setTimeout(()=>console.log(i))
}

setTimeout 虽然没有设置时间,或者设置一个非常小延迟时间,其实浏览器默认一个最小延迟时间是 4 毫秒。我们将打印隐藏于一个闭包之中,大家可以思考一个给出答案,有时候我们能够给出答案,并不意味你真正弄懂其中奥妙。

首先这里只有有一个 i 如果有父级函数 i 变量作用域就是其父级函数,如果没有就是全局全局函数,所以当console.log 时,i 已经变为了 2 说以输出 2 次 2,这里需要弄懂为什么输出 2 次,为什么两次的结果都是 2 呢。

继续我们将 var 改成 let 了解是 let 朋友应该很快给出答案,不过还不全面

for (let i = 0; i < 2; i++) {
    setTimeout(()=>console.log(i),0)
}

这我们列出下面代码,当我们在外部访问在块作用域声明的 innerConst 应该会抛出异常 ReferenceError: innerConst is not defined

if(true){
    var innerVar = 1;
    const innerConst = 1;
    let innerLet = 1;
}

console.log(innerVar);
console.log(innerConst);//ReferenceError: innerConst is not defined
console.log(innerLet);//innerLet is not defined

上面例子我们已经注意到了 let 声明的变量是受地理限制的,也就是有其活动范围,也就是其生命周期受到地理的限制,不过上面 for 循环例子我们用这个经验却无法解释通。

ECMAScript 规范实际上对这种情况进行了特殊处理。这个 let 语句看起来是在 for 的块之外生效了,看起来神奇。

每个迭代创建了一个词法作用域。所以实际上是一个新的 i 变量,不仅是给同一个变量分配一个新的值,而是一个新的变量。

let i = 0
i < 2
setTimeout(console.log(i))
i++;

通常我们认为是每次迭代这是这个顺序,其实不然。

for (let i = 0; i < 2; i++) {
    setTimeout(()=>console.log(i))\\1
    i++;
}

也就是首先 let i=0 然后条件判断语句 i < 2,如果这时运行了 setTimeout(()=>console.log(i)) 就会输出 0 不过并不是 0 而是 1。 这就是奇怪的事情。增量器 i++ 在每个迭代的开始都运行,除了第一个迭代。

这里创建了变量 let i = 0 在条件判断前创建了一个新的词法环境,将 i 的值复制到其中。然后现在 i = 0 然后 i++ 语句将更新为 1 ,下一次迭代。工作原理很像 var。但是,它在做许多跳跃性的工作,

for (const i = 0; i < 3; i++) {
    setTimeout(()=>console.log(i))
    i++;
}
TypeError: Assignment to constant variable.