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 创建新的执行上下文,并压入栈中。
相关推荐
求梦8201 小时前
前端八股文【CSS核心面试题库】
前端·css·面试
NAGNIP8 小时前
万字长文!回归模型最全讲解!
算法·面试
qq_318121598 小时前
互联网大厂Java面试故事:从Spring Boot到微服务架构的技术挑战与解答
java·spring boot·redis·spring cloud·微服务·面试·内容社区
且去填词10 小时前
Go 语言的“反叛”——为什么少即是多?
开发语言·后端·面试·go
青莲84312 小时前
RecyclerView 完全指南
android·前端·面试
青莲84312 小时前
Android WebView 混合开发完整指南
android·前端·面试
37手游后端团队16 小时前
gorm回读机制溯源
后端·面试·github
C雨后彩虹16 小时前
竖直四子棋
java·数据结构·算法·华为·面试
CC码码17 小时前
不修改DOM的高亮黑科技,你可能还不知道
前端·javascript·面试
indexsunny18 小时前
互联网大厂Java面试实战:微服务、Spring Boot与Kafka在电商场景中的应用
java·spring boot·微服务·面试·kafka·电商