以一个面试题引入问题

闭包是 JavaScript 中的一个重要概念,也是面试中经常涉及的一个话题。以下是一个常见的闭包面试题:

问题

for (var i = 1; i <= 5; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000);
}

上述代码的输出是什么?如何修复它?

回答

上述代码的输出将会是连续的数字 "6",输出五次。

这是因为在 setTimeout 中的回调函数是在循环结束后才执行的,此时 i 的值已经变成了 6。由于 JavaScript 中的变量作用域是函数级别的,而不是块级别的,因此每次迭代中的回调函数共享了相同的外部变量 i

要修复这个问题,可以使用闭包来捕获每个迭代中的变量值。可以通过创建一个立即执行函数表达式(IIFE)来包裹回调函数,并将 i 作为参数传递给它。

修复后的代码如下所示:

for (var i = 1; i <= 5; i++) {
  (function(num) {
    setTimeout(function() {
      console.log(num);
    }, 1000);
  })(i);
}

通过将 i 作为参数 num 传递给立即执行函数,每次迭代中的回调函数都会捕获到不同的 num 值,从而输出正确的结果。

另一种解决方案是使用 let 关键字来声明循环变量 i,因为 let 会创建块级作用域,使得每个迭代都有自己的 i 变量。

修复后的代码如下所示:

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

使用 let 关键字声明的变量具有块级作用域,每个迭代都会创建一个新的 i 变量,从而得到正确的输出结果。

应用场景

闭包在 JavaScript 中有许多实际应用场景。以下是一些常见的使用闭包的情况:

  1. 封装私有变量和数据安全: 闭包可以创建私有变量,这些变量对外部代码是不可见的,只能通过暴露的公共接口进行访问。这种方式可以实现数据的封装和安全性,防止外部代码直接访问和修改私有数据。

  2. 模块化开发: 闭包可以用于实现模块化开发,将相关的函数和数据封装在一个闭包内部,暴露公共接口供其他模块使用。这样可以避免全局命名冲突,同时提供一种清晰的组织和管理代码的方式。

  3. 延迟执行和函数记忆: 闭包可以用于延迟执行函数,通过捕获外部变量的值,即使在函数被调用时,仍然可以访问和使用这些变量。这在一些需要在特定时刻执行的情况下非常有用,如定时器、事件处理程序等。另外,闭包还可以用于实现函数记忆,将函数的计算结果缓存起来,避免重复计算,提高性能。

  4. 实现高阶函数和函数柯里化: 闭包可以用于实现高阶函数,即函数可以接受其他函数作为参数或返回一个函数作为结果。这种方式可以实现函数的复用和组合,使得代码更加灵活和可扩展。另外,闭包还可以用于函数柯里化,即将多个参数的函数转换为只接受部分参数的函数,方便函数的复用和组合。

  5. 事件处理和回调函数: 在事件处理和异步编程中,闭包可以用于捕获和访问事件发生时的上下文信息,或者用于传递回调函数,并保留回调函数所需要的数据和状态。

需要注意的是,闭包可能会导致内存泄漏的问题,因为闭包中的函数引用了外部的变量,导致这些变量无法被垃圾回收。因此,在使用闭包时,需要注意及时释放对外部变量的引用,以避免潜在的内存泄漏问题。

应用举例

封装私有变量和数据安全

function createCounter() {
  let count = 0;

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

const counter = createCounter();
counter.increment();
counter.increment();
console.log(counter.getCount()); // 输出:2

在上述例子中,createCounter 函数返回一个包含三个闭包函数的对象。这些闭包函数可以访问和修改 count 变量,但外部代码无法直接访问或修改它,从而实现了私有变量的封装和数据安全。

延迟执行和函数记忆

function delayExecution(message, delay) {
  setTimeout(function() {
    console.log(message);
  }, delay);
}

delayExecution("Hello, world!", 2000);

在上述例子中,delayExecution 函数使用了闭包来延迟执行 console.log,在 2 秒后输出指定的消息。闭包捕获了 message 变量的值,即使在函数被调用之后仍然可以访问。

事件处理和回调函数

function handleClick() {
  let count = 0;

  return function() {
    count++;
    console.log(`Button clicked ${count} times.`);
  };
}

const button = document.querySelector("#myButton");
button.addEventListener("click", handleClick());

在上述例子中,handleClick 函数返回一个闭包函数,用于处理按钮点击事件。闭包函数可以访问和修改 count 变量,每次按钮被点击时,它会增加计数并输出点击次数。

这些例子展示了闭包在不同场景下的实际应用。闭包使得代码具有更高的灵活性和可重用性,能够方便地封装数据和行为,并在需要时访问和操作它们。


A Student on the way to full stack of Web3.