开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第6天,点击查看活动详情
前言
前文记录了执行上下文、作用域及作用域链的相关知识,这篇文章通过一道经典面试题深入理解执行上下文和作用域链。
了解以下内容有助于理解后续代码及图解
执行上下文创建变量对象(VO)的过程:
- 确定所有
形参值以及特殊变量arguments- 确定函数中通过var声明的变量,将它们的值设置为
undefined,如果VO中已有该名称,则直接忽略- 确定函数中通过
字面量声明的函数,将它们的值设置为指向函数对象,如果VO中已存在该名称,则覆盖作用域链:
VO中包含一个额外的属性,该属性指向创建该VO的函数本身- 每个函数在创建时,会有一个隐藏属性
[[scope]],它指向创建该函数时的VO- 当访问一个变量时,会先查找自身
VO中是否存在,如果不存在,则依次查找[[scope]]属性
请看题
for (var i = 0; i < 3; i++) {
setTimeout(function () {
console.log(i);
}, 1000)
}
console.log(i);
问: console.log 输出的 i 的值分别是什么? 为什么? 如何使输出的结果为:3,0,1,2?
回答是什么:
掘金上有非常多的文章介绍过这道面试题,要回答这个问题是比较简单的:
- 全局
console先输出i,值为3 setTimeout中的console等待1000ms后输出i,值也为3
回答为什么:
为什么代码执行结果是这样的?这个问题可能少有人从 执行上下文和作用域链 的角度去分析:
var i变量提升,属于全局上下文(GO)- 进入
for循环,加入三个setTimeout任务到宏任务队列等待 - 结束循环,此时
i被赋值为3 - 全局
console.log(i),输出3 全局上下文执行完毕,执行栈中无任务需要执行,开始执行宏任务队列中的三个setTimeout回调任务队列:先进先出,先进入先执行- 执行
setTimeout回调时,创建回调函数VO - 回调函数使用到
变量i,当前VO不存在变量i,则从作用域链中查找 - 每个
回调函数VO都有一个额外的属性指向创建该VO的函数本身;每个函数又有一个属性[[scope]]指向创建该函数的VO,即回调函数VO中无变量i,继续查找回调函数的[[scope]]属性,此次[[scope]]指向全局上下文,全局上下文中存在变量i,则使用全局变量i
图解:
注:图解中省略了事件循环相关过程,并将三个匿名回调函数假设为A、B、C三个函数;执行栈中的VO不会同时存在,真实情况是:执行完一个函数就出栈,然后再将下一个回调函数VO入栈。
最终执行的三个回调函数查找到的变量i都是GO中的i,所以输出的 i 的值都是3
如何解决
其他相关文章也都有给出解决方案:
- 将
var改为let,使变量i不被定义在GO而是块级作用域中 for循环中使用立即执行函数,每次都将i的值作为形参传入,这样在函数上下文中就会存在形参i,取值时就不会取到全局的变量i
主要解释第二种方案:
for (var i = 0; i < 3; i++) {
(function (i) {
// 将i作为立即执行函数的形参传入,这样会在该函数VO中添加变量i,保证每个函数访问的变量i都是各自VO中的i
setTimeout(function () {
console.log(i);
}, 1000)
})(i)
}
console.log(i);
图解: