【js篇】深入理解 JavaScript 闭包

58 阅读5分钟

在 JavaScript 的世界中,闭包(Closure) 是一个让初学者困惑、让面试官钟爱、让高手依赖的核心机制。它不仅是函数式编程的基石,更是实现私有变量、模块化、防抖节流等功能的关键。

今天,我们就来系统性地剖析闭包的本质、用途与经典应用场景,帮你彻底打通这一“任督二脉”。


一、什么是闭包?—— 定义与本质

✅ 核心定义:

闭包是指有权访问另一个函数作用域中变量的函数。

更通俗地说:

  • 当一个函数能够“记住”并访问其外部函数的作用域时,就形成了闭包。
  • 即使外部函数已经执行完毕,其内部变量依然可以被内部函数访问。

🔧 创建闭包的最常见方式:

function outer() {
  let secret = 'I am private';
  
  function inner() {
    console.log(secret); // 可以访问 outer 的变量
  }
  
  return inner; // 返回 inner 函数
}

const closureFunc = outer();
closureFunc(); // 输出: I am private ✅

在这个例子中:

  • inner 函数就是闭包;
  • 它保留了对 outer 函数作用域的引用;
  • 即使 outer() 执行结束,secret 变量也不会被垃圾回收。

二、闭包的两大核心用途

🎯 用途一:在函数外部访问函数内部的变量(创建私有变量)

JavaScript 没有原生的 private 关键字,但我们可以利用闭包模拟私有变量。

function createCounter() {
  let count = 0; // 外部无法直接访问

  return {
    increment: function() {
      count++;
    },
    decrement: function() {
      count--;
    },
    getValue: function() {
      return count;
    }
  };
}

const counter = createCounter();
counter.increment();
counter.increment();
console.log(counter.getValue()); // 2 ✅
// count 无法被外部直接修改,实现了数据封装

💡 这就是现代 JS 模块模式的基础!


🎯 用途二:保持变量在内存中不被回收

闭包会保留对外部变量的引用,因此这些变量不会被垃圾回收机制清除。

function A() {
  let a = 1;
  window.B = function () {
    console.log(a); // B 可以访问 A 中的 a
  };
}
A();
B(); // 输出: 1 ✅

即使 A() 已经执行完毕,变量 a 依然存在于内存中,因为全局函数 B 通过闭包引用了它。


三、经典面试题:循环中的闭包问题

❌ 问题代码:使用 var 导致输出全是 6

for (var i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i); // 全部输出 6 ❌
  }, i * 1000);
}

🔍 原因分析:

  1. var 声明的 i 是函数作用域(或全局),只有一个 i
  2. setTimeout 是异步的,等它执行时,循环早已结束,此时 i === 6
  3. 所有 timer 函数都共享同一个 i

✅ 解决方案一:使用立即执行函数(IIFE)创建闭包

for (var i = 1; i <= 5; i++) {
  (function(j) {
    setTimeout(function timer() {
      console.log(j); // j 是每次循环的副本
    }, j * 1000);
  })(i);
}

📌 原理:

  • 每次循环都调用 IIFE,将当前的 i 值传给参数 j
  • j 成为每次迭代的“快照”,被 timer 函数通过闭包引用;
  • 因此每个 timer 都能访问到正确的值。

✅ 解决方案二:利用 setTimeout 的第三个参数

for (var i = 1; i <= 5; i++) {
  setTimeout(
    function timer(j) {
      console.log(j);
    },
    i * 1000,
    i // 第三个参数作为 timer 的参数传入
  );
}

📌 原理:

  • setTimeout(func, delay, arg1, arg2, ...) 支持传递参数;
  • i 被当作参数传给 timer,形成独立的作用域。

✅ 解决方案三:使用 let(推荐!)

for (let i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i); // 正确输出 1~5 ✅
  }, i * 1000);
}

📌 原理:

  • let 声明具有块级作用域
  • 每次循环都会创建一个新的 i 绑定(词法环境);
  • 每个 timer 函数闭包引用的是各自迭代中的 i

这是最简洁、最现代的写法,强烈推荐在项目中使用 let/const 替代 var


四、闭包的实际应用场景

🌐 1. 模块化设计(Module Pattern)

const MyModule = (function() {
  let privateVar = 'private';

  function privateMethod() {
    console.log('This is private');
  }

  return {
    publicMethod: function() {
      console.log(privateVar);
      privateMethod();
    }
  };
})();

MyModule.publicMethod(); // 正常访问

⏱️ 2. 防抖(Debounce)与节流(Throttle)

function debounce(func, delay) {
  let timer;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => func.apply(this, args), delay);
  };
}

const debouncedSearch = debounce(searchAPI, 300);

利用闭包保存 timer 变量,实现延迟执行。


🧩 3. 函数柯里化(Currying)

function add(a) {
  return function(b) {
    return a + b;
  };
}

const add5 = add(5);
console.log(add5(3)); // 8

b 函数通过闭包访问外部的 a


五、闭包的潜在问题:内存泄漏

⚠️ 注意: 闭包会阻止变量被回收,如果使用不当,可能导致内存泄漏。

function heavyFunction() {
  const largeData = new Array(1000000).fill('data');

  window.getData = function() {
    return largeData; // 一直持有引用
  };

  // largeData 不会被释放,即使 heavyFunction 执行完毕
}

建议:

  • 避免在闭包中引用不必要的大对象;
  • 使用完后手动解除引用:window.getData = null

六、总结:闭包核心要点一览

特性说明
定义能访问外部函数变量的函数
形成条件内部函数被返回或暴露到外部
用途1实现私有变量与模块化
用途2保持变量存活(不被回收)
经典问题循环中 var 导致的共享变量问题
解决方案IIFE、setTimeout 参数、let
最佳实践优先使用 let/const,避免内存泄漏

💡 结语

“闭包不是魔法,而是作用域链的自然延伸。”

理解闭包,本质上是理解 JavaScript 的词法作用域执行上下文。它是你迈向高级前端开发的必经之路。

无论你是想写出更优雅的代码,还是应对面试中的“经典循环题”,掌握闭包都将让你游刃有余。

📌 记住:

  • 闭包 = 函数 + 对外部作用域的引用;
  • let 是解决循环闭包问题的最佳武器;
  • 用得好是利器,用不好是隐患。