你不知道的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代码至关重要。

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

相关推荐
C_心欲无痕10 小时前
vue3 - defineExpose暴露给父组件属性和方法
前端·javascript·vue.js·vue3
贺今宵10 小时前
安装better-sqlite3报错electron-vite
javascript·sql·sqlite·sqlite3
2501_9444460011 小时前
Flutter&OpenHarmony文件夹管理功能实现
android·javascript·flutter
颜酱13 小时前
滑动窗口详解:原理+分类+场景+模板+例题(视频贼清晰)
javascript
橙某人14 小时前
LogicFlow 交互新体验:让锚点"活"起来,鼠标跟随动效实战!🧲
前端·javascript·vue.js
程序猿的程14 小时前
Stock写给前端的股票行情 SDK: stock-sdk,终于不用再求后端帮忙了
前端·javascript·node.js
用户新14 小时前
V8引擎 精品漫游指南 -解析篇 语法解析 AST 作用域 闭包 字节码 优化 一文通关
前端·javascript
社恐的下水道蟑螂15 小时前
深入理解 React 中的 Props:组件通信的桥梁
前端·javascript·react.js
凌览15 小时前
2025年,我和AI合伙开发了四款小工具
前端·javascript·后端
A242073493015 小时前
深入浅出JS事件:从基础原理到实战进阶全解析
开发语言·前端·javascript