JS词法环境和执行上下文

news2025/7/28 21:59:43

前言

JavaScript是一门解释性动态语言,但同时它也是一门充满神秘感的语言。如果要成为一名优秀的JS开发者,那么对JavaScript程序的内部执行原理要有所了解。

本文以最新的ECMA规范中的第八章节为基础,理清JavaScript的词法环境和执行上下文的相关内容。这是理解JavaScript其他概念(let/const暂时性死区、变量提升、闭包等)的基础。

本文参考的是最新发布的第十代ECMA-262标准,即ES2019
ES2019与ES6在词法环境和执行上下文的内容上是近似的,ES2019在细节上做了部分补充,因此本文直接采用ES2019的标准。你也可以对比两个版本的标准的差异。

执行上下文(Execution Context)

执行上下文是用来跟踪记录代码运行时环境的抽象概念。每一次代码运行都至少会生成一个执行上下文。代码都是在执行上下文中运行的。

你可以将代码运行与执行上下文的关系类比为进程与内存的关系,在代码运行过程中的变量环境信息都放在执行上下文中,当代码运行结束,执行上下文也会销毁。

在执行上下文中记录了代码执行过程中的状态信息,根据不同运行场景,执行上下文会细分为如下几种类型:

  • 全局执行上下文:当运行代码是处于全局作用域内,则会生成全局执行上下文,这也是程序最基础的执行上下文。
  • 函数执行上下文:当调用函数时,都会为函数调用创建一个新的执行上下文。
  • eval执行上下文:eval函数执行时,会生成专属它的上下文,因eval很少使用,故不作讨论。

执行栈

有了执行上下文,就要有合理管理它的工具。而执行栈(Execution Context Stack)是用来管理执行期间创建的所有执行上下文的数据结构,它是一个LIFO(后进先出)的栈,它也是我们熟知的JS程序运行过程中的调用栈。
程序开始运行时,会先创建一个全局执行上下文并压入到执行栈中,之后每当有函数被调用,都会创建一个新的函数执行上下文并压入栈内。

我们从一小段代码来看下执行栈的工作过程:

<script>
    console.log('script')    function foo(){        function bar(){            console.log('bar', isNaN(undefined))        }        bar()        console.log('foo')    }    foo()
</script>

当这段JS程序开始运行时,它会创建一个全局执行上下文GlobalContext,其中会初始化一些全局对象或全局函数,如代码中的console,undefined,isNaN。将全局执行上下文压入执行栈,通常JS引擎都有一个指针running指向栈顶元素:

JS引擎会将全局范围内声明的函数(foo)初始化在全局上下文中,之后开始一行行的执行代码,运行到console就在running指向的上下文中的词法环境中找到全局对象console并调用log函数。

PS:当然,当调用log函数时,也是要新建函数上下文并压栈到调用栈中的。这里为了简单流程,忽略了log上下文的创建过程。

运行到foo()时,识别为函数调用,此时创建一个新的执行上下文FooContext并入栈,将FooContext内词法环境的outer引用指向全局执行上下文的词法环境,移动running指针指向这个新的上下文:

在完成FooContext创建后,进入到FooContext中继续执行代码,运行到bar()时,同理仍需要新建一个执行上下文BarContext,此时BarContext内词法环境的outer引用会指向FooContext的词法环境:参考 前端进阶面试题详细解答

继续运行bar函数,由于函数上下文内有outer引用实现层层递进引用,因此在bar函数内仍可以获取到console对象并调用log

之后,完成barfoo函数调用,会依次将上下文出栈,直至全局上下文出栈,程序结束运行。

执行上下文的创建

执行上下文创建会做两件事情:

  1. 创建词法环境LexicalEnvironment
  2. 创建变量环境VariableEnvironment

因此一个执行上下文在概念上应该是这样子的:

ExecutionContext = {
  LexicalEnvironment = <ref. to LexicalEnvironment in memory>,
  VariableEnvironment = <ref. to VariableEnvironment in  memory>,
}

在全局执行上下文中,this指向全局对象,window in browser / global in nodejs

词法环境(LexicalEnvironment)

词法环境是ECMA中的一个规范类型 —— 基于代码词法嵌套结构用来记录标识符和具体变量或函数的关联。
简单来说,词法环境就是建立了标识符——变量的映射表。这里的标识符指的是变量名称或函数名,而变量则是实际变量原始值或者对象/函数的引用地址。

