------小Dora 的 JavaScript 修炼日记 · Day 2
"每当你以为 JS 是线性的,它就给你来一套栈帧组合拳。"
------一位被执行上下文打懵的新手程序员
昨天我们和 var / let / const
三兄弟斗了个昏天暗地,今天,新的地下迷宫浮出水面。它关乎:你写的代码到底是怎么一步步运行的?谁能访问谁?变量从哪儿冒出来?为什么闭包能穿越时间的缝隙记住变量?
本期关键词:
词法环境
、作用域链
、执行上下文
、变量查找机制
、闭包
、执行栈
🔍 一、先理清词法环境、作用域链与执行上下文的关系
很多人问:
"到底先有作用域链,还是先有词法环境?执行上下文又是什么时候出现?它们之间的关系怎么理解?"
答案是:
- 词法环境(Lexical Environment)是构成作用域的最小单元,像个房间,存放着变量和函数声明这些抽屉;
- 作用域链(Scope Chain)是多个词法环境按照定义时的父子关系串成的链条,类似房间间的走廊,决定了变量查找路径;
- 执行上下文(Execution Context)是代码运行时的"环境容器",内部包含当前词法环境和作用域链,还管理 this 绑定和变量提升。
它们的生成顺序和关系是:
- 代码"定义时",词法环境和作用域链就已经静态确定,反映代码的结构;
- 代码"执行时",引擎创建执行上下文,把当前词法环境和作用域链装进去,然后开始运行代码。
🎩 巧记:房间、走廊和钥匙------帮你牢记三者区别
- 词法环境 = "房间",抽屉里放变量;
- 作用域链 = "走廊",连通各个房间,让你能从当前房间通向父房间;
- 执行上下文 = "你带钥匙进房间开始工作的整体环境",里面包含这个房间和通往其他房间的走廊。
🔧 二、词法环境 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 上下文 | 很少用 |
执行上下文的三阶段
-
创建阶段
- 初始化词法环境和变量环境
- 确定 this 绑定
-
初始化阶段
- 函数声明提升
var
变量提升为undefined
let
和const
进入暂时性死区(TDZ)
-
执行阶段
- 按代码顺序执行,变量赋值,函数调用,闭包形成
🎩 巧记:执行上下文栈是"打怪升级排队"
- 执行上下文栈就像排队打怪升级,先打全局大BOSS,然后依次打小怪。
- 口诀:"先入栈,后出栈,执行有序排排坐。"
- 类比:函数调用就像排队买奶茶,先排的人先买完离开,后排的人继续等。
🎩 巧记:TDZ(暂时性死区)是"哑巴区"
let
和const
声明的变量,在声明之前是哑巴区,问它它不说话,直接抛错误。- 口诀:"哑巴区别乱问,先开口才有声。"
- 类比:进入一个没开灯的房间,什么也看不见,只有开灯后才能找到东西。
🧬 五、词法环境 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();
✅ 输出:undefined
。var 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% 的前端候选人。