🚀 JS 作用域链与闭包:从「寻宝游戏」到「神秘背包」的底层探秘

「为什么我的变量总是'失踪'?」

「函数执行完后变量怎么还在内存里?」

别慌!今天用「寻宝」和「背包」的比喻,带你吃透 JS 作用域链和闭包的底层逻辑~😉

一、JS 执行的「幕后黑手」:调用栈与执行上下文

先讲个小故事:

JS 引擎就像一个勤劳的「代码管家」,每次执行函数时都会创建一个「执行上下文」小本本,记录当前函数的变量、作用域等信息。

而「调用栈」就像一堆叠起来的盘子,按顺序存放这些小本本,保证函数们按规则执行~🍽️

🤖 举个栗子:

javascript 复制代码
function foo() {
  var myname = "极客邦";
  function bar() {
    console.log(myname); // 这里会找到谁?
  }
  bar();
}
foo();
  1. 全局执行上下文先入栈,记录全局变量(如果有的话)。

  2. 调用 foo() 时,foo 的执行上下文入栈,创建 myname 变量(var 会变量提升哦~)。

  3. 调用 bar() 时,bar 的执行上下文入栈,它需要找 myname,但自己没有,怎么办?

📸 看下图!执行上下文与作用域链的关系:

(图中问题:bar 函数中的 myName 会取全局还是 foo 的值?答案见下文~)

二、作用域链:变量「寻宝」的路线图

想象每个执行上下文都有一张「寻宝地图」,上面写着:「先找自己的口袋,再问爸爸,最后问爷爷(直到全局)」~🔍

这就是 作用域链当前作用域 → 父作用域 → ... → 全局作用域 的查找路径。

🔍 寻宝规则详解:

  1. 词法作用域(静态规则)

    变量的「爸爸」是谁,在代码写出来时就定了,和函数怎么调用无关!

    比如:

    javascript 复制代码
    function bar() {
      console.log(myname); // 我爸爸是全局
    }
    function foo() {
      var myname = "极客邦";
      bar(); // 虽然在 foo 里调用 bar,但 bar 的爸爸还是全局!
    }
    var myname = "极客时间";
    foo(); // 输出:极客时间(全局变量)

    (表情包:bar 摊手🤷‍♂️:我天生属于全局爸爸,别想骗我认 foo 当爹!)

  2. 变量提升与 TDZ(坑点警告)

    • var 变量会「提前搬家」到作用域顶部(初始值 undefined)。
    • let/const 有「暂时性死区(TDZ)」,声明前使用会报错!
    ini 复制代码
      console.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 作用域链和闭包的「任督二脉」已打通~🚀

记得点赞收藏,下次遇到变量问题时,想想「寻宝路线」和「背包原理」哦!💪

(😜:学会了就快写代码去~)

相关推荐
崔庆才丨静觅7 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60618 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了8 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅8 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅8 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅9 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment9 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅9 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊9 小时前
jwt介绍
前端
爱敲代码的小鱼9 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax