—— 小Dora 的 JavaScript 修炼日记 · Day 1
“如果你还不明白变量的本质,那你从来不知道 JavaScript 到底在搞啥鬼”
—— 来自一位被 TDZ 一切撤回的前端工程师
今天是小Dora修炼 JavaScript 的第一天,我们不开头讲 DOM,不搞计时器,不谈原型链,单刀直入讲清:
JavaScript 变量究竟是怎么在 V8 引擎中被创建、存储与访问的?
是时候揭开那一层编译器对我们刻意隐藏的底层真相了。
🧠 一、变量三兄弟的行为背后:来自 V8 的内存安排
🥷 1. var:作用域松散的“古早党”
当然可以,下面是一个更加严谨、符合 V8 实际内存模型的改写版本,同时保持技术趣味性与表达清晰性:
console.log(name); // undefined
var name = "小Dora";
你以为这是语法糖?不,V8 引擎早在你执行前的编译阶段就已经偷偷给你干了几件事:
- 构建了当前执行上下文,包括Variable Environment(变量环境)
- 在栈内存中为
name分配了一个变量槽 - 将变量
name初始化为undefined
此时的内存结构长这样:
[Stack - 执行上下文变量环境]
name = undefined
到了执行阶段,当解释器执行到 var name = "小Dora" 时:
- 字符串
"小Dora"被存储到 堆内存(Heap) name变量在栈中的槽位更新为:指向堆内存的地址引用
执行后的内存结构如下:
[Stack - 执行上下文变量环境]
name --> 0x001
[Heap]
0x001: "小Dora"
所以变量提升其实是指“声明被提前,赋值没动”。也就是说:你人在,但灵魂还没上线。
👨💼 2. let:带着“访问许可证”的正规军
console.log(age); // ❌ ReferenceError
let age = 18;
你一访问 age,V8 立马给你爆头——为啥这么严格?
let在创建阶段进入了所谓的 TDZ(暂时性死区)- 在 LexicalEnvironment 中,age =
<uninitialized> - 只有执行过初始化之后,才算“可被访问”
[Stack]
LexicalEnv = {
age: <uninitialized>
}
访问它?就是访问一个“不存在的合法引用”,当然 ReferenceError。
🛡️ 3. const:一旦绑定,绝不松口
const goal = "高级前端";
goal = "产品经理"; // ❌ TypeError
V8 在 const 初始化之后,会打上一个 只读锁(readonly),再也不能改。
但注意⚠️:
const config = { debug: true };
config.debug = false; // ✅ 这是允许的
因为 const 锁住的是栈上的指针地址,不是堆中对象的内容。
[Stack]
config --> 0x003
[Heap]
0x003 = { debug: true }
换人不行,换衣服可以。
🧬 二、执行上下文与作用域链:变量“在哪里”访问?
每次 JS 执行时,都会生成一个 执行上下文(EC) ,每个上下文会包含三个核心部分:
- VariableEnvironment(VE) :处理
var - LexicalEnvironment(LE) :处理
let/const,形成作用域链 - ThisBinding:绑定当前执行上下文中的
this
示例:
function foo() {
var x = 10;
let y = 20;
}
编译阶段:
ExecutionContext (EC_foo): {
VE: { x: undefined },
LE: { y: <uninitialized> },
this: undefined
}
var 和 let/const 分别存储在不同“柜子”里:一个老旧但包容(VE),一个严格到近乎傲慢(LE)。
📦 三、栈与堆:变量在内存中的真实归宿
| 数据类型 | 存储位置 | 示例 |
|---|---|---|
| 基本类型 | 栈(stack) | let a = 1 |
| 引用类型 | 栈存引用 → 堆 | let obj = { name: '小Dora' } |
| 函数对象 | 堆 | function foo() {} |
在 V8 中,基本类型直接放在栈上,引用类型放堆里,栈中只保留指针。
举个例子:
let a = 1;
let obj = { name: "Dora" };
内存结构如下:
[Stack]
a = 1
obj --> 0x100
[Heap]
0x100 = { name: "Dora" }
🌀 四、生命周期三段论:变量不是瞬间就有
| 阶段 | var 的行为 | let/const 的行为 |
|---|---|---|
| 创建阶段 | 注册变量名 | 注册变量名 + 设为 uninitialized |
| 初始化阶段 | 设为 undefined | 无任何默认值(此时 TDZ) |
| 执行阶段 | 正常赋值/使用 | 初始化 + 赋值/使用 |
⚠️ 访问 TDZ 中的变量,本质上是访问一个还未分配内存值的“挂名标识”,V8 会直接拒绝。
🛠️ 五、实战演练:你以为你懂,其实只是表象
示例 1:var 穿越块作用域
{
var foo = "JS Ninja";
}
console.log(foo); // ✅ "JS Ninja"
V8:这不是块作用域,这是空气。
示例 2:let/const 的范围防御
{
let bar = "闭环之力";
}
console.log(bar); // ❌ ReferenceError
V8:你出了花括号,就别想碰它。
示例 3:变量提升细节复盘
function test() {
console.log(a); // undefined
console.log(b); // ReferenceError
var a = 1;
let b = 2;
}
test();
V8 在创建阶段已经:
VE = { a: undefined }
LE = { b: <uninitialized> }
所以:a 可以访问但没值;b 直接报错。
🧪 六、大厂面试真题你扛得住?
💥 Q1:
{
var a = 1;
let b = 2;
}
console.log(a); // ?
console.log(b); // ?
✅ 输出:
1
ReferenceError
💥 Q2:
const person;
person = { name: "Dora" };
✅ 输出:SyntaxError: Missing initializer in const declaration
const 必须“出生即初始化”,哪怕你说“我迟点起名字”,也不行。
🧾 七、一图回顾:V8 内部变量管理模型
[栈 Stack]
fooEC = {
VE: { varA: undefined },
LE: { letB: <uninitialized> },
this: undefined
}
[堆 Heap]
0x101 = { name: "小Dora" }
0x201 = () => {} // 带 [[Environment]] 指针
变量存储结构清晰明了:栈快速回收,堆留长久数据,函数闭包保环境。
🧘 八、Day 1 修炼总结
| 特性对比 | var | let | const |
|---|---|---|---|
| 提升行为 | ✅ 声明提升 | ❌(TDZ) | ❌(TDZ) |
| 存储位置 | 栈指针 → 堆 | 栈指针 → 堆 | 栈指针 → 堆 |
| 可否重复声明 | ✅ 可以 | ❌ 报错 | ❌ 报错 |
| 可否重新赋值 | ✅ 可以 | ✅ 可以 | ❌ 不可 |
📌 变量声明,不只是写 let、var、const,它是整个 V8 引擎执行流程的入口级结构设计。
🔚 尾声:走出变量迷宫,踏入执行上下文
第一天结束了,小Dora已经理解:
- 变量三兄弟不仅语法不同,运行机制也不同
- TDZ、作用域和变量提升,是理解 JS 的起点
- 每一个变量都有生命周期,控制它就是掌控 JS
📌 下一站:词法作用域、执行上下文与闭包三连击 —— 这是所有中高级面试绕不过去的必杀技。
如果你也在学习 JavaScript,欢迎继续追更 《小Dora 的 JS 修炼日记》 。
我们不只写代码,我们要理解代码“为什么这么写”。