JavaScript 执行栈和执行上下文详解

你是否曾经好奇过,JavaScript 代码是如何一步步执行的?为什么有时候变量会"提升"?函数调用是如何管理的?今天我们就来深入浅出地聊聊 JavaScript 的执行机制。

什么是执行上下文?

想象一下,你要做一道菜。在开始操作之前,你需要准备什么?

  • 确认菜谱
  • 准备食材
  • 确定主厨
  • 准备厨具

执行上下文(Execution Context)就像是这个"准备工作"的过程。当 JavaScript 代码执行进入到一个环境时,引擎会先做一些准备工作,这个准备阶段就叫做"创建执行上下文"。

简单来说:执行上下文就是代码执行前的准备工作

具体做了什么我们后面再说,先来看下 JavaScript 执行环境有哪些?

JavaScript 中的执行环境

JavaScript 中有三种执行环境:

  1. 全局环境
  2. 函数环境
  3. eval 环境(不推荐使用)

对应的,就有三种执行上下文:

  1. 全局执行上下文 - 程序启动时创建,全局唯一
  2. 函数执行上下文 - 每次函数调用时创建
  3. eval 执行上下文 - eval 函数执行时创建

执行流程

当 JavaScript 程序运行时:

  1. 首先进入全局环境,创建全局执行上下文
  2. 遇到函数调用时,进入函数环境,创建函数执行上下文
  3. 多个函数调用会产生多个执行上下文
  4. 这些上下文通过的方式进行管理

理解栈数据结构

在深入执行栈之前,我们先来理解一下"栈"这个概念。

栈的特性

想象一下叠盘子的场景:

  • 你只能从最上面放盘子
  • 你也只能从最上面拿盘子
  • 最后放上去的盘子,会最先被拿走
  • 最先放的盘子,会最后被拿走

这就是栈的核心特性:后进先出(LIFO - Last In First Out)

特点 说明
后进先出 最后进入的元素最先出来
单一出入口 只能从栈顶操作
入栈/出栈 放入叫入栈,取出叫出栈

JavaScript 执行栈(调用栈)

理解了栈的概念,我们来看看 JavaScript 是如何用栈来管理执行上下文的。

执行栈的工作原理

执行栈就像是一个"任务管理器":

  • 栈底:永远是全局执行上下文
  • 栈顶:当前正在执行的函数上下文
  • 入栈:函数被调用时,创建新的执行上下文并推入栈顶
  • 出栈:函数执行完毕,从栈顶移除其执行上下文

JavaScript 在执行代码时最先进入全局环境,而全局环境只有一个,对应的全局执行上下文也只有一个,只有当页面被关闭之后它才会从执行栈中被推出,否则一直存在于栈底。当遇到函数调用时就会进入函数执行环境,并创建函数执行上下文并将其推入栈顶,当函数调用完成后,它就会从栈顶被推出,理想的情况下,闭包会阻止该操作。

让我们通过一个具体例子来看看执行栈是如何工作的:

javascript 复制代码
function foo() { 
    function bar() {        
        return 'I am bar';
    }
    return bar();
}
foo();

执行过程可视化:

步骤解析:

  1. 初始状态:只有全局执行上下文在栈中
  2. 调用 foo():foo 的执行上下文入栈
  3. 调用 bar():bar 的执行上下文入栈
  4. bar() 执行完:bar 的执行上下文出栈
  5. foo() 执行完:foo 的执行上下文出栈
  6. 回到全局:只剩全局执行上下文

栈溢出问题

执行栈的空间是有限的!如果函数调用层级太深,会发生栈溢出

javascript 复制代码
function foo() {
    foo();
}
foo();
// 报错:Uncaught RangeError: Maximum call stack size exceeded

常见栈溢出场景:

  • 无限递归
  • 递归层级过深
  • 忘记设置递归终止条件

执行上下文的生命周期

前面我们有说到,运行 JavaScript 代码时,当代码执行进入一个环境时,就会为该环境创建一个执行上下文,它会在你运行代码前做一些准备工作,接下来就我们就来看一下具体会做什么。

具体做的事就和执行上下文的生命周期有关,每个执行上下文都有自己的生命周期,分为两个主要阶段:

创建阶段

函数被调用时,会进入函数环境,为其创建一个执行上下文,此时进入创建阶段:

1. 创建变量对象(VO - Variable Object)

  1. 创建 Arguments 对象(并赋值)
  2. 确定函数参数(并赋值)
  3. 处理函数声明(并赋值)
  4. 处理变量声明(未赋值,初始为 undefined)

2. 确定 this 指向

  • this 的值由调用方式决定
  • 在创建阶段就已经确定

3. 确定作用域链

  • 作用域由函数定义位置决定(词法作用域)
  • 决定了变量的查找规则

当处于执行上下文的建立阶段时,我们可以将整个上下文环境看作是一个对象。该对象拥有 3 个属性,如下:

js 复制代码
executionContextObj = {
    variableObject : {}, // 变量对象,里面包含 Arguments 对象,形式参数,函数和局部变量
    scopeChain : {},// 作用域链,包含内部上下文所有变量对象的列表
    this : {}// 上下文中 this 的指向对象
}

