闭包

一、 什么是闭包?

1. 通俗定义(一句话回答)

闭包就是一个函数以及它所引用的周边环境(词法环境)的组合。 简单来说,就是内部函数可以访问外部函数的变量,即使外部函数已经执行完毕。

2. 官方/学术定义 (MDN)

A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment).

二、 核心原理

闭包的形成基于 JavaScript 的 词法作用域 (Lexical Scoping) 规则:

  1. 函数的作用域在定义时决定,而不是调用时。
  2. 内部函数持有对外部函数作用域的引用(Scope Chain)。
  3. 当外部函数执行完毕后,因为内部函数还持有引用,所以外部函数的变量对象无法被垃圾回收机制(GC)回收,从而形成了闭包。
text
123456789101112
function createCounter() {
  let count = 0; // 私有变量
  return function() {
    count++;
    console.log(count);
  }
}

const counter = createCounter();
counter(); // 1
counter(); // 2
// count 变量依然存在内存中,但外部无法直接访问

三、 闭包的常见应用场景

面试时列举 2-3 个实际场景非常加分:

1. 数据私有化 / 模拟私有变量 (最常用)

在 ES6 类出现之前,闭包是实现模块化和私有变量的主要方式。

text
123456789101112
function User(name) {
  let _name = name; // 私有变量
  
  return {
    getName: function() {
      return _name;
    },
    setName: function(newName) {
      _name = newName;
    }
  }
}

2. 柯里化 (Currying)

将多参数函数转换成一系列单参数函数。

text
1234567
function makeAdder(x) {
  return function(y) {
    return x + y;
  };
}
const add5 = makeAdder(5);
console.log(add5(2)); // 7

3. 防抖与节流 (Debounce & Throttle)

工具函数中大量使用闭包来保存定时器状态 timer。

text
1234567
function debounce(fn, delay) {
  let timer = null; // 闭包变量,保存定时器状态
  return function(...args) {
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  }
}

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

这是考察闭包最经典的题目,考察 var 和 let 的区别。

题目

text
123456
for (var i = 1; i <= 3; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000);
}
// 输出结果是什么?

答案

  • 结果:输出三次 4。
  • 原因:var 声明的变量是函数作用域(这里变成了全局),循环结束后 i 变成了 4。setTimeout 是异步执行的,当它执行时,去读取 i 的值,此时 i 已经是 4 了。

怎么修改为输出 1, 2, 3?

方法 1:使用 let (推荐) let 有块级作用域,每次循环都会创建一个新的绑定。

text
123
for (let i = 1; i <= 3; i++) {
  setTimeout(function() { console.log(i); }, 1000);
}

方法 2:使用立即执行函数 (IIFE) 构造闭包 通过传参,将当前的 i 保存为局部变量 j。

text
12345
for (var i = 1; i <= 3; i++) {
  (function(j) {
    setTimeout(function() { console.log(j); }, 1000);
  })(i);
}

五、 闭包的缺点与内存泄漏

  • 缺点:闭包会让变量始终保存在内存中,不会被垃圾回收。如果不当使用,会导致内存消耗过大。

  • 内存泄漏误区:闭包本身不是内存泄漏(它是 JS 的特性)。但如果闭包持有的大量对象不再需要使用,却一直不释放,就会导致内存泄漏。

  • 解决方法:将不再使用的闭包引用置为 null。

    text
    123
    let counter = createCounter();
    counter();
    counter = null; // 解除引用,允许 GC 回收
    

六、 面试高分回答策略 (Cheat Sheet)

如果面试官问:“讲讲闭包”,你可以这样回答:

  1. 下定义:

    “闭包是指有权访问另一个函数作用域中变量的函数。简单来说,就是内部函数引用了外部函数的变量,导致这些变量无法被垃圾回收。”

  2. 讲原理:

    “它的核心原理是基于 JavaScript 的词法作用域链。内部函数在定义时就记住了它所处的环境。”

  3. 举例子:

    “在实际开发中,闭包主要有两个用途: 一是保护变量,比如实现数据私有化、模块化; 二是保存变量,比如在防抖节流函数中保存 timer,或者在柯里化函数中保存预设参数。”

  4. 提注意:

    “不过使用闭包要注意内存管理。因为它会阻止垃圾回收,滥用可能导致内存占用过高。在不再使用时,我会手动将引用置为 null。”