最后更新:2026-02-14

闭包(Closure)是 JavaScript 面试里最常考的概念之一,但很多人背下了定义却说不清它是怎么来的。

我当时也一样,面试前背:「闭包是能访问其他函数作用域中变量的函数」,背完了,但遇到代码题还是搞不定。后来是反复看、反复敲代码,才算真正想明白了。

从作用域说起

要理解闭包,先得清楚 JavaScript 的作用域规则:词法作用域(Lexical Scope)。

意思是:函数的作用域在定义时就确定了,不是运行时决定的。

1
2
3
4
5
6
7
8
9
10
11
12
13
let name = 'global'

function outer() {
let name = 'outer'

function inner() {
console.log(name) // 输出 'outer',不是 'global'
}

inner()
}

outer()

inner 在定义时所在的环境是 outer 函数内部,所以它访问的 nameouter 里的那个,跟外面全局的 name 没关系。这就是词法作用域。

闭包是怎么形成的

正常情况下,函数执行完毕,它的局部变量就被销毁了。但有一种情况例外:

1
2
3
4
5
6
7
8
9
10
11
12
13
function counter() {
let count = 0 // 局部变量

return function () {
count++
console.log(count)
}
}

const add = counter()
add() // 1
add() // 2
add() // 3

counter 执行完了,按理说 count 应该被销毁。但 counter 返回了一个函数,这个函数引用了 count。只要这个返回的函数还在,count 就不会被回收。

这就是闭包:函数 + 它能访问的外部变量(作用域)打包在一起

一个更直观的例子

1
2
3
4
5
6
7
8
9
10
11
function createGreeter(greeting) {
return function (name) {
console.log(`${greeting}, ${name}!`)
}
}

const sayHello = createGreeter('Hello')
const sayHi = createGreeter('Hi')

sayHello('Eric') // Hello, Eric!
sayHi('Tom') // Hi, Tom!

sayHellosayHi 各自记住了自己的 greeting,互不干扰。每次调用 createGreeter 都创建了一个新的作用域,返回的函数绑定的是那次调用时的 greeting

经典问题:循环里的闭包

这是面试里最常见的闭包题,很多人栽在这里:

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

为什么?因为 var 没有块级作用域,三次循环共用同一个 i。定时器回调执行时,循环早结束了,i 已经是 3。

解法一:改用 let(最推荐)

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

let 有块级作用域,每次循环都是独立的 i,问题直接解决。

解法二:用 IIFE(立即执行函数)捕获变量

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

每次循环执行 IIFE,把当前 i 的值作为参数传进去,内部的 j 是各自独立的副本。

闭包的实际用途

数据私有化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function createAccount(initialBalance) {
let balance = initialBalance // 外部访问不到

return {
deposit(amount) {
balance += amount
},
withdraw(amount) {
if (amount > balance) {
console.log('余额不足')
return
}
balance -= amount
},
getBalance() {
return balance
}
}
}

const account = createAccount(1000)
account.deposit(500)
account.withdraw(200)
console.log(account.getBalance()) // 1300

// 外部无法直接修改 balance
console.log(account.balance) // undefined

这是 JavaScript 里模拟私有变量的经典方式,在 ES6 class 私有字段出现之前很常用。

函数工厂

1
2
3
4
5
6
7
8
9
10
// 生成带特定倍数的乘法函数
function multiplier(factor) {
return (number) => number * factor
}

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

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

缓存(Memoize)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function memoize(fn) {
const cache = {}

return function (...args) {
const key = JSON.stringify(args)

if (cache[key] !== undefined) {
console.log('从缓存返回')
return cache[key]
}

const result = fn.apply(this, args)
cache[key] = result
return result
}
}

const expensiveCalc = memoize(function (n) {
// 假设这是个耗时计算
return n * n
})

expensiveCalc(10) // 计算
expensiveCalc(10) // 从缓存返回

cache 通过闭包保存在返回函数的作用域里,不暴露在外面,但每次调用都能访问到。

闭包的内存问题

闭包会让变量无法被垃圾回收,如果处理不当,可能导致内存泄漏。

1
2
3
4
5
6
7
8
9
10
11
function setup() {
const bigData = new Array(1000000).fill('data') // 大数据

return function () {
// 如果这里引用了 bigData,bigData 就不会被回收
return bigData[0]
}
}

const fn = setup()
// fn 一直存在,bigData 就一直存在于内存里

如果返回的函数用完了不再需要,记得解除引用:

1
fn = null  // 允许 bigData 被垃圾回收

实际工作里这种大数组的情况比较少,日常用闭包不需要特别担心,但要有这个意识。


理解闭包之后,防抖节流、模块模式、函数柯里化等等很多 JavaScript 的写法就都说得通了,因为它们本质上都是对闭包的应用。