作用域链与执行上下文的地下迷宫

------小Dora 的 JavaScript 修炼日记 · Day 2

"每当你以为 JS 是线性的,它就给你来一套栈帧组合拳。"

------一位被执行上下文打懵的新手程序员

昨天我们和 var / let / const 三兄弟斗了个昏天暗地,今天,新的地下迷宫浮出水面。它关乎:你写的代码到底是怎么一步步运行的?谁能访问谁?变量从哪儿冒出来?为什么闭包能穿越时间的缝隙记住变量?

本期关键词:词法环境作用域链执行上下文变量查找机制闭包执行栈


🔍 一、先理清词法环境、作用域链与执行上下文的关系

很多人问:

"到底先有作用域链,还是先有词法环境?执行上下文又是什么时候出现?它们之间的关系怎么理解?"

答案是:

  • 词法环境(Lexical Environment)是构成作用域的最小单元,像个房间,存放着变量和函数声明这些抽屉;
  • 作用域链(Scope Chain)是多个词法环境按照定义时的父子关系串成的链条,类似房间间的走廊,决定了变量查找路径;
  • 执行上下文(Execution Context)是代码运行时的"环境容器",内部包含当前词法环境和作用域链,还管理 this 绑定和变量提升。

它们的生成顺序和关系是:

  1. 代码"定义时",词法环境和作用域链就已经静态确定,反映代码的结构;
  2. 代码"执行时",引擎创建执行上下文,把当前词法环境和作用域链装进去,然后开始运行代码。

🎩 巧记:房间、走廊和钥匙------帮你牢记三者区别

  • 词法环境 = "房间",抽屉里放变量;
  • 作用域链 = "走廊",连通各个房间,让你能从当前房间通向父房间;
  • 执行上下文 = "你带钥匙进房间开始工作的整体环境",里面包含这个房间和通往其他房间的走廊。

🔧 二、词法环境 Lexical Environment:作用域的最小单元

词法环境是存储变量和函数声明的"容器",它是一个结构体,由**环境记录器(Environment Record)**和对外部词法环境的引用组成。

词法环境内部:

  • 环境记录器:存放当前上下文的变量、函数声明等;
  • 外部引用(outer) :指向父词法环境,形成链式结构。

示例:

ini 复制代码
js
复制编辑
let x = 1;
function outer() {
  let y = 2;
  function inner() {
    let z = 3;
    console.log(x, y, z);
  }
  inner();
}
outer();

这里,inner 内部有自己的词法环境,且通过外部引用依次连接到 outer 的词法环境,再到全局词法环境。

🎩 巧记:词法环境像"房间里的抽屉"

  • 词法环境就像一个房间,房间里有抽屉(变量),外面还有父房间,抽屉的钥匙链决定了你能开哪个抽屉(作用域链)。
  • 口诀:"抽屉有钥匙,才能拿到变量。"
  • 类比:每个函数是一个房间,想拿东西只能从自己房间或父房间拿。

🧱 三、作用域链 Scope Chain:变量查找的路径

作用域链是多个词法环境按照定义时的父子关系串成的链条,是变量查找时的路径。

从当前词法环境开始,逐级向外层词法环境查找变量,直到找到或到达全局环境。

示例:

ini 复制代码
let snack = "泡面";
function dorm() {
  let drink = "可乐";
  function desk() {
    console.log(snack);
  }
  desk();
}
dorm();

查找路径:

scss 复制代码
desk() → dorm() → Global

🚨 注意:作用域链是函数定义时确定的,和函数调用时的位置无关。

🎩 巧记:作用域链就是"洋葱皮剥层法"

  • 作用域链就像一层层洋葱皮,你找变量时,要一层层剥开,从当前层往外剥,直到找到变量或者剥完没找到报错。
  • 口诀:"洋葱剥一剥,变量才现身。"
  • 类比:就像你找钥匙,先找衣兜,再找包,最后问问家人(全局对象)。

🎯 四、执行上下文 Execution Context:代码运行的"环境容器"

执行上下文是代码执行时的环境容器,包含词法环境、作用域链、this绑定、变量提升等运行时信息。

上下文类型

类型 举例
全局上下文 最初的环境
函数上下文 每次函数调用
eval 上下文 很少用

执行上下文的三阶段

  1. 创建阶段

    • 初始化词法环境和变量环境
    • 确定 this 绑定
  2. 初始化阶段

    • 函数声明提升
    • var 变量提升为 undefined
    • letconst 进入暂时性死区(TDZ)
  3. 执行阶段

    • 按代码顺序执行,变量赋值,函数调用,闭包形成

🎩 巧记:执行上下文栈是"打怪升级排队"

  • 执行上下文栈就像排队打怪升级,先打全局大BOSS,然后依次打小怪。
  • 口诀:"先入栈,后出栈,执行有序排排坐。"
  • 类比:函数调用就像排队买奶茶,先排的人先买完离开,后排的人继续等。

🎩 巧记:TDZ(暂时性死区)是"哑巴区"

  • letconst 声明的变量,在声明之前是哑巴区,问它它不说话,直接抛错误。
  • 口诀:"哑巴区别乱问,先开口才有声。"
  • 类比:进入一个没开灯的房间,什么也看不见,只有开灯后才能找到东西。

🧬 五、词法环境 vs 变量环境(面试常考)

对比点 词法环境(LE) 变量环境(VE)
包含内容 标识符映射、块作用域(let/const) 函数声明、var
作用范围 词法级别(定义位置) 函数级别(声明和赋值)
TDZ 支持 ✅ 存在暂时性死区 ❌ 提升并初始化为 undefined

