ECMAScript 执行上下文

125 阅读8分钟

前言

ECMAScript标准是深入学习JavaScript原理最好的资料,没有其二。

通过增加对ECMAScript语言的理解,理解javascript现象后面的逻辑,提升个人编码能力。

欢迎关注和订阅专栏 重学前端-ECMAScript协议上篇

执行上下文对应着 协议的 9.4 Execution Contexts

执行上下文的概念

回顾Agent相关章节, Agent代表了一组执行上下文、一个执行上下文栈、当前运行的执行上下文、一个Agent记录以及一个执行线程的集合。 所以执行上下文,执行上下文栈是归于某个Agent的。

执行上下文是一种的抽象概念,是ECMAScript实现用来追踪代码运行时评估的一种机制。

执行上下文堆栈用于跟踪执行上下文,在任何时间点,每个实际执行代码的代理(Agent)最多只有一个执行上下文正在活动。 正在运行的执行上下文始终是这个堆栈的顶部元素。每当控制从与当前运行的执行上下文相关联的可执行代码转移到与该执行上下文不相关联的执行代码时,就会创建新的执行上下文。新创建的执行上下文被推送到堆栈上,并成为正在运行的执行上下文。

想必上面这些描述,大家耳熟能详,那么来点不一样的。

执行上下文的种类

到这里,是不是会有很多人说,偶知道,偶知道。

  • 全局执行上下文
  • 函数执行上下文
  • eval函数执行上下文

实际上,协议中并未明确提到,倒是提到了有四种类型的 code 类型, 前三种确实能对应上执行上下文种类,第四种呢?

  • Global code
  • Eval Code
  • Function code
  • Module 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: 是为了开发者编写的代码/函数 运行服务的。

从上面表格,重新整理一下,如下场景创建新的执行上下文:

至于迭代器,参见内置的 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;
}

模块的加载是优先被执行的, 最终的输出如下:

执行的截图如下:

重新用图来表示整个过程如下(顶部红色是正在执行的代码):

image.png

从上可以明确的看到,模块代码被执行的时候,是有自己的执行上下文的。

多个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。

引用

上一章

下一章