- Published on
闭包
- Authors

- Name
- Li WenKang
- https://x.com/liwenkang_space
这篇我们来关注闭包
什么是闭包?
闭包是由函数和它所在词法环境组合而成的实体,这个环境包含了闭包创建时,所能访问的所有局部变量。所以当外部函数已经执行完后,依然可以通过闭包(内部函数)访问到当时词法环境的变量。
如何创建闭包:
- 存在函数嵌套(在外部函数中,再定义一个内部函数)
- 内部函数使用了外部函数中的变量
- 内部函数被导出,包括三种场景:(1)直接将内部函数作为返回值;(2)作为参数传递给其它变量;(3)被赋予给一个全局变量或对象的属性
// 一个最简单的闭包示例
function createCounter() {
let count = 0
function innerFunction(val) {
count = count + val
return count
}
return innerFunction
}
const counter = createCounter()
counter(1) // 1
counter(2) // 3
闭包有什么用?
- 创建私有变量,实现数据封装。即在函数外部无法访问和修改函数内部定义的变量值,只能通过闭包提供的方法操作数据,有利于代码的安全性和模块化
// 示例
function counterFunction() {
let count = 0
return {
plus(v) {
count = count + v
return count
},
getCount(v) {
return count
},
}
}
const counter = counterFunction()
console.log('counter().getCount()', counter.getCount()) // 0
console.log('counter().getCount()', counter.plus(1)) // 1
console.log('counter().getCount()', counter.getCount()) // 1
console.log('counter().getCount()', counter.plus(2)) // 3
console.log('counter().getCount()', counter.getCount()) // 3
- 因为内部函数(闭包)持有对这些变量的引用,导致它们无法被垃圾回收机制回收,从而“延长”了其生命周期,对于需要记住上一次操作状态的场景非常有用,例如计数器,缓存函数
// 1. 计数器--持久化的计数状态
// 1.1 工厂函数的实现
function createCounter() {
let count = 0
return {
plus: (v) => {
count = count + v
return count
},
getData: () => {
return count
},
}
}
const counterA = createCounter()
console.log('getData', counterA.getData())
console.log('plus', counterA.plus(1))
console.log('getData', counterA.getData())
console.log('plus', counterA.plus(2))
console.log('getData', counterA.getData())
// 1.2 立即执行函数
const counter = (() => {
let count = 0
return function () {
count = count + 1
return count
}
})()
console.log(counter()) // 输出: 1
console.log(counter()) // 输出: 2
// 缓存函数
function memoize(fn) {
const cache = {} // 缓存对象,被闭包“记住”
return function (...args) {
// 将参数序列化后作为缓存键
const key = JSON.stringify(args)
// 如果缓存中存在该键,直接返回缓存结果
if (cache[key] !== undefined) {
console.log('从缓存读取结果')
return cache[key]
}
// 否则,计算新结果并存入缓存
console.log('执行计算过程')
const result = fn.apply(this, args)
cache[key] = result
return result
}
}
function memoizeWithMap(fn) {
const cache = new Map() // 使用 Map 作为缓存容器
return function (...args) {
// 直接将参数数组作为键
if (cache.has(args)) {
return cache.get(args)
}
const result = fn.apply(this, args)
cache.set(args, result)
return result
}
}
- 解决异步编程中,避免因变量共享导致的问题。例如在循环中处理异步事件(如setTimeout),使用 var 声明变量时,会出现打印值始终是最后一次的值的问题。当使用 let 或 立即执行函数,在每一次循环都创建独立的作用域后,就可以解决这个问题
// 原始问题
for (var i = 0; i < 10; i++) {
setTimeout(() => {
console.log(i) // 1s 后,输出 10 次 10
}, 1000)
}
// 利用 let 的块级作用域特性
for (let i = 0; i < 10; i++) {
setTimeout(() => {
console.log(i) // 1s 后,输出 0~9
}, 1000)
}
// 利用立即执行函数,会创建独立的函数作用域
for (var i = 0; i < 10; i++) {
;((num) => {
setTimeout(() => {
console.log(num) // 1s 后,输出 0~9
}, 1000)
})(i)
}
最佳实践是什么?
- 警惕内存泄露:在闭包使用完成后,需要手动解除引用,将引用的值指向 null
- 在事件监听,定时器场景下,也要注意及时清理
- 闭包捕获的是变量的引用,所以如果在闭包创建后,执行之前值发生了修改,最后闭包访问到的会是最新的值。
// 最后闭包访问到的是最新值(如果在闭包创建后,执行之前发生了变化的话)
function createFunctions() {
const functions = []
for (var i = 0; i < 3; i++) {
// 闭包(箭头函数)捕获的是变量 i 的引用
functions.push(() => {
console.log(num)
})
}
return functions // 返回一个包含三个函数的数组
}
// 创建闭包时,循环中的 i 被捕获的是引用
const funcArray = createFunctions()
// 此时循环早已结束,变量 i 的最终值是 3
// console.log("循环结束后 i 的值为:", i); // 输出: 循环结束后 i 的值为: 3
// 执行闭包函数,访问的是同一个 i 的当前值
funcArray[0]() // 输出: 3
funcArray[1]() // 输出: 3
funcArray[2]() // 输出: 3
// 如何解决这一问题
// 1. 换用 let
// 2. 使用立即执行函数
扩展内容
- 立即执行函数(IIFE)与闭包的关系:IIFE是如何通过创建独立作用域来解决循环中的变量共享问题的?
- let与闭包的协同作用:为什么使用let可以避免var的变量提升和函数级作用域问题,从而减少对闭包的依赖。
- 现代JavaScript中的闭包应用:闭包在React Hooks、模块化开发等现代框架中的实际应用。
- 引申到内存泄露的排查方法有哪些