深入ECMAScript(二):执行上下文

栏目: JavaScript · 发布时间: 5年前

内容简介:执行上下文(Exexution Contexts):用来通过ECMAScript编译器来追踪代码运行时计算的一种规范策略。可以将执行上下文理解为伴随代码解析运行始终并记录其状态的一个抽象装置。执行上下文同时包含

执行上下文(Exexution Contexts):用来通过ECMAScript编译器来追踪代码运行时计算的一种规范策略。

可以将执行上下文理解为伴随代码解析运行始终并记录其状态的一个抽象装置。

ExecutionContext = {
    VariableEnvironment: { ... },
    LexicalEnvironment: { ... },
}
复制代码

执行上下文同时包含 变量环境组件(VariableEnvironment) 词法环境组件(LexicalEnvironment) ,这两个组件都属于我们之前所说的 词法环境(Lexical Environment) 。为什么存在两个环境组件,我们稍后将进行详细讨论

二、执行上下文栈

执行上下文栈(Execution Context Stack):是一个后进先出的栈式结构(LIFO),用来跟踪维护执行上下文。 运行执行上下文(running execution context) 始终是执行上下文栈的顶层元素。那么什么时候会创建新的执行上下文呢?

ECMAScript可执行代码有四种类型: 全局代码,函数代码,模块代码和 eval 。每当从与当前运行执行上下文相关联的可执行代码转到与此执行上下文不相关的可执行代码时,会创建新的执行上下文,将其压入执行上下文栈并成为正在运行的执行上下文。当相关代码执行完毕返回后,将正在运行的执行上下文从执行上下文栈删除,之前的执行上下文又成为了正在运行的执行上下文。

我们通过一个动图来看一下执行上下文栈的工作过程

深入ECMAScript(二):执行上下文
  1. 开始执行任何JavaScript代码前,会创建全局上下文并压入栈,所以全局上下文一直在栈底。
  2. 每次调用函数都会创建新的执行上下文(即便在函数内部调用自身),并压入栈。
  3. 函数执行完毕返回,其执行上下文出栈。
  4. 所有代码运行完毕,执行上下文栈只剩全局执行上下文。

三、执行上下文的创建、入栈及出栈

上面提到过ECMAScript可执行代码有四种类型: 全局代码,函数代码,模块代码和 eval

首先要说明的是,这里虽然说是 全局代码 ,但是JavaScript引擎其实是按照 script 标签来解析执行的,也就是说 script 标签按照它们出现的顺序解析执行,这也就是为什么我们平时要将项目依赖js库放在前面引入的原因。

JavaScript引擎是按可执行代码块来执行代码的,在任意的JavaScript可执行代码被执行时,执行步骤可按如下理解:

  1. 创建一个 新的执行上下文
  2. 设置该执行上下文的 变量环境组件(VariableEnvironment) 词法环境组件(LexicalEnvironment)
  3. 将该执行上下文 推入执行上下文栈 并成为 正在运行的执行上下文
  4. 对代码块内的 标识符进行实例化及初始化
  5. 运行代码
  6. 运行完毕后 执行上下文出栈

变量提升(Hoisting)及暂时性死区(temporal dead zone,TDZ)

我们平常所说的变量提升就发生在上述执行步骤的 第四步 ,对代码块内的 标识符进行实例化及初始化 的具体表现如下:

  1. 执行代码块内的 letconstclass 声明的标识符合集记录为 lexNames
  2. 执行代码块内的 varfunction 声明的标识符合集记录为 varNames
  3. 如果 lexNames 内的任何标识符在 varNameslexNames 内出现过,则报错 SyntaxError ,这就是为什么可以用 varfunction 声明多个同名变量,但是不能用 letconstclass 声明多个同名变量。
  4. varNames 内的 var 声明的标识符实例化并初始化赋值 undefined ,如果有同名标识符则跳过

    这就是所谓的 变量提升 ,我们用 var 声明的变量,在声明位置之前访问并不会报错,而是返回 undefined

  5. lexNames 内的标识符实例化,但并不会进行初始化,在运行至其声明处代码时才会进行初始化,在初始化前访问都会报错。

    这就是我们所说的 暂时性死区letconstclass 声明的变量其实也提升了,只不过没有被初始化,初始化之前不可访问。

  6. 最后将 varNames 内的函数声明实例化并初始化赋值对应的函数体,如果有同名函数声明,则前面的都会忽略,只有最后一个声明的函数会被初始化赋值。

    函数声明会被直接赋值,所有我们在函数声明位置之前也可以调用函数。

