🚀 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 作用域链和闭包的「任督二脉」已打通~🚀

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

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

相关推荐
JunjunZ几秒前
unibest框架开发uniapp项目:兼容小程序问题
前端·vue.js
lyc2333332 分钟前
鸿蒙Next应用启动框架AppStartup:流程管理与性能优化🚀
前端
Data_Adventure2 分钟前
Vue 3 作用域插槽:原理剖析与高级应用
前端·vue.js
Splendid6 分钟前
Geneformer:基于Transformer的基因表达预测深度学习模型
javascript·算法
EndingCoder7 分钟前
React Native 开发环境搭建(全平台详解)
javascript·react native·react.js·前端框架
curdcv_po11 分钟前
报错 /bin/sh: .../scrcpy-server: cannot execute binary file
前端
小公主11 分钟前
用原生 JavaScript 写了一个电影搜索网站,体验拉满🔥
前端·javascript·css
代码小学僧12 分钟前
通俗易懂:给前端开发者的 Docker 入门指南
前端·docker·容器
Moment14 分钟前
为什么我在 NextJs 项目中使用 cookie 存储 token 而不是使用 localstorage
前端·javascript·react.js
lyc23333314 分钟前
鸿蒙Next加解密算法框架入门:安全基石解析🔐
前端