JavaScript 闭包难到脱发?3 大核心 + 5 个案例,一篇吃透面试必考点 💥

85 阅读6分钟

闭包 —— 这个让无数前端 er 抓秃头皮的 JavaScript 概念😫,堪称面试中的 “常驻嘉宾”。有人说它像 “幽灵”,变量明明该销毁却一直存在;有人说它像 “背包”,总能带走函数里的 “宝贝”。今天,咱们就拆透它的本质,看完这篇,闭包考点再也难不倒你!

一、闭包到底是个啥?一句话讲透 🤫

闭包的本质,是 **“函数嵌套 + 作用域链嵌套” 形成的特殊结构 **。它就像一座隐形的桥梁🌉,把函数内部和外部死死连在一起 —— 哪怕外部函数执行完了,内部函数依然能拽着外部的变量不放。

阮一峰老师在文章里说得更直白:“闭包是将函数内部和函数外部连接起来的桥梁”。
形象点说:闭包就是个 “随身背包”🎒,内部函数被返回时,会把外部函数的变量 “打包” 带走,让这些变量逃过垃圾回收的 “魔爪”。

二、闭包形成的 3 个硬条件(缺一不可)✅

不是随便嵌套个函数就是闭包!必须同时满足这 3 个条件,闭包才能 “生效”:

  1. 函数要嵌套:内部函数得乖乖待在外部函数的 “肚子里”(比如f2f1内部);
  2. 内部函数得 “惦记” 外部变量:内部函数必须用到外部函数的局部变量(这些变量叫 “自由变量”);
  3. 内部函数要 “逃出去” 被调用:外部函数得把内部函数 “放出去”(return),并且这个 “逃出去” 的函数还得在外部被执行。

🌰 举个最经典的例子:

function f1() {
  var n = 999; // 外部函数的局部变量(自由变量)
  function f2() { // 内部函数(闭包函数)
    console.log(n); // 内部函数“惦记”外部变量
  }
  return f2; // 把内部函数“放出去”
}

// 外部函数执行,拿到“逃出去”的内部函数
var result = f1(); 
// 外部调用内部函数,闭包生效!
result(); // 输出 999(居然能拿到f1里的n!)

三、闭包为啥能 “留住” 变量?揭秘底层逻辑 🧠

很多人疑惑:外部函数都执行完了,它的变量为啥没被销毁?这得从两个点说起:

1. 作用域链:变量查找的 “地图”

JavaScript 里,函数内部能访问外部变量,靠的是 “作用域链”—— 每个函数都有自己的作用域,嵌套函数的作用域链会 “套娃”,内部函数能顺着链条找到外部的变量。

比如下面例子里的作用域嵌套:

var n = 999; // 全局作用域
function f1() {
  b = 123; // 没加var/let/const,自动成全局变量(文档5知识点)
  {
    let a = 1; // 块级作用域(只在{}里有效)
  }
  console.log(n); // 内部能访问外部的n(作用域链生效)
}
f1(); // 输出 999

闭包就是利用了这种 “嵌套的作用域链”,让内部函数在外部执行时,依然能顺着链条找到外部变量。

2. 垃圾回收:“引用计数” 在搞鬼

JavaScript 的垃圾回收机制有个 “引用计数” 规则:只要变量被引用着,就不会被销毁

闭包里的自由变量,因为被内部函数 “惦记”(引用),哪怕外部函数执行完了,只要内部函数还在被使用,这些变量就会一直 “赖” 在内存里。

🌰 验证一下:

    function f1() {
      let n = 999;
      // 全局函数nAdd,修改n(闭包外部也能改内部变量)
      nAdd = function() { n += 1; }; 
      function f2() { console.log(n); }
      return f2;
    }

    var result = f1(); 
    result(); // 第一次调用:输出 999
    nAdd(); // 外部修改n
    result(); // 第二次调用:输出 1000(n果然没被销毁!)

两次调用result(),n 的值从 999 变成 1000,完美证明:闭包让变量 “常住” 内存了!

四、闭包能干嘛?3 大实用场景 🔧

闭包不是花架子,实际开发中超有用!这 3 个场景一定要记住:

1. 让外部 “偷看” 函数内部的变量

函数内部的局部变量,本来是 “私有” 的,外部拿不到。但闭包能开个 “小窗口”,让外部间接访问。

比如上面例子的核心作用:通过返回内部函数,让全局能访问f1里的n

2. 让变量 “常驻内存”,保存状态

有些场景需要变量一直 “活着”(比如计数器),闭包就能做到。

🌰 实现一个自增计数器:

    function createCounter() {
      let count = 0; // 这个变量会被闭包“留住”
      return function() {
        return count++; 
      };
    }
    const counter = createCounter();
    console.log(counter()); // 0
    console.log(counter()); // 1(count一直保存在内存里)

3. 隔离作用域,避免全局变量污染

全局变量多了容易冲突,闭包能创建 “独立小房间”,把变量关在里面。

🌰 举个经典案例:

    <script>
    var name = "The Window"; // 全局变量
    var object = {
      name: "My Object",
      getNameFunc: function() {
        var that = this; // 保存当前this(指向object)
        // 返回闭包函数,能访问外部的that
        return function() {
          return that.name; 
        };
      }
    };
    // 输出"My Object",而不是全局的"The Window"
    console.log(object.getNameFunc()()); 
    </script>

这里用闭包 “锁住” 了that(指向object),完美避免了this指向全局的坑!

五、闭包的 “坑”:3 个必须注意的点 ⚠️

闭包虽香,但也有副作用,踩坑了可能出大问题!

1. 可能导致内存泄漏

变量一直 “赖” 在内存里不走,积累多了会让页面变卡。

解决办法:不用的时候,手动 “赶走” 变量 —— 把闭包引用设为null

    var result = f1(); 
    result(); 
    result = null; // 手动切断引用,让垃圾回收机制回收变量

2. 可能偷偷修改父函数的变量,引发混乱

闭包不仅能读变量,还能改!如果多个地方改同一个变量,容易出 bug。

🌰 小心这种情况:

    function f1() {
      let n = 0;
      return {
        add: () => n++, 
        log: () => console.log(n)
      };
    }
    const obj = f1();
    obj.add(); 
    obj.log(); // 输出1(n被偷偷改了)

用的时候一定要理清楚变量的修改逻辑!

3. 自由变量的生命周期 “不受控”

自由变量的 “生死”,不取决于父函数啥时候执行完,而取决于闭包啥时候被销毁。这可能导致变量意外保留状态,比如:

    function f() {
      let arr = [];
      return function(num) {
        arr.push(num);
        console.log(arr);
      };
    }
    const add = f();
    add(1); // [1]
    add(2); // [1,2](arr一直被保留,哪怕f早就执行完了)

六、总结:闭包是把 “双刃剑” 🗡️

闭包让函数内部和外部能 “沟通”,能保存状态、隔离作用域,是前端开发的 “利器”;但也可能导致内存泄漏、变量混乱,用的时候得小心。

记住一句话:理解作用域链和垃圾回收,就理解了闭包的一半;多练案例,就能吃透另一半

下次面试再被问闭包,就把这篇文章的知识点甩出来 —— 保证面试官点头称赞!😎