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 创建新的执行上下文,并压入栈中。
相关推荐
程序员Sunday1 小时前
ES Module 和 CommonJS 有什么区别?循环依赖时会发生什么?
前端·javascript·面试
溯原的技术笔记2 小时前
JavaScript 闭包
面试
无敌贵点大王2 小时前
嵌入式软件面试经(自用)
c语言·stm32·单片机·学习·mongodb·面试·职场和发展
救救孩子把2 小时前
AI面试八股文-07_6-向量数据库索引详解
数据库·人工智能·面试·向量数据库·hnsw
程序 代码狂人2 小时前
数据开发面试【问答完整版|带深挖追问 + 扩展回答】
面试·职场和发展
江华森3 小时前
Web漏洞与中间件框架漏洞实战详解
面试
美味蛋炒饭.6 小时前
java八股面试(3)
java·开发语言·面试
考研保研资料分享6 小时前
2022食品跨申农业工程保研:中农、海大与华工考核复盘
面试
怕浪猫6 小时前
腾讯又交作业了,开源版 WorkBuddy 太夯了!这回是只粉色小章鱼Octop!
javascript·面试·agent
鸥梨菌Honevid6 小时前
UVM 面试高频:uvm_test_top 与 uvm_config_db 路径机制全解析
数据库·面试·职场和发展