四、为什么需要两个环境组件

首先明确这两个环境组件的作用, 变量环境组件(VariableEnvironment) 用于记录 var 声明的绑定, 词法环境组件(LexicalEnvironment) 用于记录其他声明的绑定(如 letconstclass 等)。

一般情况下一个 Exexution Contexts 内的 VariableEnvironmentLexicalEnvironment 指向同一个词法环境,之所以要区分两个组件,主要是为了实现块级作用域的同时不影响 var 声明及函数声明。

众所周知,ES6之前并没有 块级作用域 的概念,但是ES6及之后我们可以通过新增的 letconst 命令来实现块级作用域,并且不影响 var 声明的变量,那么这是怎么实现的呢?

  1. 首先在一个正在运行的执行上下文( running Execution Context )内,词法环境由 VariableEnvironmentLexicalEnvironment 构成,此执行上下文内的所有标识符的绑定都记录在两个组件的环境记录内。
  2. 当运行至块级代码时,会将 LexicalEnvironment 记录下来,我们将其记录为 oldEnv
  3. 然后创建一个新的 LexicalEnvironment (外部词法环境 outer 指向 oldEnv ),我们将其记录为 newEnv ,并将 newEnv 设置为 running Execution ContextLexicalEnvironment
  4. 然后块级代码内的 letconst 等声明就会绑定在这个 newEnv 上面,但是 var 声明和函数声明还是绑定在原来的 VariableEnvironment 上面。

    块级代码内的函数声明会被当做 var 声明,会被提升至外部环境,块级代码运行前其值为初始值 undefined

    console.log(foo) // 输出:undefined
    {
        function foo() {console.log('hello')}
    }
    console.log(foo) // 输出: ƒ foo() {console.log('hello')}
    复制代码
  5. 块级代码运行完毕后,又将 oldEnv 还原为 running Execution ContextLexicalEnvironment

目前包括块级代码(在一对大括号内的代码)、 for 循环语句、 switch 语句、 TryCatch 语句中的 catch 从句以及 with 语句( with 语句创建的新环境为对象式环境,其他皆为声明式环境)都是这样来实现块级作用域的。

关于执行上下文我们就说到这里,下一篇我们学习领域(Realm)。

文章我是边学边写的,有什么错误或不当的地方,请务必给予指正。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

网络多人游戏架构与编程

网络多人游戏架构与编程

格雷泽 (Joshua Glazer)、马达夫 (Sanjay Madhav) / 王晓慧、张国鑫 / 人民邮电出版社 / 2017-10-1 / CNY 109.00

本书是一本深入探讨关于网络多人游戏编程的图书。 全书分为13章,从网络游戏的基本概念、互联网、伯克利套接字、对象序列化、对象复制、网络拓扑和游戏案例、延迟、抖动和可靠性、改进的延迟处理、可扩展性、安全性、真实世界的引擎、玩家服务、云托管专用服务器等方面深入介绍了网络多人游戏开发的知识,既全面又详尽地剖析了众多核心概念。 本书的多数示例基于C++编写,适合对C++有一定了解的读者阅读。本......一起来看看 《网络多人游戏架构与编程》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

在线进制转换器
在线进制转换器

各进制数互转换器

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试