LexicalEnvironment中由两个部分构成:

  • 环境记录EnvironmentRecord:存放变量和函数声明的地方;
  • 外层引用outer:提供了访问父词法环境的引用,可能为null;

this绑定ThisBinding:确定当前环境中this的指向,this binding存储在EnvironmentRecord中;

词法环境的类型

  • 全局环境(GlobalEnvironment):在JavaScript代码运行伊始,宿主(浏览器、NodeJs等)会事先初始化全局环境,在全局环境的EnvironmentRecord中会绑定内置的全局对象(Infinity等)或全局函数(evalparseInt等),其他声明的全局变量或函数也会存储在全局词法环境中。全局环境的outer引用为null

这里提及的全局对象就有我们熟悉的所有内置对象,如Math、Object、Array等构造函数,以及Infinity等全局变量。全局函数则包含了eval、parseInt等函数。

  • 模块环境(ModuleEnvironment):你若写过NodeJs程序就会很熟悉这个环境,在模块环境中你可以读取到exportmodule等变量,这些变量都是记录在模块环境的ER中。模块环境的outer引用指向全局环境。

  • 函数环境(FunctionEnvironment):每一次调用函数时都会产生函数环境,在函数环境中会涉及this的绑定或super的调用。在ER中也会记录该函数的lengtharguments属性。函数环境的outer引用指向调起该函数的父环境。在函数体内声明的变量或函数则记录在函数环境中。

环境记录ER

代码中声明的变量和函数都会存放在EnvironmentRecord中等待执行时访问。
环境记录EnvironmentRecord也有两个不同类型,分别为declarativeobjectdeclarative是较为常见的类型,通常函数声明、变量声明都会生成这种类型的ER。object类型可以由with语句触发的,而with使用场景很少,一般开发者很少用到。

如果你在函数体中遇到诸如var const let class module import 函数声明,那么环境记录就是declarative类型的。

值得一提的是全局上下文的ER有一点特殊,因为它是object ERdeclarative ER的混合体。在object ER中存放的是全局对象函数、function函数声明、asyncgeneratorvar关键词变量。在declarative ER则存放其他方式声明的变量,如let const class等。由于标准中将object类型的ER视作基准ER,因此这里我们仍将全局ER的类型视作object

GlobalExecutionContext = {
    LexicalEnvironment: {
        EnvironmentRecord: {
            type: 'object',  // 混合 object + declarative
            this: <globalObject>,
            NaN,
            parseInt,
            Object,
            myFunc,
            a,
            b,
            ...
        },
        outer: null,
    }
}

LexicalEnvironment只存储函数声明和let/const声明的变量,与下文的VariableEnvironment有所区别。

比如,我们有如下代码:

let a = 10;
function foo(){
    let b = 20
    console.log(a, b)
}
foo()

// 它们的词法环境伪码如下:
GlobalEnvironment: {
    EnvironmentRecord: {
        type: 'object',
        this: <globalObject>,
        a: <uninitialized>,
        foo: <func>
    },
    outer: <null>
}

FunctionEnvironment: {
    EnvironmentRecord: {
        type: 'declarative',
        this: <globalObject>,  // 严格模式下为undefined
        arguments: {length: 0},
        b: <uninitialized>
    },
    outer: <GlobalEnvironment>
}

函数环境记录

由于函数环境是我们日常开发过程最常见的词法环境,因此需要更加深入的研究一下函数环境的运行机制,帮助我们更好理解一些语言特性。

当我们调用一个函数时,会生成函数执行上下文,这个函数执行上下文的词法环境的环境记录就是函数类型的,有点拗口,用树形图代表一下:

FunctionContext
    |LexicalEnvironment
        |EnvironmentRecord  //--> 函数类型

为什么要强调这个类型呢?因为ECMA针对函数式环境记录会额外增加一些内部属性:

