「为什么我的变量总是'失踪'?」
「函数执行完后变量怎么还在内存里?」
别慌!今天用「寻宝」和「背包」的比喻,带你吃透 JS 作用域链和闭包的底层逻辑~😉
一、JS 执行的「幕后黑手」:调用栈与执行上下文
先讲个小故事:
JS 引擎就像一个勤劳的「代码管家」,每次执行函数时都会创建一个「执行上下文」小本本,记录当前函数的变量、作用域等信息。
而「调用栈」就像一堆叠起来的盘子,按顺序存放这些小本本,保证函数们按规则执行~🍽️
🤖 举个栗子:
javascript
function foo() {
var myname = "极客邦";
function bar() {
console.log(myname); // 这里会找到谁?
}
bar();
}
foo();
-
全局执行上下文先入栈,记录全局变量(如果有的话)。
-
调用
foo()
时,foo
的执行上下文入栈,创建myname
变量(var
会变量提升哦~)。 -
调用
bar()
时,bar
的执行上下文入栈,它需要找myname
,但自己没有,怎么办?
📸 看下图!执行上下文与作用域链的关系:

(图中问题:bar 函数中的 myName 会取全局还是 foo 的值?答案见下文~)
二、作用域链:变量「寻宝」的路线图
想象每个执行上下文都有一张「寻宝地图」,上面写着:「先找自己的口袋,再问爸爸,最后问爷爷(直到全局)」~🔍
这就是 作用域链 :当前作用域 → 父作用域 → ... → 全局作用域 的查找路径。
🔍 寻宝规则详解:
-
词法作用域(静态规则) :
变量的「爸爸」是谁,在代码写出来时就定了,和函数怎么调用无关!
比如:
javascriptfunction bar() { console.log(myname); // 我爸爸是全局 } function foo() { var myname = "极客邦"; bar(); // 虽然在 foo 里调用 bar,但 bar 的爸爸还是全局! } var myname = "极客时间"; foo(); // 输出:极客时间(全局变量)
(表情包:bar 摊手🤷♂️:我天生属于全局爸爸,别想骗我认 foo 当爹!)
-
变量提升与 TDZ(坑点警告) :
var
变量会「提前搬家」到作用域顶部(初始值undefined
)。let/const
有「暂时性死区(TDZ)」,声明前使用会报错!
iniconsole.log(test); // undefined(var 提升) var test = 1; console.log(test2); // ReferenceError(let 未声明前在 TDZ) let test2 = 2;
(表情包:var 悠哉躺平😴:我早就在这里了~ let 严肃脸😠:没喊你名字别乱认!)
📸 执行上下文的「父子关系」长这样:
(图中:foo 和 bar 的执行上下文都指向全局,因为它们都是在全局声明的函数)
三、闭包:函数的「神秘背包」
当函数嵌套时,内部函数会偷偷打包一份「外部作用域的变量」带走,就算外部函数执行完,这些变量也会留在内存里~🎒
这就是闭包:内部函数对外部作用域变量的引用。
🎒 背包原理演示:
javascript
function foo() {
var myName = "极客时间";
let test1 = 1;
const test2 = 2;
function innerBar() {
console.log(myName); // 从背包里拿 myName
}
return innerBar;
}
const bar = foo(); // foo 执行完本应「消失」,但闭包背包留住了 myName/test1/test2
bar(); // 输出:极客时间
-
为什么内存没释放? :因为
innerBar
引用着foo
的变量,JS 引擎不能随便回收。 -
应用场景 :封装数据(如计数器)、防抖节流、模块化等。
(表情包:innerBar 背着背包得意😎:走哪儿带哪儿,妈妈再也不用担心我没变量用啦!)
📸 嵌套函数的作用域链层级:

(图中:foo 函数的作用域链包含 bar → main → 全局,层层向上查找变量)
四、实战演练:看代码猜输出(附解析)
📝 案例 1:作用域链的嵌套
javascript
function outer() {
var x = 1;
function middle() {
var y = 2;
function inner() {
console.log(x + y); // 输出?
}
inner();
}
middle();
}
outer(); // 答案:3(inner → middle → outer 找变量)
📝 案例 2:闭包与变量共享
javascript
function makeCounter() {
var count = 0;
return function() {
return count++;
};
}
const counter = makeCounter();
console.log(counter()); // 0(第一次调用,count 先返回再自增)
console.log(counter()); // 1(闭包记住了 count 的值)
五、避坑指南:闭包的「双刃剑」
-
内存泄漏风险 :滥用闭包会导致变量一直占用内存,记得适时「丢弃背包」(置为
null
)。 -
循环中的闭包陷阱:
javascript
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i); // 输出 3,3,3(var 的 i 是全局作用域,循环结束后 i=3)
}, 100);
}
// 修复:用 let(块级作用域,每次循环创建新的 i)
(表情包:var 坏笑😈:没想到吧!let 叉腰得意👊:用我就对了~)
六、总结:一张图看懂作用域链与闭包
概念 | 核心描述 |
---|---|
作用域链 | 变量查找的「家族树」,从当前作用域逐级向上找,直到全局。 |
闭包 | 内部函数打包带走外部作用域的变量,形成「专属背包」,延长变量生命周期。 |
关键技巧 | 画调用栈和作用域链图(像图片里那样),标注每个变量的「爸爸」是谁! |
(表情包:恍然大悟🤯:原来这么简单!下次调试再也不怕变量迷路啦~)
如果你看懂了这篇文章,恭喜你!JS 作用域链和闭包的「任督二脉」已打通~🚀
记得点赞收藏,下次遇到变量问题时,想想「寻宝路线」和「背包原理」哦!💪
(😜:学会了就快写代码去~)