前言
ECMAScript标准是深入学习JavaScript原理最好的资料,没有其二。
通过增加对ECMAScript语言的理解,理解javascript现象后面的逻辑,提升个人编码能力。
欢迎关注和订阅专栏 重学前端-ECMAScript协议上篇
执行上下文对应着 协议的 9.4 Execution Contexts。
执行上下文的概念
回顾Agent相关章节, Agent代表了一组执行上下文、一个执行上下文栈、当前运行的执行上下文、一个Agent记录以及一个执行线程的集合。 所以执行上下文,执行上下文栈是归于某个Agent的。
执行上下文是一种的抽象概念,是ECMAScript实现用来追踪代码运行时评估的一种机制。
执行上下文堆栈用于跟踪执行上下文,在任何时间点,每个实际执行代码的代理(Agent)最多只有一个执行上下文正在活动。 正在运行的执行上下文始终是这个堆栈的顶部元素。每当控制从与当前运行的执行上下文相关联的可执行代码转移到与该执行上下文不相关联的执行代码时,就会创建新的执行上下文。新创建的执行上下文被推送到堆栈上,并成为正在运行的执行上下文。
想必上面这些描述,大家耳熟能详,那么来点不一样的。
执行上下文的种类
到这里,是不是会有很多人说,偶知道,偶知道。
- 全局执行上下文
- 函数执行上下文
- eval函数执行上下文
实际上,协议中并未明确提到,倒是提到了有四种类型的 code 类型, 前三种确实能对应上执行上下文种类,第四种呢?
Global codeEval CodeFunction codeModule code
协议中一共出现了 6次 new Execution context 即新建执行上下文, 其中五次和代码执行相关。
| 出现章节 | 作用 |
|---|---|
| 9.6 InitializeHostDefinedRealm | 初始化宿主环境Realm |
| 10.3.1 [[Call]] | 内置函数调用 |
| 15.10.3 PrepareForTailCall | 准备 尾递归 调用 |
| 27.5.3.8 CreateIteratorFromClosure | 迭代器 |
| 27.6.3.11 CreateAsyncIteratorFromClosure | 异步迭代器 |
5次 new ECMAScript Code Execution Context
| 出现章节 | 作用 |
|---|---|
| 10.2.1.1 PrepareForOrdinaryCall | 调用普通函数前的准备工作 |
| 16.1.6 ScriptEvaluation | 脚本执行。比如执行某个script标签的脚本。 |
| 16.2.1.6.4 InitializeEnvironment | 模块脚本初始化环境 |
| 16.2.1.6.5 ExecuteModule | 执行模块脚本 |
| 19.2.1.1 PerformEval | 执行 eval 调用。 |
有同志肯定会问,new Execution context 和 new ECMAScript Code Execution Context有啥区别,协议里面有明确提到 Table 26: Additional State Components for ECMAScript Code Execution Contexts, 就 是后者多了三个环境记录: 词法环境,变量环境,私有环境。
个人解读:
为了让开发者编写的代码运行起来,会有内置逻辑,自然也有相应的内置函数/代码 ,其也是遵循执行上下文创建,入栈 和出栈的机制的。
new Execution context:是为了协议内部代码/内部函数运行,或者某些机制(例如迭代器) 服务的。
new ECMAScript Code Execution Context: 是为了开发者编写的代码/函数 运行服务的。
从上面表格,重新整理一下,如下场景创建新的执行上下文:
-
初始化宿主环境Realm的时候,会创建执行上下文,即通常意义的全局执行上下文
-
模块脚本初始化的时候, 会创建执行上下文
-
脚本(普通脚本和模块脚本)执行的时候,会创建上下文
- 16.1.6 ScriptEvaluation (普通脚本)
- 16.2.1.6.5 ExecuteModule (模块脚本)
-
函数(内置函数,普通函数,尾递归函数)执行的时候,会创建上下文
- 10.3.1 [[Call]] (内置函数)
- 15.10.3 PrepareForTailCall (尾递归函数)
- 10.2.1.1 PrepareForOrdinaryCall (普通函数)
-
Eval函数执行的上下文
-
创建迭代器或者异步迭代器,会创建上下文
至于迭代器,参见内置的 JavaScript 迭代器
注意上面的 Genetator 也是内置的迭代器, 而 async/await语法是Generator函数的语法糖, 你会想到什么呢?
执行上下文栈
执行上下文栈 用于跟踪执行上下文。运行执行上下文始终是该栈的顶部元素。每当控制权从当前正在运行的执行上下文关联的可执行代码转移到不与该执行上下文关联的其他可执行代码时,就会创建一个新的执行上下文。新创建的执行上下文会被推送到栈顶,并成为运行执行上下文。
开发者工具,有个 调用堆栈 可以和这个对应
当然,执行上下文栈 也是有上限的,超过了,就是所谓的爆栈。
如何知道执行上下栈的大小呢? 简简单单的封装个函数
javascript
复制代码
function getStackSize() {
var size = 0;
function fn() {
size++;
fn();
}
try {
fn();
} catch (ex) {
console.log("size=>" + size, "err=>" + ex);
}
return size;
}
getStackSize();
比如我的Chrome 123.0.6312.123(正式版本) (64 位)的栈大小是就 12535:
执行上下文的管理
基本流程
已经知道每次执行函数,都会创建新的执行上下文,一起来分析下面的代码,调用堆栈和执行上下文的切换情况。
javascript
复制代码
<script>
function outer(){
var outerVal = 'outerVal';
inner();
console.log(outerVal);
}
function inner(){
var innerVal = 'innerVal';
console.log(innerVal);
}
outer();
// innerVal
// outerVal
</script>
直接借用 chrome的开发者工具查看调用堆栈
-
准备调用outer
- 匿名的执行上下文
-
outer执行
- outer执行上下文
- 匿名执行上下文
-
inner 执行
- inner 执行上下文
- outer执行上下文
- 匿名 执行上下文
-
inner 执行完毕,回归outer
- outer执行上下文
- 匿名 执行上下文
-
outer 执行完毕
- 匿名 执行上下文
- 全部脚本执行完毕
这里抛出一个问题,整个流程中多次出现的 匿名 上下文是不是全局执行上下文??
答案是否定的, 匿名执行上下文是 执行 script脚本(对应着HTML的script标签)创建的, 即16.1.6 ScriptEvaluation , 也即
根据上面的调用堆栈的变化过程(每次函数调用都会创建执行上下文),整理如下(顶部红色是正在执行的代码):
模块脚本的加载
先看看下面的代码,输出是什么呢
html
复制代码
<!DOCTYPE html>
<html lang="en">
<body>
<script type="module">
console.log("index.html script");
import * as math from "./math.mjs";
; (function () {
const result = math.sum(1, 2);
console.log('result:', result);
})();
</script>
</body>
</html>
javascript
复制代码
// math.mjs
console.log("math: module");
export function sum(a , b){
return a + b;
}
模块的加载是优先被执行的, 最终的输出如下:
执行的截图如下:
重新用图来表示整个过程如下(顶部红色是正在执行的代码):
从上可以明确的看到,模块代码被执行的时候,是有自己的执行上下文的。
多个Realm场景
在 Agent 的章节已提到, 一个Agent有一个执行线程和执行上下文堆栈,同时一个Agent可以有多个Realm,接下来一起看看多Realm示例。
javascript
复制代码
<!DOCTYPE html>
<html lang="en">
<body>
<iframe src="./iframe.html" id="ifr"></iframe>
<script>
const ifrEl = document.querySelector("#ifr");
ifrEl.onload = function onload() {
const ifrWindow = ifrEl.contentWindow;
const result = ifrWindow.sum(1, 2);
console.log("result:", result);
console.log("same Array?:", Array === ifrWindow.Array)
}
</script>
</body>
</html>
javascript
复制代码
// iframe.html
<!DOCTYPE html>
<html lang="en">
<body>
<script>
var sum = function sum(a, b) {
return a + b;
}
</script>
</body>
</html>
- onload 函数的下面还包裹了一个执行上下文
根据上面调用堆栈的变化,整理流程如下:
- sum函数的执行上下文 关联的Realm是
iframe.html创建的, 这里论证了多个页面会共享一个执行执行线程和 执行上下文堆栈。 - 此外异步函数的回调,都会多包裹函数调用,当然也会多一个执行上下文。
执行上下文的主要功能
从菜单可以看到协议定义了 执行上下文操作也可以得知。
| 方法 | 功能 |
|---|---|
| GetActiveScriptOrModule ( ) | 获取脚本记录或者模块记录。这些记录含了关于脚本或模块的元数据信息,比如模块的顶级变量、导出和导入声明、脚本的源代码位置等 |
| ResolveBinding ( name [ , env ] ) | 通过标志符名找到对应的引用记录。 |
| GetThisEnvironment ( ) | 获取提供this关键字绑定关系的环境记录。比如箭头函数 的环境记录是没有 this绑定关系的。 |
| ResolveThisBinding ( ) | 从当前执行中的上下文中 的 环境记录中 获得 this 的值。就是先调用GetThisEnvironment ( )获取有this的环境记录,然后取this的值。 |
| GetNewTarget ( ) | 即开发者常见的 new.target, 用于检测函数是否通过new关键字被调用。 |
| GetGlobalObject ( ) | 从上下文的关联的Realm中获取全局对象。 |
new.target 示例
javascript
复制代码
function Person(name) {
// 检查new.target是否存在,即是否通过new调用
if (new.target !== undefined) {
this.name = name;
console.log("通过new调用,创建新对象:", this);
} else {
throw new Error("必须通过new关键字调用来创建Person实例");
}
}
执行上下文的功能
- 获取脚本记录
- 标志符查找
- 获取 有 this 的环境记录和获取 this的值
- 获取全局对象
- 获取
new.target - 等等
一切都是为了 函数(代码) 执行。
执行上下文, Realm,Agent, Agent Cluster
在之前的文章中有 Realm, Agent , Agent Cluster的关系图,现在引入了执行上下文,关系图也会发生一些变化。
引入调用堆栈, 重新整理图,如下:
圈在执行上下文里的各元素或者模块,不一定表示包含,也可能是引用,比如Realm。
引用
上一章
下一章