最后更新: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) }
inner() }
outer()
|
inner 在定义时所在的环境是 outer 函数内部,所以它访问的 name 是 outer 里的那个,跟外面全局的 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() add() add()
|
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') sayHi('Tom')
|
sayHello 和 sayHi 各自记住了自己的 greeting,互不干扰。每次调用 createGreeter 都创建了一个新的作用域,返回的函数绑定的是那次调用时的 greeting。
经典问题:循环里的闭包
这是面试里最常见的闭包题,很多人栽在这里:
1 2 3 4 5 6 7
| for (var i = 0; i < 3; i++) { setTimeout(function () { console.log(i) }, 1000) }
|
为什么?因为 var 没有块级作用域,三次循环共用同一个 i。定时器回调执行时,循环早结束了,i 已经是 3。
解法一:改用 let(最推荐)
1 2 3 4 5 6
| for (let i = 0; i < 3; i++) { setTimeout(function () { console.log(i) }, 1000) }
|
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) }
|
每次循环执行 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())
console.log(account.balance)
|
这是 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)) console.log(triple(5))
|
缓存(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 () { return bigData[0] } }
const fn = setup()
|
如果返回的函数用完了不再需要,记得解除引用:
实际工作里这种大数组的情况比较少,日常用闭包不需要特别担心,但要有这个意识。
理解闭包之后,防抖节流、模块模式、函数柯里化等等很多 JavaScript 的写法就都说得通了,因为它们本质上都是对闭包的应用。