闭包是什么?

想象一个场景:你有一个小盒子(内部函数),它可以随时使用大盒子(外部函数)里的东西,即使小盒子被拿到了别的地方。这个“记住外部环境”的能力,就是闭包的核心。

在技术上,闭包是指一个函数和它的词法环境(Lexical Environment)的组合。词法环境包含了函数创建时能访问的所有变量。换句话说,闭包让函数能够访问它定义时所在的“上下文”。

来看一个简单的例子:

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

在这个例子中,createCounter 定义了一个内部函数,这个函数访问了外部的 count 变量。即使 createCounter 执行完毕,count 变量本该销毁,但内部函数通过闭包“记住”了它,每次调用都能继续操作 count

闭包的实际应用

闭包的强大之处在于它的灵活性,下面是几个常见的应用场景。

1. 数据私有化

闭包可以用来创建私有变量,外部代码无法直接访问,只能通过我们提供的方法操作。这在需要保护数据的场景中非常有用。

function createWallet(initialMoney) {
  let money = initialMoney  // 私有变量
  return {
    getMoney: function() {
      return money
    },
    deposit: function(amount) {
      money += amount
    },
    withdraw: function(amount) {
      if (amount <= money) {
        money -= amount
        return true
      }
      return false
    }
  }
}

const myWallet = createWallet(100)
console.log(myWallet.getMoney())  // 100
myWallet.deposit(50)
console.log(myWallet.getMoney())  // 150
myWallet.withdraw(30)
console.log(myWallet.getMoney())  // 120

在这个例子中,money 是一个私有变量,外部无法直接修改,只能通过 getMoneydepositwithdraw 方法操作。这就像一个真正的钱包,只有通过“接口”才能存取钱。

2. 创建函数工厂

闭包可以用来生成定制化的函数,每个函数“记住”不同的参数值。

function multiplier(factor) {
  return function(number) {
    return number * factor
  }
}

const double = multiplier(2)
const triple = multiplier(3)

console.log(double(5))  // 10
console.log(triple(5))  // 15

multiplier 是一个函数工厂,每次调用都会生成一个新的函数,分别“记住”不同的 factor 值。这种方式让代码更模块化,适合批量创建类似功能的函数。

3. 处理事件回调

闭包在事件处理中非常常见,尤其是当你需要为多个元素绑定不同的事件行为时。

function setupButtonAction(buttonId, message) {
  document.getElementById(buttonId).addEventListener('click', function() {
    alert(message)  // 闭包记住 message
  })
}

setupButtonAction('btn1', '你点击了第一个按钮')
setupButtonAction('btn2', '你点击了第二个按钮')

这里,每个按钮的点击事件都通过闭包“记住”了不同的 message 值。这种方式让动态绑定事件变得简单而灵活。

闭包的注意事项

虽然闭包很强大,但使用时也需要注意一些潜在的陷阱。

1. 循环中的闭包陷阱

在循环中创建闭包时,可能会遇到意外的结果,尤其是使用 var 声明变量时。

function createButtons() {
  for (var i = 0; i < 3; i++) {
    const button = document.createElement('button')
    button.textContent = '按钮 ' + i
    button.addEventListener('click', function() {
      alert('这是按钮 ' + i)  // 总是显示“这是按钮 3”
    })
    document.body.appendChild(button)
  }
}

问题出在 var 的作用域是整个函数,循环结束后,i 的值变成了 3,所有按钮的事件处理函数都会引用同一个 i。解决方法是使用 let,因为 let 具有块级作用域,每次循环都会创建一个新的 i

function createButtonsCorrect() {
  for (let i = 0; i < 3; i++) {
    const button = document.createElement('button')
    button.textContent = '按钮 ' + i
    button.addEventListener('click', function() {
      alert('这是按钮 ' + i)  // 正确显示按钮编号
    })
    document.body.appendChild(button)
  }
}

2. 内存管理

闭包会保持对外部变量的引用,这可能导致变量无法被垃圾回收。如果闭包引用了大量数据,可能会造成内存泄漏。

let largeDataClosure = function() {
  let largeData = new Array(1000000).fill('data')  // 占用大量内存
  return function() {
    return largeData.length
  }
}()

// 使用完后
largeDataClosure = null  // 释放引用,帮助垃圾回收

在不再需要闭包时,显式地将它设为 null,可以帮助 JavaScript 的垃圾回收机制释放内存。

闭包的工作原理

闭包的实现依赖于 JavaScript 的作用域链和垃圾回收机制。每个函数在创建时都会绑定一个词法环境,记录了它能访问的所有变量。当函数被调用时,它会沿着作用域链查找变量,即使这些变量所在的外部函数已经执行完毕。这就是为什么闭包能够“记住”外部变量。

但这也带来了一些开销:

  • 内存占用:闭包会延长变量的生命周期,可能导致内存使用量增加。
  • 性能影响:作用域链的查找比直接访问变量稍慢,尤其在嵌套较深的闭包中。

因此,在使用闭包时,需要权衡灵活性和性能,尤其是在大规模应用中。