文章

闭包

闭包

闭包(Closure)

闭包是 JavaScript 中一个重要的概念,它允许函数访问其定义时的词法作用域,即使在执行时该作用域已经不存在。闭包是许多高级 JavaScript 技巧的基础,包括函数工厂、回调函数、数据隐藏等。

一、什么是闭包?

闭包(Closure)是指一个函数能够记住它定义时的作用域,并且可以在定义之外的地方继续访问这些作用域中的变量。即使这个函数在其外部函数已经执行完毕后,仍然能够使用外部函数中的变量。

简单来说,闭包就是能够访问外部函数变量的内部函数

闭包的基本结构

闭包通常由嵌套函数构成,内层函数可以访问外层函数的变量,即使外层函数已经执行完毕。

1
2
3
4
5
6
7
8
9
10
11
12
function outerFunction() {
  const outerVar = 'Hello from the outer function!';

  function innerFunction() {
    console.log(outerVar);  // 闭包:可以访问 outerFunction 的变量
  }

  return innerFunction;
}

const closure = outerFunction();
closure();  // 输出: Hello from the outer function!

在这个例子中,innerFunction 是一个闭包,它访问了 outerFunction 中的变量 outerVar,即使 outerFunction 已经执行完毕。

二、闭包的工作原理

为了理解闭包,我们需要先了解 JavaScript 的词法作用域作用域链

1. 词法作用域

JavaScript 的作用域是词法作用域,这意味着函数的作用域在函数定义时就确定了,而不是在函数执行时确定。函数在定义时能够“记住”它在何处定义的,并且能够访问它定义时所在的作用域。

2. 作用域链

在 JavaScript 中,每个函数都有一个作用域链。函数执行时,会首先在自身作用域内查找变量,如果找不到,就会沿着作用域链向上查找,一直到全局作用域。

在闭包中,内层函数始终能通过作用域链访问到外层函数的变量。

1
2
3
4
5
6
7
8
9
10
11
12
function outer() {
  const a = 10;

  function inner() {
    console.log(a);  // 闭包访问外部作用域的变量
  }

  return inner;
}

const innerFunction = outer();
innerFunction();  // 输出: 10

在上面的例子中,inner 函数通过闭包访问到了 outer 函数中的变量 a

三、闭包的实际应用

闭包在实际开发中非常有用,尤其是在数据隐藏、函数工厂、回调函数等场景下。以下是一些常见的使用场景。

1. 数据隐藏和私有变量

闭包可以用于创建私有变量,防止外部直接访问和修改。这在构建模块或类时非常有用,可以通过闭包实现封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
function createCounter() {
  let count = 0;  // 私有变量

  return function() {
    count++;
    return count;
  };
}

const counter = createCounter();
console.log(counter());  // 输出: 1
console.log(counter());  // 输出: 2
console.log(counter());  // 输出: 3

在这个例子中,countcreateCounter 函数中的局部变量,只有闭包才能访问它。外部代码无法直接访问或修改 count,实现了数据的隐藏和封装。

2. 函数工厂

闭包可以用于创建函数工厂,生成具有不同参数的函数。这是一种非常灵活的设计模式。

1
2
3
4
5
6
7
8
function createAdder(x) {
  return function(y) {
    return x + y;
  };
}

const add5 = createAdder(5);
console.log(add5(10));  // 输出: 15

在这个例子中,createAdder 是一个工厂函数,它返回一个新的函数。这个新的函数通过闭包记住了 x 的值,因此可以执行 x + y

3. 回调函数

回调函数是 JavaScript 中常用的异步处理模式,闭包在这里同样发挥着重要作用。回调函数能够记住它定义时的上下文信息,即使在异步操作中仍然可以使用这些信息。

1
2
3
4
5
6
7
8
9
10
function fetchData(url, callback) {
  setTimeout(function() {
    const data = `Data from ${url}`;
    callback(data);
  }, 1000);
}

fetchData('https://api.example.com', function(response) {
  console.log(response);  // 输出: Data from https://api.example.com
});

在这个例子中,匿名回调函数通过闭包访问了 fetchData 执行时的参数 data

4. 模拟块级作用域

在 ES6 之前,JavaScript 没有块级作用域。开发者可以利用闭包来模拟块级作用域,防止全局变量污染。

1
2
3
4
5
6
7
for (var i = 0; i < 5; i++) {
  (function(j) {
    setTimeout(function() {
      console.log(j);  // 输出: 0, 1, 2, 3, 4
    }, 1000);
  })(i);
}

在这个例子中,立即执行函数表达式(IIFE)创建了一个闭包,每次迭代都保存了当前的 i 值。这样,异步操作 setTimeout 执行时能够正确输出迭代值。

5. 节流与防抖(Throttling 和 Debouncing)

在性能优化中,闭包常用于实现节流和防抖函数。它们可以控制函数调用频率,避免某些操作(如窗口调整大小、滚动事件)频繁触发。

1
2
3
4
5
6
7
8
9
10
11
12
13
function debounce(func, wait) {
  let timeout;
  return function(...args) {
    clearTimeout(timeout);
    timeout = setTimeout(() => func.apply(this, args), wait);
  };
}

const handleResize = debounce(() => {
  console.log('Window resized');
}, 300);

window.addEventListener('resize', handleResize);

在这个例子中,debounce 函数通过闭包保存了 timeout,防止在规定的时间内多次触发 handleResize 函数。

四、闭包的优点与注意事项

优点

  • 数据封装:闭包可以保护变量不被外部访问,实现数据隐藏。
  • 模块化:闭包可以创建模块和工厂函数,有助于代码的模块化和复用性。
  • 延迟执行:闭包允许在未来的某个时间点使用当前作用域中的变量,尤其在异步编程中很有用。

注意事项

  • 内存占用:由于闭包会保持对外部函数作用域的引用,如果使用不当,可能会导致内存泄漏。
  • 调试困难:闭包的作用域链有时可能使调试变得复杂,特别是在嵌套较深时。

五、总结

闭包是 JavaScript 中强大且灵活的工具,它允许函数访问定义时的作用域变量。通过闭包,我们可以实现私有变量、工厂函数、异步操作以及性能优化等多种应用场景。

本文由作者按照 CC BY 4.0 进行授权