一句话结论:
JS 的变量查找永远沿「作用域链」往上爬;
闭包则把"本该销毁"的作用域偷偷塞进函数口袋,让它长生不死。
下面用三段递进式代码,从"最简 demo"到"闭包 full feature",逐行剖给你看。
第一段:最简作用域链------「静态词法」碾压「动态调用」
javascript
var myName = '极客世界';
function bar() {
console.log(myName); // ① 打印什么?
}
function foo() {
var myName = '极客邦';
bar(); // ② 在 foo 里调用 bar
}
foo();
运行结果
极客世界
为什么不是"极客帮"?
在我们的脑海里看到这幅代码应该很快就能想到下面这幅图
对啊如果是这幅图按调用栈的顺序没找到应该往下找啊,但是这其实是一个坑。 先来讲讲作用域链吧。 作用域链也叫词法作用域链,静态的,只和函数声明的位置有关,在编译阶段就决定好了,和调用没有关系。 真正的查找应该是按作用域链的规则查找,也就是按下面这幅图的规则 
- 作用域链在定义时(词法阶段)就写好,跟在哪调用没半毛钱关系;
bar的外部词法环境是 全局;- 变量查找路径:
bar → 全局;永远找不到foo的myName。
记住口诀:"调用位置只影响栈,变量查找只看出生证明。"
第二段:套娃加深------"同名变量"在作用域链上打伏击
javascript
let myAge = 10;
let test = 1; // 全局 test
var myName = '极客时间'; // 全局 myName
function bar() {
var myName = '极客世界'; // ② bar 函数环境
let test1 = 100;
if (1) {
let myName = 'Chrome 浏览器'; // ③ 块级环境
console.log(test); // 打印哪个 test ?
}
}
function foo() {
var myName = '极客邦'; // ① foo 函数环境
let test = 2; // 遮蔽全局 test
{
let test = 3; // 块级 test,完全互不影响
bar(); // 在块里调用 bar
}
}
foo();
运行结果
1
作用域链图解

- 块级
test=3与test=2都住在更外层,对 bar 不可见; - 再次验证:词法作用域只看出身,不看调用栈。
第三段:闭包登场------把外层环境"偷渡"出栈
javascript
function foo() {
var myName = '极客时间';
let test1 = 1;
let test2 = 2;
// 1. 对象里两个函数都「引用」外部变量
var innerBar = {
getName: function () {
console.log(test1); // 引用 test1
return myName; // 引用 myName
},
setName: function (newName) {
myName = newName; // 修改外部 myName
}
};
return innerBar; // 2. 把内部对象抛到外部
}
var bar = foo(); // foo 执行上下文出栈
// 但 myName/test1/test2 因为被引用**不会**被 GC
bar.setName('极客邦'); // 修改的是原来 foo 环境下的 myName
console.log(bar.getName()); // 1. 先打印 test1 → 1
// 2. 再打印返回值 → 极客邦
闭包形成条件(背)
- 函数嵌套函数;
- 内部函数引用外部变量;
- 内部函数被返回到外部并存活。
V8 底层怎么做到的?
按理说在var bar=foo() ;这一步的结束的时候要执行出栈操作 bar 里面的变量要回收吧? 但是实际情况并没有这是为什么呢?其实是因为foo 函数执行完后,其执行上下文从栈顶弹出了,但是由于返回的setName,getName 使用了foo 函数内部的变量myName和tsst1,这两个变量依然在内存中,有点像getName,setName 方法背的一个专属背包。 这个背包我们就叫做闭包,这个闭包里面的变量叫自由变量 看图理解吧

因为你在 foo() 内部定义了 getName 和 setName 这两个函数,并且它们引用了 foo 内部的变量 (比如 myName、test1),那么:
- 这两个函数就形成了闭包。
- 它们会"记住"自己被创建时所处的词法环境 (也就是
foo的作用域)。 - 即使
foo()执行完毕、调用栈弹出,只要这两个函数还被外部(比如bar)引用着,JavaScript 引擎就不会销毁foo中被引用的那些变量。
你可以形象地理解为:
每个函数都背着一个小背包(闭包),里面装着它创建时能访问到的外部变量。
- 于是
test1还能读到 1,myName还能被改。
口诀:"栈帧可死,闭包长存。"
日常开发 3 句忠告
- 少用闭包随便拉数据------内存泄漏就是这么来的;
- 循环里返回函数先
let再包,别用var; - 调试打开 DevTools → Scope 面板,Local/Closure/Global 一眼看清变量到底从哪来。
30 秒背完收工
变量查找看出生,调用位置是浮云;
内部函数拎外部,闭包环境永长存。