内部属性Value说明补充
[[ThisValue]]Any函数内调用this时引用的地址,我们常说的函数this绑定就是给这个内部属性赋值
[[ThisBindingStatus]]"lexical" / "initialized" / "uninitialized"若等于lexical,则为箭头函数,意味着this是空的;强行new箭头函数会报错TypeError错误
FunctionObjectObject在这个对象中有两个属性[[Call]][[Construct]],它们都是函数,如何赋值取决于如何调用函数正常的函数调用赋值[[Call]],而通过newsuper调用函数则赋值[[Construct]]
[[HomeObject]]Object / undefined如果该函数(非箭头函数)有super属性(子类),则[[HomeObject]]指向父类构造函数若你写过extends就知道我在说什么
[[NewTarget]]Object / undefined如果是通过[[Construct]]方式调用的函数,那么[[NewTarget]]非空在函数中可以通过new.target读取到这个内部属性。以此来判断函数是否通过new来调用的

此外,函数环境记录中还存有一个arguments对象,记录了函数的入参信息。

ThisBinding

this绑定是一个老生常谈的问题,由于存在多种分析场景,这里不便展开,this绑定的目的是在执行上下文创建之时就明确this的指向,在函数执行过程中读取到正确的this引用的对象。

小结

概念类型太多,有一些凌乱了。简单速记一下:

词法环境分类 = 全局 / 函数 / 模块
词法环境 = ER + outer + this
ER分类 = declarative(DER) + object(OER)
全局ER = DER + OER

VariableEnvironment 变量环境

在ES6前,声明变量都是通过var关键词声明的,在ES6中则提倡使用letconst来声明变量,为了兼容var的写法,于是使用变量环境来存储var声明的变量。

var关键词有个特性,会让变量提升,而通过let/const声明的变量则不会提升。为了区分这两种情况,就用不同的词法环境去区分。

变量环境本质上仍是词法环境,但它只存储var声明的变量,这样在初始化变量时可以赋值为undefined

有了这些概念,一个完整的执行上下文应该是什么样子的呢?来点例子🌰:

let a = 10;
const b = 20;
var sum;

function add(e, f){
    var d = 40;
    return d + e + f 
}

let utils = {
    add
}

sum = utils.add(a, b)

完整的执行上下文如下所示:

GlobalExecutionContext = {
    LexicalEnvironment: {
        EnvironmentRecord: {
            type: 'object',
            this: <globalObject>,
            add: <function>,
            a: <uninitialized>,
            b: <uninitialized>,
            utils: <uninitialized>
        },
        outer: null
    },
    VariableEnvironment: {
        EnvironmentRecord: {
            type: 'object',
            this: <globalObject>
            sum: undefined
        },
        outer: null
    },
}

// 当运行到函数add时才会创建函数执行上下文
FunctionExecutionContext = {
    LexicalEnvironment: {
        EnvironmentRecord: {
            type: 'declarative',
            this: <utils>,
            arguments: {0: 10, 1: 20, length: 2},
            [[NewTarget]]: undefined,
            e: 10,
            f: 20,
            ...
        },
        outer: <GlobalLexicalEnvironment>
    },
    VariableEnvironment: {
        EnvironmentRecord: {
            type: 'declarative',
            this: <utils>
            d: undefined,
        },
        outer: <GlobalLexicalEnvironment>
    },
}

执行上下文创建后,进入到执行环节,变量在执行过程中赋值、读取、再赋值等。直至程序运行结束。
我们注意到,在执行上下文创建时,变量a``b都是<uninitialized>的,而sum则被初始化为undefined。这就是为什么你可以在声明之前访问var定义的变量(变量提升),而访问let/const定义的变量就会报引用错误的原因。

let/const 与 var

简单聊聊同是变量声明,两者有何区别?

let 与 const 的区别这里不再赘述

存放位置
从上一结中,我们知道了let/const声明的变量是归属于LexicalEnvironment,而var声明的变量归属于VariableEnvironment

