在说一个概念前,我们需要确定它的前提,此文以 ECMAScript5 为基础撰写
执行上下文就是一段代码执行时所带的所有信息
《重学前端》的作者 winter 曾经对什么是执行上下文做过这样的解释:
JavaScript 标准把一段代码(包括函数),执行所需的所有信息定义为:“执行上下文
并且他整理出在不同 ECMAScript 版本中执行上下文所代表的含义:
执行上下文在 ES3 中,包含三个部分。
- scope:作用域,也常常被叫做作用域链
- variable object:变量对象,用来存储变量的对象
- this value: this 值
在 ES5 中,我们改进了命名方式,把执行上下文最初的三个部分改成下面这个样子
- lexical environment:词法环境,当获取变量时使用
- variable environment:变量环境,当声明变量时使用
- this value: this 值
在 ES2018 中,执行上下文又变成了这个样子,this 值被归入 lexical environment,但是增加了不少内容
- lexical environment:词法环境,当获取变量或者 this 值时使用
- variable environment:变量环境,当声明变量时使用
- code evaluation state: 用于恢复代码执行位置
- Function:执行的任务是函数时使用,表示正在被执行的函数
- ScriptOrModule:执行的任务是脚本或者模块时使用,表示正在被执行的代码
- Realm:使用的基础库和内置对象实力
- Generator:仅生成器上下文有这个属性,表示当前生成器
总结的很完整,按照 选新不选旧
原则,本文应该以 ES2022 为切入点展开,最次也要 ES2018,但主流的解释执行上下文都以 ES3/ES5 为例,权衡之后,笔者将以 ES5 为基础撰写执行上下文,并在后续补充说明 ES3 中的执行上下文
我们在讲 词法环境 时,曾经画过一张执行生命周期图,当时所讲的词法环境是在**(预)编译阶段产生,现在讲的执行上下文是在引擎执行阶段**进行
一段代码如果要执行,首先会往调用栈(call stack)中压入全局执行上下文;再创建词法环境,此时变量该提升提升,函数该提升提升,并将这些变量登记到词法环境中(编译阶段);接着进入执行阶段,执行可执行代码,该赋值赋值,遇到函数,就创建一个函数执行上下文,并往调用栈中压入该函数的执行上下文;而后创建该函数的词法环境,当该函数执行完后,从调用栈中弹出;反复循环,到最后调用栈中只剩一个全局执行上下文,除非你关闭浏览器,不然全局执行上下文不会弹出
我们要往调用栈中压入执行上下文,调用栈的数据结构为 栈
。特点为先进后出
如果我们的代码是这样的
var a = 1;
function foo() {function bar() {console.log(a);}bar();
}
function baz() {foo();
}
baz();
那么执行过程应该是这样:
图中的蓝色方块为 执行上下文
,外面黑框白底的区域就是模拟 调用栈
。整个过程遵循先进后出的原则
baz()
,创建 baz()
的函数执行上下文,并往调用栈中压栈baz()
调用 foo()
,创建 foo()
执行上下文,并将其压入调用栈中,foo()
调用 bar()
,创建 bar()
执行上下文,并将其压入调用栈,bar()
执行 console.log()
,同理将其压入调用栈,console.log()
后,被弹出bar()
执行完毕,弹出调用栈foo()
也执行完毕,弹出调用栈baz()
同样执行完毕,弹出调用栈只剩下全局执行上下文,留在栈底
在这里,我们看到 console.log()
也被压入到执行栈中,不禁有个思考,哪些代码元素会被执行到调用栈中呢?
事实上,不仅仅是 function 可以作为执行上下文在执行栈中运行,在 JavaScript 里定义了四种可执行代码:
所以才会看到 console.log()
被压入调用栈中,因为它属于 global code
JavaScript 引擎是按照可执行代码来执行代码的,每次执行步骤如下:
1.创建一个新的执行上下文(Execution Context)
2.创建一个新的词法环境(Lexical Environment)
3.把 LexicalEnvironment 和 VariableEnvironment 指向新创建的词法环境
4.把这个执行上下文压入执行栈并成为正在运行的执行上下文
5.执行代码
6.执行结束后,把这个执行上下文弹出执行栈
到现在,我们已经知道 JavaScript 是如何管理执行上下文的,现在让我们了解一下 JavaScript 引擎是怎样创建执行上下文的
创建执行上下文有两个阶段:1) 创建阶段 和 2) 执行阶段
在 JavaScript 代码执行前,执行上下文将经历创建阶段。在创建阶段会发生以下三件事:
ExecutionContext = {ThisBinding = ,LexicalEnvironment = { ... },VariableEnvironment = { ... },
}
这里需要再多嘴一句:
在很多文章中我们看到执行上下文只有 LexicalEnvironment 和 VariableEnvironment ,并没有 this。那是因为在 ES2018 后,this 就归纳到 LexicalEnvironment (如上文 winter 所说),但本文是以 ES5 为基础撰写,故此版本的执行上下文中是有 this 的
this 的指向很简单,谁调用我,我只想谁
在执行上下文中,this 就指向那个调用者
这两”姐妹“有点像,只是分工不同。
变量环境组件(VariableEnvironment component ) 用来登记 var
、function
等变量声明
词法环境组件(LexicalEnvironment component ) 用来登记 let
、const
、class
等变量声明
按上例可以这么画图:
LexicalEnvironment
和 VariableEnvironment
则都是词法环境(Lexical Environment)。很多文章中常把 LexicalEnvironment
理解成 词法环境,这是不对的,LexicalEnvironment
是一个单词,表示执行上下文中的是标识 let
、const
、class
等变量声明,而 VariableEnvironment
则是标识 var
、function
等变量声明
如果非要用中文来表示 LexicalEnvironment
的话,我更愿意用 词法环境组件 来表示;同理,VariableEnvironment
则用 变量环境组件 来表示
对我而言,变量环境组件和词法环境组件就好比”装黄豆瓶“和”装绿豆瓶“,一个负责装黄豆(var,function),一个负责装绿豆(let,const,class),它们指向词法环境,本质是从词法环境中拿数据
所以无论是词法环境组件还是变量环境组件,都有一个环境记录器和一个 outer 对象,其中环境记录器记录变量,outer 指向父级作用域
具体可以看 ECMAScript 6 标准中的第八节 详细了解一下
回头看上述例子:
bar()
函数中的 console.log(a)
,本环境记录器中找不到,就引着 outer
找,在 window 中找到变量 a ,赋值后,弹出,foo()
执行完后也弹出,baz()
执行完弹出,留下全局执行上下文在栈底
之前在说 词法环境 时,我们曾下过这样的定义:outer 就是指向词法环境的父级词法环境(作用域)
但是这里就有个疑惑了,outer 既然指词法环境的父级作用域,那作用域链从那里来?
以上面的 demo 为例,bar 的执行上下文的伪代码:
BarExecutionContext = {ThisBinding = ,LexicalEnvironment = { EnvironmentRecord: { ... },outer:
},VariableEnvironment = {EnvironmentRecord: { ... },outer: },
}
bar 的 outer 指向 foo 执行上下文, foo 的 outer 指向 window,变量先从当前执行作用域查找变量,如果找不到,就引着 outer 继续查找。这一过程中,bar 作用域—foo 作用域—window 作用域。
当代码要访问一个变量时——首先会搜索当前词法环境,然后搜索外部环境,然后搜索更外部的环境,以此类推,直到全局词法环境
而我们潜意识中的”作用域链“是在 ES3 中的才有的,因为那个时候的执行上下文中有 scope(作用域),当前作用域找不到变量,就往外层作用域中找,然后再到更外层的作用域找,知道全局作用域,作用域与作用域之间以链表的形式连接着,这就是作用域链
分三种
如果你喜欢看有关执行上下文的文章,应该常看到这样的描述,执行上下文由变量对象(Variable object,VO)、作用域链(Scope chain) 、this 构成。
其实我们可以把变量对象(VO) 看成是 ES5 中的词法环境,scope 为词法环境中的 outer
例如:
function foo1() {foo2();
}
function foo2() {foo3();
}
function foo3() {foo4();
}
function foo4() {console.lg('foo4');
}
foo1();
得到错误提示如图:
或者在 Chrome 中执行代码,打断点得到:
笔者最开始了解执行上下文、执行上下文栈以及闭包时,大家是用变量对象和活动对象来解释的,这是 ES3 时的词汇,而本文是以 ES5 为标准展开
其两者的涵义简单来说,变量对象(Variable object)是与执行上下文相关的对象,存储了在上下文中定义的变量和函数声明。而在函数上下文中,我们用活动对象(activation object,AO)来表示变量对象
作用域在(预)编译阶段确定,但是作用域链是在执行上下文的创建阶段完成生产的。因为函数在调用时,才会开始创建对应的执行上下文。执行上下文包括了:变量对象、作用域链以及 this 的指向
执行上下文可以理解为函数的执行环境,当函数执行时,都会创建一个执行环境
每次只能有一个执行上下文处于运行状态,因为 JavaScript 是单线程语言,它由执行栈或(叫)调用栈来管理
创建一个函数,就生成了一个作用域;调用一个函数,就生成一个作用域链
执行上下文创建阶段分为绑定 this,创建词法环境,变量环境三步
调用函数时,创建一个新的词法环境
词法环境这个说法,是 ES5 规范中的内容,可以理解为 ES3 中的变量对象,scope 为 词法环境中的 outer
在 ES5 中,词法环境 和 变量环境 的一个不同就是前者被用来存储函数声明和变量(let
和 const
)绑定,而后者只用来存储 var
变量绑定
在 ES3 时,执行上下文包括了变量对象、作用域链以及 this
在 ES5 时,执行上下文则包括词法环境、变量环境、this。其中词法环境或者变量环境都是由用于环境记录器和 outer 对象组成,其中 outer 指向父级作用域,环境记录器记录自由变量
Q:是不是说在定义时确认了作用域,在调用时确认了作用域链?
A: yes,这里需要注意的是每一个执行上下文都会进行提升操作
Q: ES5 中的执行上下文的词法环境和编译阶段的词法环境有什么不同?
A:一段 JavaScript 代码在执行之前需要被 JavaScript 引擎编译,编译完成之后,才会进行执行阶段,大致流程如下:
整理了75个JS高频面试题,并给出了答案和解析,基本上可以保证你能应付面试官关于JS的提问。
有需要的小伙伴,可以点击下方卡片领取,无偿分享