闭包是指函数与其词法环境的组合,也就是内部函数可以访问外部函数作用域中的变量,即使外部函数已经执行完毕。
什么是闭包?
在 javascript 中,每当创建一个函数,就会形成闭包。更准确地说,闭包是由函数以及该函数被声明时所在的作用域共同组成的。这个作用域包含了函数内部引用的所有外部变量。
看一个最简单的闭包示例:
1 | function outer() { |
在这个例子中,inner 函数使用了外部函数 outer 的变量 message。当 outer 执行完毕返回 inner 后,按照常理 outer 的作用域应该被销毁,但由于 inner 仍然持有对 message 的引用,javascript 引擎会保留这个变量,使得 inner 可以在外部继续访问它。这个 inner 函数及其引用的环境就构成了一个闭包。
闭包的原理
闭包的形成依赖于 javascript 的词法作用域(静态作用域)和垃圾回收机制。
- 词法作用域:函数的作用域在定义时就已经确定,而不是在执行时确定。因此,内部函数总能访问外部函数中声明的变量。
- 垃圾回收:通常,当函数执行完后,其局部变量会被标记为可回收。但如果还有别的函数(如内部函数)仍然引用这些变量,它们就不会被回收,从而继续存在。
闭包常见的应用场景
创建私有变量
javascript 没有真正的私有成员,但可以利用闭包模拟私有变量。通过函数作用域隐藏变量,只暴露特定方法进行操作。
1 | function createCounter() { |
函数工厂
闭包可以用于生成具有特定行为的函数,例如根据参数创建不同的处理函数。
1 | function multiplyBy(factor) { |
回调函数与事件处理
在异步操作或事件监听中,闭包常用于保留状态。
1 | for (var i = 0; i < 3; i++) { |
上面的例子中,由于 var 没有块级作用域,所有回调共享同一个 i,循环结束后 i 变为 3,因此输出三个 3。利用闭包可以解决这个问题:
1 | for (var i = 0; i < 3; i++) { |
或者使用 let 创建块级作用域:
1 | for (let i = 0; i < 3; i++) { |
模块化
在 ES6 模块出现之前,闭包常用于实现模块模式,隔离内部实现,只暴露公共 API。
1 | var myModule = (function() { |
闭包的内存管理
由于闭包会持续引用外部函数的变量,这些变量无法被垃圾回收,如果使用不当可能导致内存泄漏。例如,在不需要闭包时,仍然保持引用:
1 | function heavyProcess() { |
上例中 largeData 无法被回收,因为闭包一直引用它。如果不再需要闭包,应该将引用设为 null:
1 | leak = null; // 解除引用,允许垃圾回收 |
闭包与性能
闭包虽然强大,但过度使用可能带来性能问题,因为每个闭包都额外维护自己的作用域链。在频繁创建大量闭包的场景(如循环中创建函数)时需谨慎,尽量重用函数。
总结
- 闭包 = 函数 + 函数定义时的词法环境。
- 内部函数可以访问外部函数的变量,即使外部函数已经返回。
- 常见用途:私有变量、函数工厂、回调、模块化。
- 注意内存管理,及时释放不再需要的闭包。
- 现代 javascript 中,let/const 和箭头函数可以简化某些闭包场景,但闭包的核心思想依然不变。