前言
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
。
之后,实现 bar
和foo
函数调用,会顺次将上下文出栈,直至全局上下文出栈,程序完结运行。
执行上下文的创立
执行上下文创立会做两件事件:
- 创立词法环境
LexicalEnvironment
; - 创立变量环境
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
等)或全局函数 (eval
、parseInt
等),其余申明的全局变量或函数也会存储在全局词法环境中。全局环境的outer
援用为null
。
这里提及的全局对象就有咱们相熟的所有内置对象,如 Math、Object、Array 等构造函数,以及 Infinity 等全局变量。全局函数则蕴含了 eval、parseInt 等函数。
- 模块环境 (
ModuleEnvironment
):你若写过 NodeJs 程序就会很相熟这个环境,在模块环境中你能够读取到export
、module
等变量,这些变量都是记录在模块环境的 ER 中。模块环境的outer
援用指向全局环境。 - 函数环境 (
FunctionEnvironment
):每一次调用函数时都会产生函数环境,在函数环境中会波及this
的绑定或super
的调用。在 ER 中也会记录该函数的length
和arguments
属性。函数环境的outer
援用指向调起该函数的父环境。在函数体内申明的变量或函数则记录在函数环境中。参考视频解说:进入学习
环境记录 ER
代码中申明的变量和函数都会寄存在 EnvironmentRecord
中期待执行时拜访。
环境记录 EnvironmentRecord
也有两个不同类型,别离为 declarative
和object
。declarative
是较为常见的类型,通常函数申明、变量申明都会生成这种类型的 ER。object
类型能够由 with
语句触发的,而 with
应用场景很少,个别开发者很少用到。
如果你在函数体中遇到诸如 var const let class module import 函数申明
,那么环境记录就是declarative
类型的。
值得一提的是全局上下文的 ER
有一点非凡,因为它是 object ER
与declarative ER
的混合体。在 object ER
中寄存的是全局对象函数、function 函数申明、async
、generator
、var
关键词变量。在 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]] ,而通过new 或super 调用函数则赋值[[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 中则提倡应用 let
和const
来申明变量,为了兼容 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 xxx
或 let xxx = ???
(赋值行)的具体行时,提前读取变量会报ReferenceError
的谬误。(这个个性又叫 暂时性死区
)var
在初始化时先被赋值为 undefined
,即便没有执行到赋值行,仍能够读取var
变量(undefined
)。
块环境记录 (块作用域)
在 ECMA 规范中提到,当遇到 Block
或CaseBlock
时,将会新建一个环境记录,在块中申明的 let/const
变量、函数、类都寄存这个新的环境记录中,这些变量与块 强绑定,在块外界则无奈读取这些申明的变量。这个个性就是咱们相熟的块作用域。
什么是 Block?
被花括号 ({}) 括起来的就是块。
在 Block
中的 let/const
变量仅在块中无效,块外界无奈读取到块内变量。var
变量不受此限度。
var
不论在哪,都会变量晋升~
与 ES3 的区别
如果你理解 ES5 版本的无关执行上下文的内容,会感到奇怪为啥无关VO
、AO
、作用域、作用域链等内容没有在本文中提及。其实两者概念并不抵触,一个是 ES3 标准中的定义,而词法环境则是 ES6 标准的定义。不同期间,不同称说。
ES3 –> ES6
作用域 –> 词法环境
作用域链 –> outer 援用
VO|AO –> 环境记录
你问我该学哪个?立足当初,铭刻历史,拥抱将来。
总结
本文对于执行上下文的理论知识比拟多,不容易马上排汇了解,倡议你逐步消化、重复浏览了解。当你相熟了执行上下文和词法环境,置信去了解意识更多 JS 个性和概念时,会更加轻松容易。