开启掘金成长之旅!这是我参与「掘金日新计划 · 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);
图解: