这是我参与8月更文挑战的第11天,活动详情查看8月更文挑战
前言
上次,我们通过这篇文章JavaScript 的闭包,可以这样理解,让大家对闭包有了一个不一样的理解。今天我们从一道经典面试题出发,剖析 JavaSctipt 闭包面试考什么。
知己知彼百战百胜
“循环体与闭包”系列
把闭包和循环体结合起来考察,是闭包最为经典的一种命题方式。
熟悉但有可能答错的题
我们来看一下这个代码
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
console.log(i);
请问:上面代码输出的结果是什么?可以将自己的答案记下来,继续往下看
第一种答案:
0 1 2 3 4 5
为什么会想到这样的答案呢?一般,是刚入门的新手,不熟悉 setTimeout 的用法和作用,导致对代码实际执行的顺序理解错误。
第二种答案:
5 0 1 2 3 4
首先给出这种答案的同学,说明他是对 setTimeout 的用法和作用有相关了解,但是对于作用域、闭包等相关知识不太熟悉,认为会输出 0 1 2 3 4。
正确答案:
5 5 5 5 5 5
为什么是这个答案呢? 首先,最后一行的打印,输出 5,大家基本都是知道的,其实就是同步和异步的区别; 其次,大家也知道 setTimeout 函数会被推迟执行; 最终,我们来看看为什么 setTimeout 打印的结果都是 5。
最简单的理解 因为 for 循环是很快的,当执行完 for 循环,变量 i 已经变成了 5,for 循环里面的 setTimeout 执行了 5 次,每次都会将这个函数的执行推迟 1000 ms,然后打印变量 i,打印的都是 5。
更深入的解答 先来个文字版
当下面这个函数被调用的时候,使用到变量 i,但是当前作用域没有变量 i,那么这个时候,JS 引擎探出头去,去上层作用域(全局作用域)找变量 i。此时 for 循环早已经执行完毕,i 的值,已经是 5。
当 1000ms 后,这个函数第一次被执行的时候,打印的变量 i 的已经是 5。
function() {
console.log(i);
}
我们来画图加深理解
作用域关系示意如下:
作用域链关系展示如下:
我们结合文字版和图片,能够分析出,每隔 1000 ms,setTimeout 回调被执行的时候,都是去全局作用域,拿变量 i,所以每一次输出的结果都是 5。
改造代码,输出目标结果
现在的输出结果:
5 5 5 5 5 5
期望的目标结果:
5 0 1 2 3 4
思路1:setTimeout 的第三个参数
说明:setTimeout 从第三个入参位置开始往后,是可以传入无数个参数的。这些参数会作为回调函数的附加参数存在。
将每一轮循环的 i 的值,存储到 setTimeout 的第三个参数,回调函数的形参 j,会接收到每一轮循环的 i,所以可以输出正确结果。
for (var i = 0; i < 5; i++) {
setTimeout(function(j) {
console.log(j);
}, 1000, i);
}
console.log(i);
思路2:外部函数入参
在 setTimeout 外面再套一层函数,利用这个外部函数的入参来缓存每一个循环中的 i 值:
var output = function (i) {
setTimeout(function() {
console.log(i);
}, 1000);
};
for (var i = 0; i < 5; i++) {
// 这里的 i 被赋值给了 output 作用域内的变量 i
output(i);
}
我们看图,加深理解
思路3:立即执行函数入参
第二种思路,需要单独声明一个函数,多声明一个全局变量,我们可以改成立即执行函数,同样也是利用函数的入参来缓存每一个循环中的 i 的值
注:变量 j,其实是立即执行函数的局部变量,为了不让大家混淆实参 i,所以改成 j
for (var i = 0; i < 5; i++) {
// 这里的 i 被赋值给了立即执行函数作用域内的变量 j
(function(j) {
setTimeout(function() {
console.log(j);
}, 1000);
})(i);
}
我们画图,加深理解
思路4:ES6 的 let
其实就是将 var 声明改成 let 声明,let 声明,有块级作用域,找变量 i 的时候,是直接找到对应块级作用域的变量 i。
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
console.log(i);
改变一下下
题一
function test (){
var num = []
var i
for (i = 0; i < 10; i++) {
num[i] = function () {
console.log(i)
}
}
return num[9]
}
test()()
输出结果:10
这里就不文字描述了,直接看图理解吧。
题二
var test = (function() {
var num = 0
return () => {
return num++
}
}())
for (var i = 0; i < 10; i++) {
test()
}
console.log(test())
输出结果:10
需要注意的是变量 test 对应的值,是立即执行返回的结果
() => {
return num++
}
“复杂作用域”系列
它的套路非常简单粗暴 —— 就是在一道题里面,尽可能地想办法给你折腾出一堆作用域(有时还会杂糅一些较为零碎的 JS 语法知识点),目的就是把你整懵。
做题技巧 没有别的,就是画图,从被执行的函数看起,一层一层由内往外地把作用域关系图给画出来,图出来了,基本上答案,也就出来了。 步骤:
- step1:读题
- step2:画图
- 分层:我们从内向外画。
- 找变量:这是这题的难点!
实践出真知
我们来做一道题
var a = 1;
function test(){
a = 2;
return function(){
console.log(a);
}
var a = 3;
}
test()();
假设大家对变量提升和作用域规则这两个知识点,是熟悉的,如果不熟悉,请看下这两篇文章
- JavaScript 基础之变量声明 搜索“存在变量提升”
- JavaScript 的作用域和作用域链,原来就是这么一回事
因为使用 var 声明存在变量提升,所以实际运行的代码,是这样的:
var a = 1;
function test(){
var a = 2;
return function(){
console.log(a);
}
a = 3;
}
test()();
其次,作用域规则
我们作用域的划分,是在书写的过程中,根据你把它写在哪个位置来决定的。像这样划分出来的作用域,遵循的就是词法作用域模型。
所以 test 作用域的变量的值,实际上是 2。
下面我们来画图吧
作用域关系示意如下:
作用域链关系展示如下:
到这里,输出结果已经清晰明了,就是 2。
参考
文中如有错误,欢迎在评论区指正,如果这篇文章帮助到了你或者喜欢,欢迎点赞和关注。