面试官:说一下闭包吧,什么是闭包,闭包的作用以及优点缺点(全网最易理解闭包改概念)

95 阅读4分钟

1. 什么是闭包?

核心定义: 闭包是指那些能够访问和操作其外部作用域中变量的函数,即使其外部函数已经执行完毕。

更通俗地讲,当一个内部函数引用了其外部函数的变量或参数时,就创建了一个闭包。这个内部函数“记住”并“持有”了它被创建时的环境。

闭包的形成有两个必要条件:

  1. 函数嵌套。
  2. 内部函数引用了外部函数的数据(变量/参数)。

代码示例:

function outer() {
  let count = 0; // 外部作用域的变量

  // 内部函数 inner 就是一个闭包
  function inner() {
    count++; // 引用了外部函数的变量 count
    console.log(count);
  }

  return inner; // 将内部函数返回
}

// 调用 outer(),得到内部函数 inner
const myClosure = outer();

// 即使 outer 函数已经执行完毕,myClosure 仍然能访问到 count 变量
myClosure(); // 输出:1
myClosure(); // 输出:2
myClosure(); // 输出:3

发生了什么?

  1. 执行 const myClosure = outer(); 时,outer 函数被调用,创建了变量 count 和函数 inner
  2. outer 函数返回了 inner 函数,并将其赋值给 myClosure
  3. 通常来说,当 outer 执行完毕后,其内部的局部变量 count 应该被垃圾回收。但是,因为返回的 inner 函数仍然在引用 count,所以 count 不会被销毁,而是被“封闭”在了 inner 函数的生命周期里。这就形成了闭包。

2. 闭包的作用

闭包在前端开发和JavaScript编程中无处不在,其主要作用包括:

  1. 创建私有变量: 这是闭包最经典的作用。如上例所示,外部无法直接访问和修改 count 变量,只能通过提供的闭包函数 myClosure 来间接操作,这实现了数据的封装和私有化。

  2. 实现数据持久化/状态保持: 闭包可以让一个函数的局部变量在多次调用之间“存活”下来,就像一个轻量级的全局变量,但又不会污染全局命名空间。上面的计数器例子就是最好的体现。

  3. 在异步编程和回调函数中广泛应用: 在事件监听、setTimeout、Ajax请求等场景中,回调函数常常需要记住它被定义时的上下文信息。

    function sayHelloLater(name) {
      // 内部函数(回调)记住了外部函数的参数 `name`
      setTimeout(function() {
        console.log(`Hello, ${name}!`);
      }, 1000);
    }
    sayHelloLater('Alice'); // 1秒后输出:Hello, Alice!
    

    这里的匿名回调函数就是一个闭包,它记住了参数 name

  4. 模块化开发(Module Pattern): 在ES6的模块系统出现之前,开发者广泛使用闭包来创建模块,模拟公有和私有方法。

    const myModule = (function() {
      let privateVar = 0; // 私有变量
    
      function privateMethod() {
        // 私有方法
      }
    
      return {
        publicMethod: function() {
          // 公有方法,可以访问私有变量和方法
          privateVar++;
          console.log(privateVar);
        }
      };
    })();
    
    myModule.publicMethod(); // 输出:1
    // myModule.privateVar 是无法直接访问的
    

3. 闭包的优点

  • 封装性: 可以创建私有变量和方法,避免全局污染,提高代码的安全性和可维护性。
  • 灵活性: 允许函数携带状态(数据),使得函数的行为更加灵活和强大。
  • 实现高级功能: 是许多高级编程模式(如模块模式、柯里化、函数节流防抖等)的基础。

4. 闭包的缺点与注意事项

闭包最主要的问题是内存泄漏的风险。

  • 内存消耗: 由于闭包会长期驻留内存(直到闭包本身被销毁),比普通函数占用更多的内存。
  • 内存泄漏: 如果闭包的作用域链中包含一些不再需要但占用大量内存的对象(如DOM元素),而这些闭包又被长期持有,就会导致这些对象无法被垃圾回收,从而引发内存泄漏。

示例与解决方案:

// 假设一个函数创建了一个巨大的数据
function heavyOperation() {
  let bigData = new Array(1000000).fill('*'); // 一个很大的数组

  return function() {
    // 这个闭包理论上持有着 bigData 的引用
    console.log('Closure is called');
    // 但实际上,这个闭包可能并不需要 bigData
  };
}

const closureWithBigData = heavyOperation();
// 即使 heavyOperation 执行完,bigData 也不会被回收,因为 closureWithBigData 还在引用它

如何避免?

  • 及时释放引用: 当你确定不再需要一个闭包时,将其设置为 null
    closureWithBigData = null; // 这样,闭包和它引用的 bigData 就可以被垃圾回收了
    
  • 谨慎使用: 在不需要持久化数据时,尽量避免不必要的闭包。

总结

闭包 是JavaScript中一个强大且核心的概念。它是一个能“记住”并访问其词法作用域的函数,即使该函数在其作用域之外执行。

它的核心作用创建私有变量实现数据持久化,广泛应用于模块化、异步回调等场景。

它的优点是增强了封装性和灵活性,但缺点是如果使用不当,可能导致内存泄漏。因此,我们在享受闭包带来的便利时,也需要有意识地管理其生命周期,及时释放不再需要的引用。