前言

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 + thisER分类 = 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个性和概念时,会更加轻松容易。