初始化(词法阶段)
let/const在初始化时会被置为<uninitialized>标志位,在没有执行到let xxxlet xxx = ???(赋值行)的具体行时,提前读取变量会报ReferenceError的错误。(这个特性又叫暂时性死区var在初始化时先被赋值为undefined,即使没有执行到赋值行,仍可以读取var变量(undefined)。

块环境记录(块作用域)
在ECMA标准中提到,当遇到BlockCaseBlock时,将会新建一个环境记录,在块中声明的let/const变量、函数、类都存放这个新的环境记录中,这些变量与块强绑定,在块外界则无法读取这些声明的变量。这个特性就是我们熟悉的块作用域。

什么是Block?
被花括号({})括起来的就是块。

Block中的let/const变量仅在块中有效,块外界无法读取到块内变量。var变量不受此限制。

var不管在哪,都会变量提升~

与ES3的区别

如果你了解ES5版本的有关执行上下文的内容,会感到奇怪为啥有关VOAO、作用域、作用域链等内容没有在本文中提及。其实两者概念并不冲突,一个是ES3规范中的定义,而词法环境则是ES6规范的定义。不同时期,不同称呼。

ES3 --> ES6
作用域 --> 词法环境
作用域链 --> outer引用
VO|AO --> 环境记录

你问我该学哪个?立足现在,铭记历史,拥抱未来。

总结

本文关于执行上下文的理论知识比较多,不容易马上吸收理解,建议你逐渐消化、反复阅读理解。当你熟悉了执行上下文和词法环境,相信去理解认识更多JS特性和概念时,会更加轻松容易。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/367946.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

嵌入式常问问题和知识

12、并发和并行的区别&#xff1f; 最本质的区别就是&#xff1a;并发是轮流处理多个任务&#xff0c;并行是同时处理多个任务。 你吃饭吃到一半&#xff0c;电话来了&#xff0c;你一直到吃完了以后才去接&#xff0c;这就说明你不支持并发也不支持并行。 你吃饭吃到一半&…

【ROS学习笔记2】使用vscode开发ROS全流程

【ROS学习笔记2】使用vscode开发ROS全流程 写在前面&#xff0c;本系列笔记参考的是AutoLabor的教程&#xff0c;具体项目地址在 这里 文章目录【ROS学习笔记2】使用vscode开发ROS全流程一、安装终端工具Terminator二、安装VsCode及插件三、使用VsCode开发全流程1、创建工作空…

亚马逊短期疲软,但长期前景乐观

来源&#xff1a;猛兽财经 作者&#xff1a;猛兽财经 由于投资者对亚马逊(AMZN)前景的担忧&#xff0c;导致该公司的股价在过去一年中下跌了39%。然而猛兽财经认为亚马逊近期面临的不利因素只是暂时的&#xff0c;该公司还是有充分的条件可以在医疗保健和物流领域获得重大增长机…

ACM 记忆化搜索

一.记忆化搜索概述 1.概念 搜索是一种简单有效但是效率又很低下的算法结构&#xff0c;其低效的原因主要在于存在很多重叠子问题。而记忆化搜索则是在搜索的基础上&#xff0c;利用数组来记录已经计算出来的重叠子问题状态&#xff0c;进行合理化的剪枝&#xff0c;从而降低时…

高防CDN的知识了解

高防CDN是一种基于CDN&#xff08;内容分发网络&#xff09;技术的网络安全服务&#xff0c;旨在提供高级的防御措施来保护网站或应用程序免受DDoS&#xff08;分布式拒绝服务&#xff09;攻击和其他网络安全威胁。CDN是一种通过将内容分发到全球多个节点来加速网站或应用程序的…

DBC 文件

概述Vector的DBC文件描述了CAN网络的通信规范&#xff0c;通过定义signal可以表示CAN帧中的各个物理信号的含义。通过CANdb Editor软件可以创建和修改DBC文件&#xff0c;一般监控或控制CAN网络内的节点&#xff0c;不需要解析DBC文件里的全部信息&#xff0c;因为有些信息是给…

前端借助Canvas实现压缩base64图片两种方法

一、具体代码 1、利用canvas压缩图片方法一 // 第一种压缩图片方法&#xff08;图片base64,图片类型,压缩比例,回调函数&#xff09;// 图片类型是指 image/png、image/jpeg、image/webp(仅Chrome支持)// 该方法对以上三种图片类型都适用 压缩结果的图片base64与原类型相同// …

02--微信小程序开发流程

开发小程序一般流程&#xff1a;申请小程序帐号安装小程序开发者工具开发小程序提交审核和发布1、注册小程序帐号在微信公众平台官网首页&#xff08;mp.weixin.qq.com&#xff09;点击右上角的“立即注册”按钮。2、填写帐号信息 主体为企业时需要一些信息包括&#xff1a;企业…

狂神说:面向对象(一) —— OOP与方法回顾

OOP详解以类的方式组织代码&#xff0c;以对象的方式组织&#xff08;封装&#xff09;数据什么是面向对象封装 【口袋装数据&#xff0c;留个口&#xff0c;可以用】继承 【儿子和父亲】多态 【同一个事物表现出多种形态】对象和类实际&#xff1a;先有对象后有类代码&#xf…

商城进货记录交易-课后程序(JAVA基础案例教程-黑马程序员编著-第七章-课后作业)

【实验7-2】商城进货记录交易 【任务介绍】 1.任务描述 每个商城都需要进货&#xff0c;而这些进货记录整理起来很不方便&#xff0c;本案例要求编写一个商城进货记录交易的程序&#xff0c;使用字节流将商场的进货信息记录在本地的csv文件中。程序具体要求如下&#xff1a; …

网络编程NIO

Java NIO&#xff08;New IO 或 Non Blocking IO&#xff09;是从Java 1.4 版本开始引入的一个新的IO API&#xff0c;可以替代标准的 Java IO API。NIO 支持面向缓冲区的、基于通道的 IO 操作。NIO 将以更加高效的方式进行文件的读写操作。 非阻塞 IO(NIO) 通过Selector去实…

ASP.NET Core MVC 项目 IOC容器

目录 一&#xff1a;什么是IOC容器 二&#xff1a;简单理解内置Ioc容器 三&#xff1a;依赖注入内置Ioc容器 四&#xff1a;生命周期 五&#xff1a;多种注册方式 一&#xff1a;什么是IOC容器 IOC容器是Inversion Of Control的缩写&#xff0c;翻译的意思就是控制反转。 …

【Azure 架构师学习笔记】-Azure Data Factory (3)-触发器详解-翻转窗口

本文属于【Azure 架构师学习笔记】系列。 本文属于【Azure Data Factory】系列。 接上文【Azure 架构师学习笔记】-Azure Data Factory (2)-触发器 前言 上文中提到触发器的类型有以下4种&#xff0c;其中第一种【计划】是常用的&#xff0c; 与其他工具/服务类似的方式&#…

游戏、广告作底盘,价值直播为引擎,搜狐活在当下

2022年&#xff0c;中国互联网行业迎来了集体性的“中年危机”。 流量见顶、红利耗尽&#xff0c;再加上疫情的影响&#xff0c;国内互联网企业在过去一年真真实实地感受到了寒气。根据工信部数据&#xff0c;2022年&#xff0c;中国规模以上互联网和相关服务企业总收入达1.46…

【异构图笔记,篇章1】RGCN:Modeling Relational Data with Graph Convolutional Networks

【异构图笔记&#xff0c;篇章1】RGCN:Modeling Relational Data with Graph Convolutional Networks论文信息论文要点快览论文内容介绍背景任务RGCN Conv的介绍RGCN的trick论文实验结果实体分类链路预测评价及总结本文仅供学习&#xff0c;未经同意请勿转载 后期会陆续公开关于…

顺序表的增删查改

数据结构 是数据存储的方式&#xff0c;对于不同的数据我们要采用不同的数据结构。就像交通运输&#xff0c;选用什么交通工具取决于你要运输的是人还是货物&#xff0c;以及它们的数量。 顺序存储结构 包括顺序表、链表、栈和队列等。 例如腾讯QQ中的好友列表&#xff0c;…

运动蓝牙耳机什么款式最好、公认最好用的运动耳机推荐

如今大家对于运动越来越热衷&#xff0c;健身意识的逐渐加强&#xff0c;也带动了对运动装备的需求&#xff0c;其中运动蓝牙耳机也成为运动达人不可缺少的一部分了&#xff0c;在运动的过程中增加点音乐元素进来也会增多点动力。所以市面上出现了各种款式不一的运动耳机&#…

渗透测试之局域网信息探测实验

渗透测试之局域网信息探测实验实验目的一、实验原理1.1 SoftPerfect Network Scanner 流量监控软件二、实验环境2.1 操作机器2.2 SoftPerfectNetscan Scanner三、实验步骤1. 解压并运行SoftPerfect Network Scanner软件2. 使用SoftPerfect Network Scanner进行局域网信息探测实…

并发编程学习篇从0-1合集

一、synchronized 一、原子性、有序性、可见性 1.1 原子性 数据库的事务&#xff1a;ACID A&#xff1a;原子性-事务是一个最小的执行的单位&#xff0c;一次事务的多次操作要么都成功&#xff0c;要么都失败。 并发编程的原子性&#xff1a;一个或多个指令在CPU执行过程中…