JavaScript执行上下文详解

这边文章我们来学习JavaScript中的核心概念------------执行上下文。理解执行上下文是理解JavaScript代码如何运行(底层机制)的关键,对于弄懂作用域、闭包、变量提升等现象至关重要。

什么是执行上下文?

执行上下文是 JavaScript 代码被解析和执行时所在环境的抽象概念 。每当 JavaScript 引擎执行一段可执行代码时,都会创建一个对应的执行上下文。

你可以把它想象成一个包含代码运行所需所有信息的"容器"或"快照"。

执行上下文的类型

1. 全局执行上下文

  • 默认的、最外层的上下文
  • 在浏览器环境中,它关联着window对象。
  • 一个程序中只会有一个全局执行上下文。
  • 它会做两件事:① 创建一个全局对象(浏览器中为 window);② 将 this 指这个全局对象。

2. 函数执行上下文

  • 每次调用函数时,都会为该函数创建一个新的函数执行上下文。
  • 每个函数都有自己独立的执行上下文,即使是同一个函数被多次调用。

执行栈(调用栈 Call Stack)

执行上下文是如何管理的呢?答案是通过执行栈(也称为调用栈)。

  • 执行栈是一个后进先出(LIFO)的栈结构,用于存储在代码执行期间创建的所有执行上下文。
  • 当 JavaScript 引擎开始执行脚本时,它首先会创建一个全局执行上下文并将其压入栈底。
  • 每当遇到一个函数调用,引擎就会为该函数创建一个新的函数执行上下文并将其压入栈顶。
  • 引擎会执行位于栈顶的执行上下文中的代码。
  • 当该函数执行完毕后,它的执行上下文会从栈中弹出,控制权交还给栈中的下一个(即之前的)执行上下文。

示例

javascript 复制代码
  let a = 'Hello';

  function first() {
    console.log('Inside first function');
    second();
    console.log('Again inside first function');
  }

  function second() {
    console.log('Inside second function');
  }

  first();
  console.log('Inside Global Execution Context');

执行栈的变化过程:

  1. 初始状态:栈为空。
  2. 脚本开始:创建全局执行上下文并压入栈。
  3. 调用 first():创建 first 的函数执行上下文并压入栈。
  4. 在 first() 中调用 second():创建 second 的函数执行上下文并压入栈。
  5. second() 执行完毕:second() 的函数执行上下文从栈中弹出。
  6. first() 执行完毕:first() 的函数执行上下文从栈中弹出。
  7. 脚本执行完毕:全局执行上下文从栈中弹出,栈清空。

图解: 输出结果:

text 复制代码
  Inside first function
  Inside second function
  Again inside first function
  Inside Global Execution Context

执行上下文的生命周期

1. 创建阶段
2. 执行阶段

我们重点看创建阶段 ,因为变量提升等行为就发生在这里。在创建阶段,执行上下文会关联一个非常重要的对象------变量对象(Variable Object, VO)

对于全局上下文函数上下文,变量对象的细微差别导致了不同的行为:

  • 全局上下文中的变量对象(VO):
    • 其实就是全局对象本身(浏览器中是 window)。
    • 所有在全局层面声明的变量和函数都成为全局对象(window)的属性。
  • 函数上下文中的变量对象(VO):
    • 它被称为活动对象(Activation Object, AO)。你可以认为在函数上下文中,VO 就是 AO。
    • AO 除了包含变量和函数声明,还包含了 arguments 对象。

创建阶段,引擎会扫描当前上下文中的代码,并完成以下工作:

  1. 创建变量对象(VO/AO):
    • 处理函数声明:扫描并将所有函数声明添加到 VO/AO 中,并指向函数在内存中的引用。如果函数名已存在,则覆盖之前的引用。
    • 处理变量声明:扫描并将所有通过 var 声明的变量添加到 VO/AO 中,并初始化为 undefined。如果变量名已存在,则跳过本次声明,防止覆盖函数。
    • 处理参数(仅函数上下文):处理函数的参数。对于形参,将其添加到 AO 中并赋值为实参的值;如果没有实参,则赋值为 undefined。
      这个过程就是大家常说的变量提升 的本质。函数声明会整体提升,而 var 变量只会声明提升,赋值操作留在原地。
      不了解变量提升的本质可以看作者写的另一篇文章:JavaScript预编译机制详解
  2. 创建作用域链
    • 作用域链是当前 VO/AO 和所有父级执行上下文的 VO/AO 的集合。它保证了当前执行上下文有权访问的所有变量和函数的有序访问。
    • 作用域链是在函数定义时就确定的,而不是调用时(这就是词法作用域/静态作用域)。
  3. 确定 this 的指向:
    • 在全局上下文中,this 指向全局对象(浏览器中是 window)。
    • 在函数上下文中,this 的指向取决于函数是如何被调用的。

执行阶段:

  • 创建阶段完成后,引擎开始按顺序一行一行执行代码。
  • 此时,会根据代码逻辑,对 VO/AO 中的变量进行赋值。
  • 如果执行过程中遇到新的函数调用,则重复上述过程:为该函数创建新的执行上下文,并压入执行栈。

示例:分解执行上下文的创建

javascript 复制代码
  function greet(name) {
    var welcome = 'Hello ';
    function sayHello() {
      console.log(welcome + name);
    }
    return sayHello();
  }

  var message = greet('World');

分析 greet('World') 函数执行上下文的创建阶段:

  1. 创建 AO
    • 处理参数:name: 'World'
    • 处理函数声明:sayHello: <指向 sayHello 函数>
    • 处理变量声明:welcome: undefined
      此时,greet 的 AO 大致是:
javascript 复制代码
 AO = {
    name: 'World',
    sayHello: <reference to function>,
    welcome: undefined
 }
  1. 创建作用域链:
  • greet 的作用域链是: [greet's AO] + [Global VO]
  • sayHello 函数在定义时,其内部属性 [[Scope]] 就已经保存了这条作用域链。
  1. 确定 this:
  • 在这个简单的调用中(greet('World')),this 通常指向全局对象(非严格模式)。

执行阶段:

然后引擎开始执行 greet 函数体内的代码:

  • var welcome = 'Hello '; -> 将 AO 中的 welcome 从 undefined 赋值为 'Hello '。
  • 执行 return sayHello(); -> 调用 sayHello 函数,从而为 sayHello 创建新的执行上下文,并压入栈中。
相关推荐
Lotzinfly2 小时前
8 个经过实战检验的 Promise 奇淫技巧你需要掌握😏😏😏
前端·javascript·面试
知其然亦知其所以然2 小时前
MySQL 社招必考题:如何优化查询过程中的数据访问?
后端·mysql·面试
努力的小郑2 小时前
从一次分表实践谈起:我们真的需要复杂的分布式ID吗?
分布式·后端·面试
bug_kada3 小时前
前端路由:深入理解History模式
前端·面试
bug_kada3 小时前
前端路由:Hash vs History,一篇讲明白!
前端·面试
Baihai_IDP4 小时前
AI Agents 能自己开发工具自己使用吗?一项智能体自迭代能力研究
人工智能·面试·llm
Sailing5 小时前
别再放任用户乱填 IP 了!一套前端 IP 与 CIDR 校验的高效方案
前端·javascript·面试
大模型真好玩5 小时前
大模型工程面试经典(七)—如何评估大模型微调效果?
人工智能·面试·deepseek
绝无仅有5 小时前
后端 Go 经典面试常见问题解析与总结
后端·面试·github