前言
可以通过我下面两篇文章简单的了解一下,作用域相关的概念:
在这篇文章中我们主要是对 执行上下文、执行栈 做个介绍。本文是基于 ES6
一、什么是执行上下文
官方一点地说,执行上下文(Execution context stack
简称 ECS
)就是一个评估和执行JavaScript
代码的环境的抽象概念。通俗地说,就是每当 Javascript
代码在运行的时候,它都是在执行上下文中运行。
通俗的解释,就是一段代码执行时所带的所有信息。包括变量、函数声明、参数(arguments)、作用域链、this等信息。
在不同 ECMAScript 版本中执行上下文所代表的含义:
- ES3
- scope:作用域,作用域链
- variable object: 变量对象,用来存储变量的对象
- this value: this 值
- ES5,改进了命名方式,把执行上下文最初的三个部分改成下面的样子
- lexical environment:词法环境,当获取变量时使用
- variable environment:变量环境,当声明变量时使用
- this value: this 值
- ES2018,this 值被归入 lexical environment,但是增加了不少内容
- lexical environment:词法环境,当获取变量或者 this 值时使用
- variable environment:变量环境,当声明变量时使用
- code evaluation state: 用于恢复代码执行位置
- Function:执行的任务是函数时使用,表示正在被执行的函数
- ScriptOrModule:执行的任务是脚本或者模块时使用,表示正在被执行的代码
- Realm:使用的基础库和内置对象实力
- Generator:仅生成器上下文有这个属性,表示当前生成器
二、执行上下文的类型
JavaScript中有三种执行上下文类型:
- 全局执行上下文:一个程序中只有一个全局执行上下文,任何不在函数内部的代码,都属于全局上下文,也就意味着
this
的指向是window
对象(浏览器环境下) - 函数执行上下文:是在函数被调用的时候创建的,函数上下文可以有很多个,调用一次函数生成一个,执行顺序是函数调用的顺序
- eval执行上下文:是在执行
eval
函数内部的代码是会有子级的执行上下文(在日常开发不会用到)
三、执行上下文的生命周期
执行上下文的生命周期包括:创建阶段 -> 执行阶段 -> 回收阶段
3.1 创建阶段
在JavaScript
代码执行前,执行上下文将经历创建阶段。在创建阶段需要做的三件事:
this
值的决定,就是this绑定- 创建此法环境组件(lexical environment)
- 创建变量环境组件(variable environment)
1) this绑定
- 在全局执行上下文中,
this
对象指向全局对象(浏览器环境值得是window) - 在函数执行上下文中,
this
取决于该函数是否调用,且被什么调用。- 如果是一个引用对象调用,那么this会被设置成那个对象
- 如果函数没有被调用,那么此时this的值会被设置成
全局对象或者undefined
严格模式下,如果函数没有被绑定在任何对象上,函数执行上下文中的this会被设置为
undefined
。这是因为在这种情况下,函数没有任何上下文可依赖,因此this被设置为undefined
let foo = {
baz: function() {
console.log(this);
}
}
foo.baz(); // 'this' 引用 'foo', 因为 'baz' 被
// 对象 'foo' 调用
let bar = foo.baz;
bar(); // 'this' 指向全局 window 对象,因为
// 没有指定引用对象
2) 词法环境
官方的 ES6[1] 文档把词法环境定义为
词法环境是一种规范类型,基于 ECMAScript 代码的词法嵌套结构来定义标识符和具体变量和函数的关联。一个词法环境由环境记录器和一个可能的引用outer词法环境的空值组成。
简单来说,词法环境是一种持有标识符—变量映射的结构。(这里的标识符指的是变量/函数的名字,而变量是对实际对象[包含函数类型对象]或原始数据的引用)。
-
词法环境的内部有两个组件:(1)环境记录器;(2)一个外部环境的引用
- 环境记录器:是存储变量和函数声明的实际位置
- 外部环境的引用:意味着它可以访问其父级词法环境(作用域)
-
词法环境的类型:
- 全局执行上下文中是没有外部环境引用的词法环境。
- 全局环境的外部环境引用是
null
。它拥有内建的 Object/Array等 - 在环境记录器内的原型函数(关联全局对象,比如window对象),还有任何用户定义的全局变量,并且this的值指向全局对象
- 全局环境的外部环境引用是
- 函数执行上下文中,函数内部用户定义的变量存储在环境记录器中。并且引用的外部环境可能是全局环境,或者任何包含此内部函数的外部函数
- 全局执行上下文中是没有外部环境引用的词法环境。
-
环境记录器的类型:在全局环境中,环境记录器是对象环境记录器;在函数环境中,环境记录器是声明式环境记录器
-
声明式环境记录器存储变量、函数和参数
-
对象环境记录器用来定义出现在全局上下文中的变量和函数的关系
注意 — 对于函数环境,声明式环境记录器还包含了一个传递给函数的
arguments
对象(此对象存储索引和参数的映射)和传递给函数的参数的 length。 -
3) 变量环境
同样是一个词法环境,其环境记录器持有变量声明语句在执行上下文中创建的绑定关系
如上所述,变量环境也是一个词法环境,所以它有这上面定义的词法环境的所有属性
在ES6中,词法环境组件和变量环境的一个不同点,就是前者被用来存储函数声明和变量(let
和 const
)绑定,而后者只用来存储 var
变量绑定。
示例代码:
let a = 20;
const b = 30;
var c;
function multiply(e, f) {
var g = 20;
return e * f * g;
}
c = multiply(20, 30);
执行上下文看起来像这样:
GlobalExectionContext = {
ThisBinding: <Global Object>,
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 在这里绑定标识符
a: < uninitialized >,
b: < uninitialized >,
multiply: < func >
}
outer: <null>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Object",
// 在这里绑定标识符
c: undefined,
}
outer: <null>
}
}
FunctionExectionContext = {
ThisBinding: <Global Object>,
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 在这里绑定标识符
Arguments: {0: 20, 1: 30, length: 2},
},
outer: <GlobalLexicalEnvironment>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 在这里绑定标识符
g: undefined
},
outer: <GlobalLexicalEnvironment>
}
}
3.2 执行阶段
在此阶段,完成对所有这些变量的分配,最后执行代码
注意: 在执行阶段,如果
JavaScript
引擎不能在源码中声明的实际位置找到 let 变量的值,它会被赋值为 undefined
3.3 回收阶段
执行上下文出栈等待虚拟机回收执行上下文
注意 : 在执行阶段,如果 JavaScript 引擎不能在源码中声明的实际位置找到 let
变量的值,它会被赋值为 undefined
。
四、执行栈
4.1 什么是执行栈
执行栈,也就是在其它编程语言中的“调用栈”,是一种拥有 LIFO(后进先出)的数据结构,被用来存储代码运行时创建的所有执行上下文。
当 JavaScript 引擎第一次遇到你的脚本时,它会创建一个全局的执行上下文并且压入当前执行栈。每当引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈的顶部。
引擎会执行处于栈顶的执行上下文的函数。当该函数执行结束时,执行上下文从栈中弹出,控制流程到达当前栈中的下一个上下文。
4.2 删除代码在浏览器加载时的执行过程
下面我们通过一个示例来理解:
let a = 'Hello World!';
first() {
console.log('Inside first function');
second();
console.log('Again inside first function');
}
second() {
console.log('Inside second function');
}
first();
console.log('Inside Global Execution Context');
大致流程如下:
- JavaScript 引擎创建一个 全局执行上下文,并把它压入当前执行栈
- 当遇到
first()
函数调用时,JavaScript引擎为该函数创建一个新的执行上下文,并把它压入当前执行栈的顶部 - 在
first()
内部调用second()
函数时,JavaScript 引擎为second()
函数创建了一个新的执行上下文,并把它压入当前执行栈的顶部 - 当
second()
函数执行完毕,它的执行上下文会从当前栈弹出 - 控制流程到达下一个执行上下文,即
first()
函数的执行上下文 - 当
first()
执行完毕,它的执行上下文从栈弹出 - 控制流程到达全局执行上下文
- 所有代码执行完毕,JavaScript 引擎从当前栈中移除全局执行上下文。
4.3 查看调用栈的信息
调用栈是 JavaScript 引擎追踪函数执行的一个机制。
-
使用浏览器查看调用栈信息的方式:
- 打开“开发者工具”
- 点击“Source”标签
- 选择 JavaScript 代码的页面,然后在函数内部加上断点,并刷新页面
- 可以看到执行到该函数时,执行流程就暂停了
- 通过右边“调用堆栈”来查看当前的调用栈的情况
- 栈的最底部就是全局的函数入口
-
通过命令:
console.trace()
输出var a = 2; function add(b, c) { console.trace(); // 输出当前的函数调用关系 return b+c; } function addAll(b, c) { var d = 10; return a + add(b, c) + d; } addAll(1, 1)
打印出函数调用关系:
五、总结
5.1 程序执行过程
- 程序启动,全局执行上下文被创建,压入调用栈
- 创建全局上下文的 词法环境
- 创建 对象环境记录器 。它用来定义出现在 全局上下文 中的变量和函数的关系(负责处理
let
和const
定义的变量) - 创建 外部环境引用。值为
null
- 创建 对象环境记录器 。它用来定义出现在 全局上下文 中的变量和函数的关系(负责处理
- 创建全局上下文的 变量环境
- 创建 对象环境记录器 。它持有 变量声明语句 在执行上下文中创建的绑定关系(负责处理
var
定义的变量,初始值为undefined
造成声明提升) - 创建 外部环境引用。值为
null
- 创建 对象环境记录器 。它持有 变量声明语句 在执行上下文中创建的绑定关系(负责处理
- 确定
this
值为全局对象(以浏览器为例,就是window
)
- 创建全局上下文的 词法环境
- 函数被调用,函数执行上下文被创建,压入调用栈
- 创建函数上下文的 词法环境
- 创建 声明式环境记录器 。存储变量、函数和参数,它包含了一个传递给函数的
arguments
对象(此对象存储索引和参数的映射)和传递给函数的参数的 length。(负责处理let
和const
定义的变量) - 创建 外部环境引用,值为全局对象,或者为父级词法环境(作用域)
- 创建 声明式环境记录器 。存储变量、函数和参数,它包含了一个传递给函数的
- 创建函数上下文的 变量环境
- 确定
this
值
- 创建函数上下文的 词法环境
- 进入函数执行上下文的执行阶段
- 在上下文中运行/解释函数代码,并在代码逐行执行时分配变量值。
5.2 JavaScript的阶段
JavaScript是解释型语言,JavaScript的执行分为解释和执行两个阶段,并且这两个阶段所做的事情不同:
- 解释阶段
- 词法分析
- 语法分析
- 作用域规则确定
- 执行阶段
- 创建执行上下文
- 执行函数代码
- 垃圾回收
JavaScript解释阶段便会确定作用域规则,因此作用域在函数定义时就已经确定了,而不是在函数调用时确定,但是执行上下文是函数执行之前创建的。执行上下文最明显的就是this的指向是执行时确定的。而作用域访问的变量是编写代码的结构确定的。
作用域和执行上下文之间最大的区别是: 执行上下文在运行时确定,随时可能改变;作用域在定义时就确定,并且不会改变。
一个作用域下可能包含若干个上下文环境。有可能从来没有过上下文环境(函数从来就没有被调用过);有可能有过,现在函数被调用完毕后,上下文环境被销毁了;有可能同时存在一个或多个(闭包)。同一个作用域下,不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值。
简要概况一下作用域
,词法环境
,执行上下文
这三者的概念:
- 作用域:作用域就是一个独立的区域,它可以让变量不会向外暴露出去。作用域最大的用处就是隔离变量。内层作用域可以访问外层作用域。一个作用域下可能包含若干个执行上下文。
- 词法环境:指相应代码块内标识符与变量值、函数值之间的关联关系的一种体现。词环境内部包含环境记录器和对外部环境的引用。环境记录器是存储变量和函数声明的实际位置,对外部环境的引用意味着可以访问父级词法环境。
- 执行上下文:JavaScript代码运行的环境。分为全局执行上下文,函数执行上下文和eval函数执行上下文(前两个较常见)。创建执行上下文时会进行this绑定、创建词法环境和变量环境。