Blog
Blog copied to clipboard
深入JavaScript系列(二):执行上下文
一、执行上下文(Exexution Contexts)
执行上下文(Exexution Contexts):用来通过ECMAScript编译器来追踪代码运行时计算的一种规范策略。
执行上下文简单理解就是代码执行时所在环境的抽象。
执行上下文同时包含变量环境组件(VariableEnvironment)和词法环境组件(LexicalEnvironment),这两个组件多数情况下都指向相同的词法环境(Lexical Environment),那为什么还要存在两个环境组件呢?我们稍后将进行详细讨论。如果不太了解词法环境的可以看下我的上一篇文章深入ECMAScript系列(一):词法环境。
ExecutionContext = {
VariableEnvironment: { ... },
LexicalEnvironment: { ... },
}
二、执行上下文栈
执行上下文栈(Execution Context Stack):是一个后进先出的栈式结构(LIFO),用来跟踪维护执行上下文。运行执行上下文(running execution context) 始终位于执行上下文栈的顶层。那么什么时候会创建新的执行上下文呢?
ECMAScript可执行代码有四种类型:全局代码,函数代码,模块代码和eval
。每当从当前执行代码运行至其他可执行代码时,会创建新的执行上下文,将其压入执行上下文栈并成为正在运行的执行上下文。当相关代码执行完毕返回后,将正在运行的执行上下文从执行上下文栈删除,之前的执行上下文又成为了正在运行的执行上下文。
我们通过一个动图来看一下执行上下文栈的工作过程
- 开始执行任何JavaScript代码前,会创建全局上下文并压入栈,所以全局上下文一直在栈底。
- 每次调用函数都会创建新的执行上下文(即便在函数内部调用自身),并压入栈。
- 函数执行完毕返回,其执行上下文出栈。
- 所有代码运行完毕,执行上下文栈只剩全局执行上下文。
三、执行上下文的创建、入栈及出栈
上面提到过ECMAScript可执行代码有四种类型:全局代码,函数代码,模块代码和eval
。
这里虽然说是全局代码,但是JavaScript引擎其实是按照
script
标签来解析执行的,也就是说script
标签按照它们出现的顺序解析执行,这也就是为什么我们平时要将项目依赖js库放在前面引入的原因。
JavaScript引擎是按可执行代码块来执行代码的,在任意的JavaScript可执行代码被执行时,执行步骤可按如下理解:
- 创建一个新的执行上下文(Execution Context)
- 创建一个新的词法环境(Lexical Environment)
- 将该执行上下文的 变量环境组件(VariableEnvironment) 和 词法环境组件(LexicalEnvironment) 都指向新创建的词法环境
- 将该执行上下文 推入执行上下文栈 并成为 正在运行的执行上下文
- 对代码块内的 标识符进行实例化及初始化
- 运行代码
- 运行完毕后执行上下文出栈
变量提升(Hoisting)及暂时性死区(temporal dead zone,TDZ)
我们平常所说的变量提升就发生在上述执行步骤的第四步,对代码块内的标识符进行实例化及初始化的具体表现如下:
- 执行代码块内的
let
、const
和class
声明的标识符合集记录为lexNames
- 执行代码块内的
var
和function
声明的标识符合集记录为varNames
- 如果
lexNames
内的任何标识符在varNames
或lexNames
内出现过,则报错SyntaxError
这就是为什么可以用
var
或function
声明多个同名变量,但是不能用let
、const
和class
声明多个同名变量。 - 将
varNames
内的var
声明的标识符实例化并初始化赋值undefined
,如果有同名标识符则跳过这就是所谓的变量提升,我们用
var
声明的变量,在声明位置之前访问并不会报错,而是返回undefined
- 将
lexNames
内的标识符实例化,但并不会进行初始化,在运行至其声明处代码时才会进行初始化,在初始化前访问都会报错。这就是我们所说的暂时性死区,
let
、const
和class
声明的变量其实也提升了,只不过没有被初始化,初始化之前不可访问。 - 最后将
varNames
内的函数声明实例化并初始化赋值对应的函数体,如果有同名函数声明,则前面的都会忽略,只有最后一个声明的函数会被初始化赋值。函数声明会被直接赋值,所有我们在函数声明位置之前也可以调用函数。
四、为什么需要两个环境组件
首先明确这两个环境组件的作用,变量环境组件(VariableEnvironment)
用于记录var
声明的绑定,词法环境组件(LexicalEnvironment)
用于记录其他声明的绑定(如let
、const
、class
等)。
一般情况下一个Exexution Contexts
内的VariableEnvironment
和LexicalEnvironment
指向同一个词法环境,之所以要区分两个组件,主要是为了实现块级作用域的同时不影响var
声明及函数声明。
众所周知,ES6之前并没有块级作用域的概念,但是ES6及之后我们可以通过新增的let
及const
等命令来实现块级作用域,并且不影响var
声明的变量和函数声明,那么这是怎么实现的呢?
- 首先在一个正在运行的执行上下文(
running Execution Context
)内,词法环境由VariableEnvironment
和LexicalEnvironment
构成,此执行上下文内的所有标识符的绑定都记录在两个组件的环境记录内。 - 当运行至块级代码时,会将
LexicalEnvironment
记录下来,我们将其记录为oldEnv
。 - 然后创建一个新的
LexicalEnvironment
(外部词法环境outer
指向oldEnv
),我们将其记录为newEnv
,并将newEnv
设置为running Execution Context
的LexicalEnvironment
。 - 然后块级代码内的
let
、const
等声明就会绑定在这个newEnv
上面,但是var
声明和函数声明还是绑定在原来的VariableEnvironment
上面。块级代码内的函数声明会被当做
var
声明,会被提升至外部环境,块级代码运行前其值为初始值undefined
console.log(foo) // 输出:undefined { function foo() {console.log('hello')} } console.log(foo) // 输出: ƒ foo() {console.log('hello')}
- 块级代码运行完毕后,又将
oldEnv
还原为running Execution Context
的LexicalEnvironment
。
目前包括块级代码(在一对大括号内的代码)、for
循环语句、switch
语句、TryCatch
语句中的catch
从句以及with
语句(with
语句创建的新环境为对象式环境,其他皆为声明式环境)都是这样来实现块级作用域的。
系列文章
准备将之前写的部分深入ECMAScript文章重写,加深自己理解,使内容更有干货,目录结构也更合理。
欢迎前往阅读系列文章,如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。
菜鸟一枚,如果有疑问或者发现错误,可以在相应的 issues 进行提问或勘误,与大家共同进步。
开始执行任何JavaScript代码前,会创建全局上下文并压入栈,所以全局上下文一直在栈底。
这句话似乎有一些矛盾,如果全局上下文一直在栈底,那么它就不会出栈,也不存在压入栈;“执行任何代码前”也似乎有些问题。
所有代码运行完毕,执行上下文栈只剩全局执行上下文。
这句话似乎也是在说执行上下文一直在栈底。
但我个人觉得所有代码执行完,全局上下文是会出栈的(所有栈空的时候才会调用异步队列的任务)。
所有想请教下,全局上下文是否会出栈这个问题。
@LasyIsLazy
我认为栈空的时候才会调用异步队列的任务
描述不太准确,应该是同步任务队列空的时候调用异步任务队列,全局上下文出栈违背了只有一个全局上下文的原则,不管同步代码还是异步代码,也都是在全局上下文定义执行的,我理解全局上下文出栈意味着页面生命周期结束。
@LasyIsLazy 我认为
栈空的时候才会调用异步队列的任务
描述不太准确,应该是同步任务队列空的时候调用异步任务队列,全局上下文出栈违背了只有一个全局上下文的原则,不管同步代码还是异步代码,也都是在全局上下文定义执行的,我理解全局上下文出栈意味着页面生命周期结束。
在 MDN 关于事件循环的介绍 中,一直用的“栈”这个概念来解释任务队列中的函数的执行。
The processing of functions continues until the stack is once again empty
我想这里的“栈”与“执行上下文栈”应该是同一个吧?
又看了下HTML标准文档,其实描述为全局上下文并不合适,应该描述为领域(Realm)上下文。
例如存在iframe时会存在多个Realm。
There is always a 1-to-1-to-1 mapping between JavaScript realms, global objects, and environment settings objects
领域执行上下文realm execution context
挂载在environment settings objects
上的
-
run script
时将realm execution context
压栈 -
clean up after running script
时realm execution context
出栈
所以之前描述有误,领域执行上下文会出栈,也会再次入栈,领域执行上下文一直是那个领域执行上下文,只是执行的script变了。
又看了下HTML标准文档,其实描述为全局上下文并不合适,应该描述为领域(Realm)上下文。
例如存在iframe时会存在多个Realm。
There is always a 1-to-1-to-1 mapping between JavaScript realms, global objects, and environment settings objects
领域执行上下文
realm execution context
挂载在environment settings objects
上的
run script
时将realm execution context
压栈clean up after running script
时realm execution context
出栈所以之前描述有误,领域执行上下文会出栈,也会再次入栈,领域执行上下文一直是那个领域执行上下文,只是执行的script变了。
那我这么理解对吗:“全局上下文”(也就是最初创建的 realm execution context)也应该在所有代码运行完之后,“clean up after running script”,也就是出栈。当有任务队列中的任务要运行的时候再创建这个“全局上下文”。
@logan70
ECMAScript 规范 中这样描述:
Execution of a Job can be initiated only when there is no running execution context and the execution context stack is empty.
When there is no running execution context and the execution context stack is empty, the ECMAScript implementation removes the first PendingJob from a Job Queue and uses the information contained in it to create an execution context and starts execution of the associated Job abstract operation.
这意味着执行上下文栈是会空的。
我的理解:
- 所有代码执行完成之后,全局执行上下文会出栈
- 当有异步任务要执行的时候,也不会创建全局执行上下文,而只是创建这个回调函数的执行上下文(所以栈底可能不一定是全局执行上下文)。
- 全局执行上下文会被销毁,但是全局词法环境不会被销毁。全局执行上下文的销毁不代表页面的结束,全局词法环境的销毁才代表页面的结束。
以上是我的推测。
执行上下文
ExecutionContext = {
VariableEnvironment: { ... },
LexicalEnvironment: { ... },
}
在上篇文章中写到全局环境记录,那全局的执行上下文的伪代码怎么写呢?var变量和函数放到了Object Environment Record,那VariableEnvironment中放什么呢?
执行上下文
ExecutionContext = { VariableEnvironment: { ... }, LexicalEnvironment: { ... }, }
在上篇文章中写到全局环境记录,那全局的执行上下文的伪代码怎么写呢?var变量和函数放到了Object Environment Record,那VariableEnvironment中放什么呢?
我也想问。。