📝 注意:从 ES6 开始,词法环境包含了变量环境,两个常常一起工作。


📚 六、执行上下文栈(Call Stack)进阶

JS 引擎通过维护一个执行上下文栈追踪代码执行路径。

scss 复制代码
function f1() {
  f2();
}
function f2() {
  console.log("Hello");
}
f1();

🎞️ 栈变化过程:

markdown 复制代码
1. GlobalContext 入栈
2. f1Context 入栈
3. f2Context 入栈
4. f2 执行完毕 → 出栈
5. f1 执行完毕 → 出栈
6. 程序结束 → Global 出栈
css 复制代码
[顶部]           
┌───────────────┐
│ f2 执行上下文 │
├───────────────┤
│ f1 执行上下文 │
├───────────────┤
│ GlobalContext │
└───────────────┘
[底部]

⚔️ 七、作用域链 ≠ 执行上下文栈(常见误区)

特性 作用域链 执行上下文栈
构建时间 定义时 执行时
结构类型 单链表(LexicalEnvironment.outer) 栈(LIFO)
作用 查找变量 管理执行流程
scss 复制代码
function outer() {
  let a = 1;
  return function inner() {
    console.log(a);
  }
}
const fn = outer();
fn();

💡 闭包是作用域链的体现,而不是执行上下文栈的副产品。


🧪 八、自测题 & 面试题精选(进阶)

✅ 快问快答

1. JS 中作用域链的建立发生在哪一阶段?

  • A. 函数被调用时
  • B. 函数被定义时
  • C. 执行上下文创建时
  • D. 变量初始化时

✅ 答案:B

2. 执行上下文中 this 绑定在哪一阶段决定?

  • A. 代码解析前
  • B. 创建阶段
  • C. 初始化阶段
  • D. 执行阶段

✅ 答案:B


🧠 简答题

Q:为什么闭包可以访问函数外的变量?它依赖的是执行上下文吗?

✅ 答案:闭包访问外部变量,依赖于函数定义时建立的词法环境链(作用域链),而不是运行时的上下文栈。执行上下文栈只在运行时决定函数的调用顺序。

Q:以下代码输出什么?为什么?

ini 复制代码
var a = 1;
function foo() {
  console.log(a);
  var a = 2;
}
foo();

✅ 输出:undefinedvar a 在函数内被提升为 undefined,屏蔽了外层全局的 a


🧠 高频大厂面试题(含解析)

题 1:下面代码输出什么?为什么?

js 复制代码
var a = 10;
function foo() {
  console.log(a);
  var a = 20;
}
foo();

✅ 答案:输出 undefined

解析foo 内部的 var a 变量提升,且未赋值时为 undefined,屏蔽了全局 a


题 2:函数 bar 能访问变量 x 吗?为什么?

js 复制代码
function foo() {
  let x = 1;
  function bar() {
    console.log(x);
  }
  return bar;
}
const fn = foo();
fn();

✅ 答案:可以,输出 1

解析bar 定义时捕获了 foo 的词法环境形成闭包,能访问 x


题 3:解释闭包变量的内存释放机制

js 复制代码
function counter() {
  let count = 0;
  return function () {
    return ++count;
  };
}
const c = counter();
c(); c(); c();

✅ 解析:闭包持续引用 count,导致变量不会被垃圾回收,直到闭包本身不再被引用。


📋 大厂题自查 Checklist(加强版)

  • 我能用图表示出作用域链的结构?
  • 我能讲清上下文栈和作用域链的区别?
  • 我能准确描述执行上下文三阶段?
  • 我知道闭包变量从哪里"保存"下来?
  • 我能解释函数嵌套中变量的真实查找路径?
  • 我理解 TDZ、变量提升背后的环境记录器机制?
  • 我能用手写代码模拟执行上下文入栈/出栈流程?
  • 我能写出 memoize、闭包缓存函数等实战案例?

✅ Day 2 总结打卡(进阶版)

概念 核心理解
词法环境 存放变量和函数声明的最小环境单位
作用域链 由词法环境组成的链条,决定变量查找路径
执行上下文 代码执行时的环境容器,包含词法环境和作用域链
执行上下文栈 管理代码执行流程的栈结构(函数调用入栈、完成出栈)
闭包 依赖词法环境链延续的变量访问能力

📌 如果你能掌握这些概念,且能用图形手绘出词法环境链和执行上下文栈,那么你已经超过了 70% 的前端候选人。

相关推荐
晓131339 分钟前
JavaScript加强篇——第七章 浏览器对象与存储要点
开发语言·javascript·ecmascript
Hockor1 小时前
用 Kimi K2 写前端是一种什么体验?还支持 Claude Code 接入?
前端
杨进军1 小时前
React 实现 useMemo
前端·react.js·前端框架
海底火旺1 小时前
浏览器渲染全过程解析
前端·javascript·浏览器
_一条咸鱼_1 小时前
Android Runtime直接内存管理原理深度剖析(73)
android·面试·android jetpack
你听得到111 小时前
揭秘Flutter图片编辑器核心技术:从状态驱动架构到高保真图像处理
android·前端·flutter
驴肉板烧凤梨牛肉堡1 小时前
浏览器是否支持webp图像的判断
前端
Xi-Xu1 小时前
隆重介绍 Xget for Chrome:您的终极下载加速器
前端·网络·chrome·经验分享·github
摆烂为不摆烂1 小时前
😁深入JS(九): 简单了解Fetch使用
前端
杨进军1 小时前
React 实现多个节点 diff
前端·react.js·前端框架