JS 的内存管理是自动的,但“自动”不等于“与你无关”。一旦你写出错误的引用持有(闭包 / 全局变量 / DOM 引用),问题往往不是“报错”,而是越来越卡、越来越慢、越来越难定位。
目录导航
-
- 为什么学内存:你写的每一行代码都在占内存
-
- 内存管理的生命周期:申请 → 使用 → 释放
-
- 为什么这章坚持讲
var:它是理解执行过程的“历史入口”
- 为什么这章坚持讲
-
- 执行上下文栈(调用栈):代码到底按什么顺序跑?
-
- 变量提升、
undefined与is not defined:差在哪?
- 变量提升、
-
- LHS / RHS:用“查询视角”把提升讲透
-
- 函数为什么也能“提升”:编译阶段到底做了什么
-
- AO / GO / VO / VE:术语别背,先抓住结构
-
- 作用域链:变量到底沿着什么路径被找到
-
var的一个经典坑:全局属性覆盖(浏览器 vs Node 的差异)
-
- 留几道题:用来检验你是不是真懂了
1)为什么学内存:你写的每一行代码都在占内存
在 JS 里,原始值、对象、函数……都会占用内存;只不过引擎帮你“自动管理”,所以它看起来像黑盒。黑盒的代价是:你写错了引用关系,不会立刻炸,而是悄悄把内存越撑越大,最后变成“线上性能事故”。
这也是这章的核心目标:
用“执行过程视角”把内存模型讲清楚——你至少要知道:
- 变量/函数在执行前后分别存在哪里
- 为什么某些值是
undefined,某些直接ReferenceError - 作用域链怎么找变量,闭包为什么能“把东西留住”
2)内存管理的生命周期:申请 → 使用 → 释放
不管哪门语言,内存管理逃不开三步:
- 申请内存
- 使用内存(存对象/值/函数等)
- 不再需要时释放内存
区别在于:
- C/C++ 这类是“手动管理”(
malloc/free),能力强但容易踩坑 - Java/JavaScript/Python 这类是“自动管理”,靠 GC(垃圾回收)做释放,写起来更安全,但你仍然需要理解引用关系,否则照样内存泄漏
关键点:GC 回收的是“不可达对象”,不是“你觉得不用了的对象”。你写的引用链,决定了它可不可达。
3)为什么这章坚持讲 var:它是理解执行过程的“历史入口”
很多人会说:我日常只用 let/const,学 var 干嘛?
原因很现实,也很重要:
- ES6 之前只有
var,大量存量项目仍在用,不懂你就维护不了 var的缺陷(提升、全局挂载、作用域边界模糊)恰好能逼你把“执行过程”想清楚- 真正理解
var,你才会明白let/const到底修复了什么
这也是为什么后面我会用 var 来讲:提升、执行上下文、作用域链这些“底层规则”。
4)执行上下文栈(调用栈):代码到底按什么顺序跑?
JS 引擎内部有一个结构:执行上下文栈(ECS / Call Stack) ,用来管理“当前正在执行的上下文”。
你可以把它理解成:
- 先把全局执行上下文 GEC 压栈,开始跑全局代码
- 每调用一个函数,就创建一个函数执行上下文 FEC 压栈
- 函数执行完,FEC 出栈,回到上一个上下文继续跑
这就是“为什么递归会爆栈”“为什么某些函数先打印后执行”的底层原因。
5)变量提升、undefined 与 is not defined:差在哪?
先看这个经典片段:
console.log(num1);
var num1 = 20
var num2 = 30
var result = num1 + num2
console.log(result);
为什么第一行打印 undefined,不是报错?核心原因:提升。
5.1 三种常见 undefined
- 变量声明未赋值
- 访问对象不存在的属性
- 函数没有显式 return
在上面例子里,属于第一种:声明提升了,但赋值还没发生。
5.2 undefined vs ReferenceError: xxx is not defined
undefined:有这个变量,只是当前时刻它还没有值(或就是 undefined)is not defined:连变量都不存在(没有声明/没有绑定记录),引擎找不到“引用”就直接抛错
6)LHS / RHS:用“查询视角”把提升讲透
很多人对提升的理解停留在“会被提到上面”。但真正写复杂代码时,你需要一个更稳的视角:LHS / RHS 查询。
- LHS(Left-Hand Side) :找“容器”,为了赋值
- RHS(Right-Hand Side) :找“值”,为了读取
把“打印 num1”这件事换成查询语言:
console.log(num1) 触发 RHS 查询:我要读取 num1 的值 → 引擎沿作用域查找绑定 → 找到了声明(被提升过)→ 但未赋值 → 返回 undefined。
图示(保留原图):
图4-1 赋值的过程
扩展图(保留原图):
7)函数为什么也能“提升”:编译阶段到底做了什么?
你会发现:函数在声明前也能调用(函数声明形式):
foo2()
function foo2(){
console.log('XiaoWu')
}
这背后离不开“编译阶段”。文档里用了一张图概括:语法分析 → 预编译 → 即时编译 → 优化。
(保留原图)
图4-3 预编译与即时编译
更“严谨”的说法是:规范层面不是 AO/VO 这些对象,而是 Environment Record / Lexical Environment 等结构;但为了建立直觉,用 AO/GO 讲清执行过程非常有效——后面你再替换成规范术语会更顺。
8)AO / GO / VO / VE:术语别背,先抓住结构
先记一个对你最有用的结构化结论:
- GO(Global Object) :全局对象(浏览器里常见是
window),全局变量在这里能被访问到 - AO(Activation Object) :函数执行时的“局部环境”(函数上下文里用来承载局部变量、形参等的抽象容器)
- VO(Variable Object) :更泛化的叫法,表示“变量所在环境”;全局时近似 GO,函数时近似 AO
- VE(Variable Environment) :规范/新表述方向:把 VO 更明确成“环境”概念(你可以先把它当成 VO 的升级叫法)
你真正需要掌握的是:变量在哪个环境里被创建、函数调用时会创建新的环境、环境之间如何连接(作用域链) 。
9)作用域链:变量到底沿着什么路径被找到?
作用域链本质是:**当前环境 + 父级环境 + 父级的父级……**形成的一条“向外查找链”。在任意执行上下文中,查变量会先查当前,再沿链向外。
(保留原图)
图4-8 作用域链的描述
再看这个用来“打穿直觉”的例子:
foo(123)
var m = 666
function foo(num){
console.log('1', m);
var m = 10
console.log('2', m);
var n = 20
}
它会输出:undefined 和 10。原因不是玄学,而是:函数内部 var m 先提升到当前 AO 的顶端,所以第一次打印时,当前作用域里已经“有 m 了”,只是没赋值。
(保留原图)
图4-4 全局函数执行内存图
(保留原图)
图4-6 函数内存执行过程
(保留原图)
图4-7 函数内存执行结束
你会看到一个更重要的“内存启示”:
函数执行结束,执行上下文出栈;如果没有任何引用再指向 AO(或其内部对象),它就会变成“不可达”,后续会被 GC 回收。
这句话就是你未来理解“闭包为什么会泄漏/为什么能保留数据”的关键入口。
10)var 的一个经典注意点:全局属性覆盖(浏览器 vs Node)
看这个例子(保留原文核心案例与结论):
var name = "XiaoWu"
foo(123)
function foo(num){
console.log("1", m);
var n = 10
var m = 20
function bar(){
console.log("2", name)
}
bar()
}
输出:
1 undefined
2 XiaoWu
这里注意一个很“坑”的点:如果你把全局 name 的声明注释掉,在浏览器里你可能仍然得到 XiaoWu,这常常来自:
var在全局作用域下会“挂到”全局对象上- 浏览器的
window本身就可能已有name这个属性 - 于是你以为你删了变量,实际上你只是改写了全局对象属性,产生了“黑魔法效果”
(保留原图)
图4-13 var 的全局覆盖缺陷
同时,Node 环境下全局对象不是 window,行为会不同(甚至直接报错),这也是很多同学“浏览器能跑、Node 不行”的来源之一。
11)留几道题:用来检验你是不是真懂了
把题留在这里,建议你先“脑内执行一遍”,再去跑代码对答案(非常练功)。
//面试题1
var n = 100
function foo(){
n = 200
}
foo()
console.log(n)
//面试题2
function foo(){
console.log(m)
var m = "小吴"
console.log(m);
}
var m = "coderwhy"
foo()
//面试题3
var n = 100
function foo1(){
console.log("这是foo1内部",n);
}
function foo2(){
var n = 200
console.log("这是foo2内部",n);
foo1()
}
foo2()
console.log("这是最外层",n);
//面试题4
var a = 100
function foo(){
console.log(a)
return
var a = 200
}
foo()
//面试题5
function foo(){
var a = b = 10
}
foo()
console.log(a);
console.log(b);
收个尾:这章你真正应该带走的“底层能力”
- 代码运行不是“从上到下”这么简单,它受 ECS(调用栈)+ 编译阶段信息 控制
undefined多半是“有声明但没赋值”,is not defined多半是“压根没声明/没绑定”- 作用域链是“当前环境向外找”的规则,它解释了:为何闭包能拿到外层变量,也解释了为何错误引用会导致内存无法回收
如果你愿意,我可以在你下一章(垃圾回收/闭包)里把这套“引用可达性”模型继续往下推:
- V8 分代回收(新生代/老生代)
- 标记清除/标记整理的直觉
- 真实项目里最常见的泄漏模式与排查路径(Performance/Memory 面板 + 快照对比)