你不知道的JavaScript系列——执行上下文,调用栈解析

JavaScript 的代码执行机制宛如一台精密的机器,而执行上下文调用栈 正是这台机器的核心齿轮。大家好,我是kada,这篇文章将带您深入了解执行上下文词法环境变量环境调用栈作用域之间的交互体验。

一、执行上下文:JavaScript 的代码执行单元

1. 执行上下文的类型

  • 全局执行上下文:代码的起点,创建全局对象(浏览器中为 window)
  • 函数执行上下文:每次函数调用时动态创建
  • Eval 执行上下文(较少使用)

2. 执行上下文的生命周期

每个执行上下文经历两个阶段:

  • 创建阶段(编译时):
  1. 创建变量环境(VariableEnvironment):存储 var 声明和函数声明
  2. 创建词法环境(LexicalEnvironment):存储 let/const 声明
  3. 建立 outer 引用(作用域链)
  4. 确定 this 绑定
  • 执行阶段 (运行时):
    按顺序执行代码,进行赋值操作

二、关键组件解析

1. 变量环境 vs 词法环境

  • 变量环境(Variable Environment)

    • 存储 var 声明和函数声明

    • 在函数/全局作用域初始化时完全创建

    • 表现为对象环境记录(Object Environment Record)

    javascript 复制代码
    // 伪代码实现
    variableEnvironment = {
      a: undefined,
      c: undefined // 块内的 var 同样提升到函数作用域
    }
  • 词法环境(Lexical Environment)

    • 存储 let/const 声明

    • 采用声明式环境记录(Declarative Environment Record)

    • 具有层级结构,每个代码块创建新环境

    yaml 复制代码
    // 伪代码结构
    lexEnv = {
      b: <uninitialized>,
      outer: globalEnv,
      // 块级环境
      blockEnv: {
        d: <uninitialized>,
        outer: lexEnv
      }
    }

关键差异对比:

特性 变量环境(var) 词法环境(let/const)
初始化时机 执行上下文创建时设为 undefined 进入作用域时保持未初始化
内存分配 编译阶段分配固定空间 动态创建层级环境
块级作用域支持 无(渗透到函数作用域) 通过嵌套环境实现
删除属性 可删除(window 对象) 不可删除
暂存死区(TDZ) 不存在 声明前访问会抛出引用错误

经典案例解析:

ini 复制代码
console.log(a); // undefined
var a = 10;

console.log(b); //报错
let b = 20;

2. 作用域链的形成

javascript 复制代码
function outer() {
  const a = 10;
  function inner() {
    console.log(a); // 通过 outer 引用链查找
  }
  return inner;
}

在这个例子中,当我们调用 outer() 函数并获取返回的 inner 函数时,即使在 outer 函数执行完毕后,inner 函数仍然能访问变量 a。这是因为当创建 inner 函数时,JavaScript引擎不仅创建了函数对象本身,还创建了一个闭包(closure),该闭包包含了对 outer 函数的执行上下文的引用。

具体来说,当 inner 函数被调用时,JavaScript引擎首先会在 inner 函数的作用域内查找变量 a。如果找不到,则会沿着作用域链向上查找,直到找到或者到达全局作用域为止。在这种情况下,由于 a 不是在 inner 函数内部定义的,所以JavaScript引擎会通过 inner 函数的闭包找到 outer 函数的作用域,并最终在那里找到变量 a

三、调用栈的运行机制

让我们通过一个具体的例子来展示调用栈和执行上下文是如何工作的:

ini 复制代码
javascript

function first() {
  const a = 'First';
  second();
  console.log(a);
}

function second() {
  const b = 'Second';
  third();
  console.log(b);
}

function third() {
  const c = 'Third';
  console.log(c);
}

first();

调用栈的变化流程如下:

  1. 初始状态:[全局执行上下文]

  2. 调用 first() 后:[全局执行上下文, first()执行上下文]

    • 创建 a = 'First'
    • 调用 second()
  3. 调用 second() 后:[全局执行上下文, first()执行上下文, second()执行上下文]

    • 创建 b = 'Second'
    • 调用 third()
  4. 调用 third() 后:[全局执行上下文, first()执行上下文, second()执行上下文, third()执行上下文]

    • 创建 c = 'Third'
    • 打印 c 输出 'Third'
  5. third() 执行完毕并从栈中弹出:[全局执行上下文, first()执行上下文, second()执行上下文]

  6. second() 中打印 b 输出 'Second'

  7. second() 执行完毕并从栈中弹出:[全局执行上下文, first()执行上下文]

  8. first() 中打印 a 输出 'First'

  9. first() 执行完毕并从栈中弹出:[全局执行上下文]

输出结果依次为:

sql 复制代码
Third
Second
First

在这个过程中,我们可以看到:

  • 每个函数调用都会创建一个新的执行上下文,并将其压入调用栈。
  • 函数执行完毕后,对应的执行上下文会从调用栈中弹出。
  • 通过作用域链,函数可以访问其定义时所在的作用域中的变量,即使是在不同的执行上下文中调用该函数。

调用栈和执行上下文是JavaScript引擎管理代码执行的核心机制。调用栈负责跟踪函数调用的顺序,而执行上下文则提供了每个函数调用所需的环境信息。两者紧密合作,确保了JavaScript代码能够按照预期的方式执行,特别是在处理作用域、闭包和异步编程等方面时。理解这两者的工作原理对于编写高效、可靠的JavaScript代码至关重要。

最后,感谢大家的阅读,希望这篇文章有益于你🚀

相关推荐
GalenWu1 小时前
对象转换为 JSON 字符串(或反向解析)
前端·javascript·微信小程序·json
zwjapple2 小时前
“ES7+ React/Redux/React-Native snippets“常用快捷前缀
javascript·react native·react.js
数据潜水员2 小时前
插槽、生命周期
前端·javascript·vue.js
优雅永不过时·2 小时前
实现一个漂亮的Three.js 扫光地面 圆形贴图扫光
前端·javascript·智慧城市·three.js·贴图·shader
春天姐姐4 小时前
vue知识点总结 依赖注入 动态组件 异步加载
前端·javascript·vue.js
Pop–5 小时前
Vue3 el-tree:全选时只返回父节点,半选只返回勾选中的节点(省-市区-县-镇-乡-村-街道)
开发语言·javascript·vue.js
滿5 小时前
Vue3 + Element Plus 动态表单实现
javascript·vue.js·elementui
阿金要当大魔王~~5 小时前
面试问题(连载。。。。)
前端·javascript·vue.js
yuanyxh6 小时前
commonmark.js 源码阅读(一) - Block Parser
开发语言·前端·javascript
进取星辰6 小时前
22、城堡防御工事——React 19 错误边界与监控
开发语言·前端·javascript