写点什么

浅谈对 JavaScript 中的执行上下文和执行栈的理解

作者:梁木由
  • 2023-03-05
    北京
  • 本文字数:3402 字

    阅读完需:约 11 分钟

浅谈对JavaScript 中的执行上下文和执行栈的理解

大家好,金三银四马上也快到了,总听说行情不好,面试不好面,不过好像也没什么太大关系,该换新工作就换,只要准备充分还怕它什么行情不好。笔者呢最近也有想法所以再回顾 JavaScript 知识时,又看到了 JavaScript 的执行上下文


那么这篇文章呢一小部分内容是我自己的一些理解。


大部分内容来自[译] 理解 JavaScript 中的执行上下文和执行栈


原文地址:Understanding Execution Context and Execution Stack in Javascript

例题

大家先来看一道较为简单的题,看下是否能看出来结果


var a = 10;function fn(b) {  b = 20;  console.log(a, b);}function fn1() {  a = 100;  fn(a);}fn(200); //输出结果fn1(); // 输出结果
复制代码


大家可以看出来输出结果是什么吗?


如果你已经算出来的话,那么说明你对执行上下文还是有一些理解的,欢迎继续往下看加深印象


如果你没算出来或者输出结果与你算的不相符,那也先不要着急,先看下边内容,看完后再回来算

执行上下文

概念

大家都知道,JavaScript 代码的在运行的时候都是自上而下按顺序执行的,但是呢实际并非是一行一行的执行,那大家有没有了解过它在执行代码的时候做过哪些准备,做过哪些事情,比如代码解析、分配内容都是在哪处理的,那这个地方呢就是执行上下文,是准备工作的所在环境

执行上下文类型

执行上下文呢有三种类型,分别是


  • 全局执行上下文

  • 函数执行上下文

  • 还有就是 eval 函数执行上下文


那么我们继续,执行上下文呢是在代码编译阶段创建的,来看看执行上下文的生命周期

执行上下文生命周期

  • 创建阶段

  • 执行阶段

创建阶段

执行上下文的创建阶段具体做了什么事呢,又分为三部分


ExecutionContext = {  ThisBinding = <this value>,  LexicalEnvironment = { ... },  VariableEnvironment = { ... },}
复制代码
确定 this 指向

在全局执行上下文中,this 指向的是全局对象


在函数执行上下文中,this 指向取决于该函数是如何被调用的


看下这个 demo


const obj = {  fn: function(){    console.log(this)  }}obj.fn(); //fn: f();const func = obj.fn;func(); // Window
复制代码
词法环境

官方的 ES6 文档把词法环境定义为


词法环境是一种规范类型,基于 ECMAScript 代码的词法嵌套结构来定义标识符和具体变量和函数的关联。一个词法环境由环境记录器和一个可能的引用外部词法环境的空值组成。


简单来说词法环境是一种持有标识符—变量映射的结构。(这里的标识符指的是变量/函数的名字,而变量是对实际对象[包含函数类型对象]或原始数据的引用)。


现在,在词法环境的内部有两个组件:(1) 环境记录器和 (2) 一个外部环境的引用


  1. 环境记录器是存储变量和函数声明的实际位置。

  2. 外部环境的引用意味着它可以访问其父级词法环境(作用域)。


词法环境有两种类型:


  • 全局环境(在全局执行上下文中)是没有外部环境引用的词法环境。全局环境的外部环境引用是 null。它拥有内建的 Object/Array/等、在环境记录器内的原型函数(关联全局对象,比如 window 对象)还有任何用户定义的全局变量,并且 this的值指向全局对象。

  • 函数环境中,函数内部用户定义的变量存储在环境记录器中。并且引用的外部环境可能是全局环境,或者任何包含此内部函数的外部函数。


环境记录器也有两种类型(如上!):


  1. 声明式环境记录器存储变量、函数和参数。

  2. 对象环境记录器用来定义出现在全局上下文中的变量和函数的关系。


简而言之,


  • 全局环境中,环境记录器是对象环境记录器。

  • 函数环境中,环境记录器是声明式环境记录器。


注意 — 对于函数环境声明式环境记录器还包含了一个传递给函数的 arguments 对象(此对象存储索引和参数的映射)和传递给函数的参数的 length


抽象地讲,词法环境在伪代码中看起来像这样:


