一道 JavaScript 面试题涵盖 3 个主题
编程面试很难。面试时现场写代码更是难上加难。
我感觉,只要我得在别人面前敲代码myIntelligence -= 10;
,我现在的公司首席开发人员就会定期面试潜在的新候选人。公司为 JavaScript 开发人员准备了一些面试题,但几乎总是会问到这样一个问题:
// what will be logged in the console
// and how to fix it to log 0, 1, 2??
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
这是一个很典型的问题。
它有什么特别之处呢?
嗯,在我看来,这五行代码触及了 JavaScript 的三个有趣方面。
- var、let 和 const
- 闭包
- 事件循环
让我们分解一下,看看这里发生了什么。
var let const
ES6 引入了新的变量赋值关键字:const
和let
。你可能已经知道它们是块级作用域的,并且var
是函数级作用域的。
以下是一些简单的示例来说明这种行为。
// simplest JavaScript example in the world:
{
var name = 'maciek';
let surname = 'chmura';
}
console.log(name); // -> maciek
console.log(surname); // -> surname is not defined
// what's happening under the hood:
var name;
{
let surname;
name = 'maciek';
surname = 'chmura';
}
console.log(name);
console.log(surname);
好的,让我们将其放在 for 循环的上下文中。
for (var i = 0; i < 3; i++) {
console.log(i); // -> 0 1 2
}
// what will happen when var is changed to let?
for (let j = 0; j < 3; j++) {
console.log(j); // -> 0 1 2
}
两个循环都生成正确的输出。但方式略有不同。var
“跳转”到循环,global scope
并“跳入”循环,并在每次迭代时进行初始化。 可以这样说明:let
// var lives here
for (var i = 0; i < 3; i++) {
console.log(i); // -> 0 1 2
}
console.log(i); // -> 3
for (let j = 0; j < 3; j++) {
// let is available only from here
console.log(j); // -> 0 1 2
}
console.log(j); // ReferenceError: j is not defined
好的,非常简单...这就是块作用域的工作方式...继续。
闭包
JavaScript 闭包的神秘世界。
闭包的原始定义是什么?
可以去MDN查看。
闭包是函数和声明该函数的词法环境的组合。
请深入阅读 MDN 的这篇文章。这个知识库的贡献者非常聪明,让我们相信他们 :)
- 这到底是什么
lexical environment
? - 它会在某个时候消失吗?
- 谁、何时决定?
- 我该如何控制它?
很长一段时间我都无法理解。
直到我添加了两个视觉辅助工具来帮助我理解。
- 🎒 背包。我喜欢把闭包想象成函数的背包。当一个函数被定义时,它会把所有未来可能需要的值都添加到背包里。
- 🚚 垃圾收集器。一辆清理旧代码的卡车。与 C 语言不同,你无需执行任何操作
malloc()
,free()
它会被自动处理。
当某个函数执行完毕并返回一个值时,我们可以安全地从内存中移除此函数定义🚚🗑。对于不再可访问的值,也是如此。
当函数返回一个函数时,事情会变得有趣。
我不想重新发明新的例子和定义,所以我只会添加一些可视化的帮助。MDN
示例(带行号):
function makeFunc() { // 1
var name = 'Mozilla'; // 2
function displayName() { // 3
alert(name); // 4
} // 5
return displayName; // 6
} // 7
// 8
var myFunc = makeFunc(); // 9
myFunc(); // 10
让我们想象一个简化的 JavaScript 解释器工作流程。JavaScript 运行时在运行代码时在“思考”什么。
- (第 1 行)
makeFunc
函数定义,继续。 - (9)声明
myFunc
变量并将运行结果赋给它makeFunc
,执行makeFunc
- (1)跳入
makeFunc
定义。 - (2)好的,一个
name
值为的变量Mozilla
。 - (3)
displayName
函数定义,继续。 - (4)
return
displayName
函数定义
第一个情节转折。这里返回了完整的函数定义。 的末尾没有 () displayName
。
第二个情节转折。观察到一个闭包。Where?displayName
将其放入其 🎒 中var name
(它在 的词法范围内displayName
)。
makeFunc
执行并返回了 的整个函数定义displayName
及其闭包(a 🎒),该闭包保存了对 中值的引用name
。
垃圾收集器无法从内存中删除第 1 行到第 7 行,因为将来某个时候myFunc
可能会执行这些代码,到时候displayName
就需要用到它们的闭包了。
- (10)执行
myFunc
这就是我理解的闭包。
现在我明白了!
让我们进入谜题的最后一部分。
事件循环
学习事件循环的最佳方式莫过于 Philip Roberts 在 JSConf EU 上的精彩演讲。
快来观看吧……
🤯 是不是有点不可思议?
好!最后,了解了所有知识之后,我们来分析一下这道面试题到底讲了什么。
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
随着循环的每次迭代,setTimeout
将 发送console.log(i)
到 Web API 的函数并开始倒计时。
与此同时,我们将继续循环。另一个console.log(i)
将被推送到 Web API,依此类推......
循环完成执行。调用堆栈为空。
在 Web API 中,1 秒后console.log(i)
被推送到回调队列。然后又一个,又一个。
由于调用堆栈为空,回调队列可以将其第一个元素推送到调用堆栈以执行它。
因此第一个console.log(i)
执行。
它查找i
。
的值是多少i
?
它是 3。从全局范围来看。
为什么? 循环
完成迭代并i
在最后将 更新为 3。var是函数作用域(for 循环不是函数),并且被提升到循环之外。 调用堆栈再次为空。 第二个移至调用堆栈。 的值是多少?它又是 3。它是相同的值。i
global scope
console.log(i)
i
如何修复它以记录 0、1、2?
一种修复方法是将其更改var
为let
。
现在,在循环中,每个 都i
被初始化并赋值给当前迭代的值,然后放入将记录它的函数的闭包(一个🎒)中。1
秒后,当调用堆栈为空时,回调队列会将带有console.log(i)
及其闭包值的函数i
推回调用堆栈并执行它。0、1、2
将分别被记录。
完成。
请问下一个问题。
现在,当您知道到底发生了什么事情时,还能做些什么来解决它呢?
免责声明:
我写这篇文章主要是为了自己研究这些主题。如果有什么错误,请在评论中指出,以便我们大家共同学习 :)