一句话结论:
JS 的变量查找永远沿「作用域链」往上爬;
闭包则把“本该销毁”的作用域偷偷塞进函数口袋,让它长生不死。
下面用三段递进式代码,从“最简 demo”到“闭包 full feature”,逐行剖给你看。
第一段:最简作用域链——「静态词法」碾压「动态调用」
1var myName = '极客世界'; 2 3function bar() { 4 console.log(myName); // ① 打印什么? 5} 6 7function foo() { 8 var myName = '极客邦'; 9 bar(); // ② 在 foo 里调用 bar 10} 11 12foo(); 13
运行结果
1极客世界 2
为什么不是“极客帮”?
在我们的脑海里看到这幅代码应该很快就能想到下面这幅图
对啊如果是这幅图按调用栈的顺序没找到应该往下找啊,但是这其实是一个坑。 先来讲讲作用域链吧。 作用域链也叫词法作用域链,静态的,只和函数声明的位置有关,在编译阶段就决定好了,和调用没有关系。 真正的查找应该是按作用域链的规则查找,也就是按下面这幅图的规则
- 作用域链在定义时(词法阶段)就写好,跟在哪调用没半毛钱关系;
bar的外部词法环境是 全局;- 变量查找路径:
bar → 全局;永远找不到foo的myName。
记住口诀:“调用位置只影响栈,变量查找只看出生证明。”
第二段:套娃加深——“同名变量”在作用域链上打伏击
1let myAge = 10; 2let test = 1; // 全局 test 3var myName = '极客时间'; // 全局 myName 4 5function bar() { 6 var myName = '极客世界'; // ② bar 函数环境 7 let test1 = 100; 8 if (1) { 9 let myName = 'Chrome 浏览器'; // ③ 块级环境 10 console.log(test); // 打印哪个 test ? 11 } 12} 13 14function foo() { 15 var myName = '极客邦'; // ① foo 函数环境 16 let test = 2; // 遮蔽全局 test 17 { 18 let test = 3; // 块级 test,完全互不影响 19 bar(); // 在块里调用 bar 20 } 21} 22 23foo(); 24
运行结果
11 2
作用域链图解
- 块级
test=3与test=2都住在更外层,对 bar 不可见; - 再次验证:词法作用域只看出身,不看调用栈。
第三段:闭包登场——把外层环境“偷渡”出栈
1function foo() { 2 var myName = '极客时间'; 3 let test1 = 1; 4 let test2 = 2; 5 6 // 1. 对象里两个函数都「引用」外部变量 7 var innerBar = { 8 getName: function () { 9 console.log(test1); // 引用 test1 10 return myName; // 引用 myName 11 }, 12 setName: function (newName) { 13 myName = newName; // 修改外部 myName 14 } 15 }; 16 return innerBar; // 2. 把内部对象抛到外部 17} 18 19var bar = foo(); // foo 执行上下文出栈 20 // 但 myName/test1/test2 因为被引用**不会**被 GC 21 22bar.setName('极客邦'); // 修改的是原来 foo 环境下的 myName 23console.log(bar.getName()); // 1. 先打印 test1 → 1 24 // 2. 再打印返回值 → 极客邦 25
闭包形成条件(背)
- 函数嵌套函数;
- 内部函数引用外部变量;
- 内部函数被返回到外部并存活。
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 秒背完收工
变量查找看出生,调用位置是浮云;
内部函数拎外部,闭包环境永长存。
《作用域链 × 闭包:三段代码,看懂 JavaScript 的套娃人生》 是转载文章,点击查看原文。