GlobalExectionContext = {  LexicalEnvironment: {    EnvironmentRecord: {      Type: "Object",      // 在这里绑定标识符    }    outer: <null>  }}FunctionExectionContext = {  LexicalEnvironment: {    EnvironmentRecord: {      Type: "Declarative",      // 在这里绑定标识符    }    outer: <Global or outer function environment reference>  }}
复制代码
变量环境

它同样是一个词法环境,其环境记录器持有变量声明语句在执行上下文中创建的绑定关系。


如上所述,变量环境也是一个词法环境,所以它有着上面定义的词法环境的所有属性。


在 ES6 中,词法环境组件和变量环境的一个不同就是前者被用来存储函数声明和变量(letconst)绑定,而后者只用来存储 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>  }}
复制代码


注意 — 只有遇到调用函数 multiply 时,函数执行上下文才会被创建。


可能你已经注意到 letconst 定义的变量并没有关联任何值,但 var 定义的变量被设成了 undefined


这是因为在创建阶段时,引擎检查代码找出变量和函数声明,虽然函数声明完全存储在环境中,但是变量最初设置为 undefinedvar 情况下),或者未初始化(letconst 情况下)。


这就是为什么你可以在声明之前访问 var 定义的变量(虽然是 undefined),但是在声明之前访问 letconst 的变量会得到一个引用错误。


这就是我们说的变量声明提升。

执行阶段

这是整篇文章中最简单的部分。在此阶段,完成对所有这些变量的分配,最后执行代码。


注意 — 在执行阶段,如果 JavaScript 引擎不能在源码中声明的实际位置找到 let 变量的值,它会被赋值为 undefined

执行栈

那根据上述执行上下文的理解,那我们知道在执行代码中会有很多的执行上下文,那么执行上下文是怎么确定执行顺序的。


执行上下文存放的位置就是在执行上下文栈,也叫调用栈。具有 LIFO(Last In First Out 后进先出,也就是先进后出)的特性。


那我们来看下之前的例题,来分析下


var a = 10;function fn(b) {  b = 20;  console.log(a, b);}function fn1() {  a = 100;  fn(a);}fn(200); //输出结果fn1(); // 输出结果
复制代码


  1. 首先进入全局执行环境,创建全局执行上下文环境并加入栈中

  2. fn()函数被调用,进入对应的函数执行环境,创建函数执行环境并加入栈

  3. 执行 console.log(a, b);代码

  4. console.log(a, b);代码出栈

  5. fn()函数执行完毕后出栈

  6. fn1()函数被调用,进入对应的函数执行环境,创建函数执行环境并加入栈

  7. 继续 fn()函数被调用,进入对应的函数执行环境,创建函数执行环境并加入栈

  8. 执行 console.log(a, b);代码

  9. console.log(a, b);代码出栈

  10. fn()函数执行完毕后出栈

  11. fn1()函数出栈

  12. 全局执行上下文出栈


题解

那我们再来分析下例题的答案


var a = 10;function fn(b) {  b = 20;  console.log(a, b);}fn(200);
复制代码


在执行 fn 函数时,此 fn 活动对象为


AO : {  a: 10,  b: 20,  arguments: {0 : 20, length:0} }
复制代码


所以此时输出结果为 10,20


继续看


var a = 10;function fn(b) {  b = 20;  console.log(a, b);}function fn1() {  a = 100;  fn(a);}fn1();
复制代码


在执行 fn1 函数时,此 fn1 活动对象为


AO : {  a: 100,  fn: reference to function fn(){}  arguments: {length: 0} }
复制代码


在继续执行 fn 函数时,此 fn 活动对象为


AO : {  a: 100,  b: 20,  arguments: {0 : 20, length:0} }
复制代码


所以此时输出结果为 100,20

结语

如果感觉此文的大屏数据交互方式对你帮助的话,请不吝点个赞🥺🥺🥺,支持一下

发布于: 2023-03-05阅读数: 30
用户头像

梁木由

关注

公众号:前端新气象 2023-01-16 加入

一个有想头的前端,对2023年充满希望、怀抱期待,相信自己做个自律的人。要学习,拒绝躺平,从我做起。⛽️

评论 (1 条评论)

发布
用户头像
如果你有10%的录取通过率,那么只要保证一个月有10家面试机会,那就能在一个月里搞定工作。所以,该换就换。
2023-03-05 22:51 · 广东
回复
没有更多了
浅谈对JavaScript 中的执行上下文和执行栈的理解_梁木由_InfoQ写作社区