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

------小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% 的前端候选人。

相关推荐
豐儀麟阁贵6 分钟前
8.5在方法中抛出异常
java·开发语言·前端·算法
程序员念姐9 分钟前
软件测试系统流程和常见面试题
测试工具·面试
zengyuhan50336 分钟前
Windows BLE 开发指南(Rust windows-rs)
前端·rust
Bro_cat36 分钟前
Java基础
java·开发语言·面试
醉方休39 分钟前
Webpack loader 的执行机制
前端·webpack·rust
前端老宋Running1 小时前
一次从“卡顿地狱”到“丝般顺滑”的 React 搜索优化实战
前端·react.js·掘金日报
隔壁的大叔1 小时前
如何自己构建一个Markdown增量渲染器
前端·javascript
用户4445543654261 小时前
Android的自定义View
前端
WILLF1 小时前
HTML iframe 标签
前端·javascript
枫,为落叶1 小时前
Axios使用教程(一)
前端