这里我们重点来看一下变量对象里面所拥有的东西,在函数的建立阶段,首先会建立 Arguments 对象。然后确定形式参数,检查当前上下文中的函数声明,每找到一个函数声明,就在 variableObject 下面用函数名建立一个属性,属性值就指向该函数在内存中的地址的一个引用。最后,是确定当前上下文中的局部变量。

执行阶段

创建阶段完成后,开始执行代码:

  1. 变量赋值:给之前声明的变量赋予实际值
  2. 函数表达式赋值:处理函数表达式
  3. 执行代码:按顺序执行函数体中的代码

案例:

让我们通过一个例子来理解这两个阶段:

javascript 复制代码
const foo = function(i) {
    var a = "Hello";
    var b = function privateB() {};
    function c() {}
}
foo(10);

创建阶段的变量对象:

javascript 复制代码
fooExecutionContext = {
    variableObject: {
        arguments: {0: 10, length: 1}, // Arguments 对象
        i: 10,                         // 形参赋值
        c: pointer to function c(),    // 函数声明提升
        a: undefined,                  // 变量声明,未赋值
        b: undefined                   // 变量声明,未赋值
    },
    scopeChain: {},
    this: {}
}

执行阶段的变量对象:

javascript 复制代码
fooExecutionContext = {
    variableObject: {
        arguments: {0: 10, length: 1},
        i: 10,
        c: pointer to function c(),
        a: "Hello",                    // 变量被赋值
        b: pointer to function privateB() // 函数表达式被赋值
    },
    scopeChain: {},
    this: {}
}

我们看到,只有在代码执行阶段,局部变量才会被赋予具体的值。在建立阶段局部变量的值都是 undefined。这其实也就解释了变量提升的原理。

现在我们再通过一个例子来加深对函数这两个阶段的过程的理解

javascript 复制代码
(function () {
    console.log(typeof foo); // ?
    console.log(typeof bar); // ?
    
    var foo = "Hello";
    var bar = function () {
        return "World";
    }

    function foo() {
        return "good";
    }
    
    console.log(foo, typeof foo); // ?
})()

创建阶段分析:

javascript 复制代码
IIFEExecutionContext = {
    variableObject: {
        arguments: {length: 0},
        foo: pointer to function foo(), // 函数声明优先
        bar: undefined                  // 变量声明
        // 注意:同名的 var foo 被忽略了!
    }
}

执行阶段结果:

javascript 复制代码
(function () {
    console.log(typeof foo); // "function" - 函数声明提升
    console.log(typeof bar); // "undefined" - 变量提升但未赋值
    
    var foo = "Hello";       // foo 被重新赋值为字符串
    var bar = function () {
        return "World";
    }

    function foo() {
        return "good";
    }
    
    console.log(foo, typeof foo); // "Hello" "string"
})()

关键要点总结

阶段 主要工作 变量状态 函数状态
创建阶段 准备工作 声明但未赋值(undefined) 完全可用
执行阶段 执行代码 获得实际值 函数表达式被赋值

面试真题解析

Q: 谈谈你对 JavaScript 执行上下文栈的理解

A: 完整回答思路

什么是执行上下文?

执行上下文是 JavaScript 代码执行时的环境抽象概念。每当代码运行时,都是在特定的执行上下文中运行的。

执行上下文的类型

JavaScript 中有三种执行上下文:

  1. **全局执行上下文:**这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事,创建一个全局的 window 对象(浏览器的情况下),并且设置 this 的值等于这个全局对象。一个程序中只会有一个全局执行上下文。

  2. **函数执行上下文:**每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序(将在后文讨论)执行一系列步骤。

  3. **Eval 执行上下文:**执行在 eval 函数内部的代码也会有它属于自己的执行上下文

调用栈

调用栈是解析器(如浏览器中的的 JavaScript 解析器)的一种机制:

  • 当脚本要调用一个函数时,解析器把该函数推入到栈中并且执行这个函数。
  • 被这个函数中调用到的函数会进一步添加到调用栈中。
  • 当函数运行结束后,解释器将它从堆栈中取出,并在主代码列表中继续执行代码。
  • 如果栈占用的空间比分配给它的空间还大,那么则会导致"栈溢出"错误。

希望这篇文章能帮助你更好地理解 JavaScript 的执行机制!如果你有任何问题,欢迎在评论区讨论。

相关推荐
巴巴_羊1 小时前
React JSX语法
javascript·react.js·ecmascript
烛阴1 小时前
Python多进程开发实战:轻松突破GIL瓶颈
前端·python
爱分享的程序员1 小时前
前端面试专栏-主流框架:11. React Router路由原理与实践
前端·javascript·react.js·面试
weixin_459074351 小时前
在el-image组件的预览中添加打印功能(自定义功能)
前端·javascript·vue.js
知否技术1 小时前
2025微信小程序开发实战教程(三)
前端·微信小程序
海的诗篇_2 小时前
前端开发面试题总结-vue3框架篇(二)
前端·javascript·vue.js·面试·前端框架·vue
大熊程序猿2 小时前
quartz 表达式最近10次执行时间接口编写
java·服务器·前端
广药门徒2 小时前
ad24智能pdf输出的装配图没有四个边角那里的圆孔
前端·javascript·pdf
zhangxingchao2 小时前
Flutter与H5页面的交互
前端
粥里有勺糖2 小时前
视野修炼第124期 | 终端艺术字
前端·javascript·github