面试时,面试官给了你一段代码:
var arr = []
for (var i = 0; i < 10; i++) {
arr[i] = function() {
console.log(i);
}
}
for (var j = 0; j < arr.length; j++) {
arr[j]()
}
问:输出什么?
这个时候,你还以为面试官太仁慈,你太爱了,心里喜滋滋,爽快的回答,0,1,2,3,4,5,6,7,8,9。殊不知,这是面试官给你挖的坑,结果你还高兴的跳了进去,凉凉。
而有些人很警觉,看着不对劲,仔细思考了下,认真回答:10个‘10’,面试官点点头,你心里一松,顿时感到舒畅,自信心起来了,还没缓口气,面试官接着提问,为什么?这个时候你懵了,你心里知道,但是不知道怎么表达,然后说了堆乱七八糟的东西,结果没get到点,回答完后,气氛尴尬起来了,你恨不得找个洞钻进去,因为你也不知道你刚才说的是什么东西。
不过没关系,看完这篇文章后,以后遇到这种情况就不会陷入尴尬的场景了。
for循环无法形成作用域,var 声明的变量 i 在整个函数作用域内都是可见的,包括循环和循环外的部分。因此,当第一个for循环结束后,i 的值变为 10,而函数只是声明赋值了,并没调用,也就是说还没有执行,并且所有的函数共享这个 i 变量。当执行第二个for循环时才执行这些函数,此时全局作用域中的 i 已经是为 10,因此每个函数都输出 10。
因为你看过这篇文章,所以你轻松流畅的解释完,但你以为就到此结束了吗?
面试官问:怎样改才能输出0到9?
还好,你很快的说出把var改为let,并顺便把原因说了:
var 声明的变量具有函数作用域,而不是块作用域。这意味着在使用 var 声明的变量在整个函数范围内都是可见的,而不仅仅是在声明它们的块内部。在原始的代码中,for 循环中的 i 变量实际上是在外部函数作用域中声明的,因此对所有的循环迭代都是可见的。
在循环的每一次迭代中,i 的值都会递增,最终达到 10。当循环结束后,所有通过闭包引用的函数都会共享相同的 i 变量,这个变量的值已经是循环结束时的最终值 10。因此,无论调用哪个函数,它们都会输出 i 的最终值,即 10。
而使用 let 声明的变量具有块作用域,每次迭代会创建一个新的 i 变量,因此每个闭包捕获的是不同的 i 值,而不是共享同一个 i 变量。
你简直是个天才,回答的太完美了,但此时面试官还不愿意放过你,问:还有其他方法吗?
此时,你有点汗流浃背了,CPU快速运转,灵机一动,你想到了闭包,于是你这样说:
可以使用闭包来创建函数的副本,以便在每次迭代中捕获 i 的当前值。例如,可以使用立即执行函数表达式(IIFE)来创建闭包,如下所示:
var arr = []
for (var i = 0; i < 10; i++) {
arr[i] = (function(num) {
return function() {
console.log(num);
};
})(i);
}
for (var j = 0; j < arr.length; j++) {
arr[j]();
}
在这个修正后的版本中,每个函数都有自己的闭包,捕获了在循环中当前迭代的 i 的值。
面试官听完后,说了句话:“可以。”
你终于松了口气,再也不敢小看任何一道提问。
你感叹道:小小的一段代码,包含了作用域,作用域链,闭包等难点,果然不可小觑啊!
如果你不了解上述知识点,可以先看这两篇文章:
加深理解!谢谢。