首页 > 其他分享 >JS词法环境和执行上下文

JS词法环境和执行上下文

时间:2022-10-25 13:12:20浏览次数:69  
标签:上下文 函数 环境 JS 词法 执行 变量

前言

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错误
FunctionObject Object 在这个对象中有两个属性[[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

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

标签:上下文,函数,环境,JS,词法,执行,变量
From: https://www.cnblogs.com/hellocoder2029/p/16824503.html

相关文章

  • JS知识点梳理之作用域、作用域链、柯里化、闭包
    一、作用域与作用域链作用域是指js变量使用时所存在的一个区域,分为全局作用域(window)和局部作用域(function、setTimeout...等都会产生局部作用域)。当局部作用域变量名与......
  • js异步编程,eventLoop,消息队列,宏任务,微任务
    1.单线程的JavaScriptJavaScript是一门单线程语言,起因是设计之初js只用来操作dom,对表单进行简单的校验。在这种执行环境简单的情况下,自然就选择了单线程来处理程序......
  • Node.js实现大文件断点续传
    前言平常业务需求:上传图片、Excel等,毕竟几M的大小可以很快就上传到服务器。针对于上传视频等大文件几百M或者几G的大小,就需要等待比较长的时间。这就产生了对应的解决方......
  • mockJS与file-saver依赖的冲突问题
    今天在使用POST下载并使用file-saver中的saveAs保存文件时意外地发现保存下来的文件直接报错说文件损坏,如下图Excel为例:网上查阅大量方法,经过切换后端服务器,改为上线项目......
  • Nodejs+Redis实现简易消息队列
    前言消息队列是存储数据的一个中间件,可以理解为一个容器。生产者生产消息投递到队列中,消费者可以拉取消息进行消费,如果消费者目前没有消费的打算,则消息队列会保留消息,直......
  • Flask学习笔记(十五)-Flask 上下文详解
    一、上下文说明上下文:在程序中可以理解为在代码执行到某一时刻时,根据之前代码所做的操作以及下文即将要执行的逻辑,可以决定在当前时刻下可以使用到的变量,或者可以完成的事......
  • JS 中为什么要有 Iterator,JS 中数组,对象,Map,Set遍历的推荐方法
    JavaScript原有的表示“集合”的数据结构主要是数组(Array)和对象(Object),ES6又添加了Map和Set。这样就有了4种数据集合,用户还可以组合使用它们,定义自己的数据......
  • Gin-JSON,ProtoBuf渲染
    1.输出json和protobuf新建user.proto文件 syntax="proto3";optiongo_package=".;proto";messageTeacher{stringname=1;repeatedstringcour......
  • 创建一个json-server服务器
    json-server官网https://www.npmjs.com/package/json-server#getting-started1全局安装json-servernpminstall-gjson-server2创建json文件存储数据//db.......
  • 我把一个json格式的数据读到dataframe里面了 怎么解析出自己需要的字段呢?
    大家好,我是皮皮。一、前言前几天在Python最强王者交流群【WYM】问了一个pandas处理的问题,提问截图如下:原始数据如下图所示:后来还提供了一个小文件。二、实现过程......