1. 核心概念
闭包是指一个函数能够记住并访问其词法作用域,即使该函数在其词法作用域之外执行。
简单说:函数 + 它创建时的作用域环境 = 闭包
function outer() {
const name = '张三' // 局部变量
function inner() {
console.log(name) // 内部函数访问外部函数的变量
}
return inner // 返回内部函数
}
const closureFunc = outer() // outer执行完毕,name应该被销毁
closureFunc() // 但这里仍然可以访问name,输出:张三
2. 工作原理
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
const counter2 = createCounter() // 新的闭包,独立的count
console.log(counter2()) // 1
console.log(counter2()) // 2
3. 闭包的典型应用场景
3.1 私有变量封装
function createPerson(name) {
let privateAge = 0 // 私有变量
return {
getName() {
return name
},
getAge() {
return privateAge
},
setAge(newAge) {
if (newAge > 0) privateAge = newAge
},
haveBirthday() {
privateAge++
}
}
}
const person = createPerson('李四')
console.log(person.getName()) // 李四
console.log(person.privateAge) // undefined,无法直接访问
person.haveBirthday()
console.log(person.getAge()) // 1
3.2 模块模式
const calculator = (function() {
let memory = 0 // 私有变量
return {
add(x, y) {
const result = x + y
memory = result
return result
},
multiply(x, y) {
const result = x * y
memory = result
return result
},
getMemory() {
return memory
},
clearMemory() {
memory = 0
}
}
})()
console.log(calculator.add(2, 3)) // 5
console.log(calculator.getMemory()) // 5
console.log(calculator.multiply(4, 5)) // 20
console.log(calculator.getMemory()) // 20
3.3 函数工厂
function createMultiplier(factor) {
return function(x) {
return x * factor
}
}
const double = createMultiplier(2)
const triple = createMultiplier(3)
console.log(double(5)) // 10
console.log(triple(5)) // 15
3.4 事件处理与回调
function setupButtons() {
const buttons = ['按钮1', '按钮2', '按钮3']
for (let i = 0; i < buttons.length; i++) {
const button = document.createElement('button')
button.textContent = buttons[i]
// 使用闭包保存每个按钮的索引
button.addEventListener('click', (function(index) {
return function() {
console.log(`点击了第${index + 1}个按钮:${buttons[index]}`)
}
})(i))
document.body.appendChild(button)
}
}
// 不使用闭包会有问题的版本:
function setupButtonsWrong() {
for (var i = 0; i < 3; i++) { // 注意:使用var
const button = document.createElement('button')
button.textContent = `按钮${i + 1}`
button.addEventListener('click', function() {
console.log(`点击了第${i + 1}个按钮`) // 所有按钮都会输出"第4个按钮"
})
}
}
4. 闭包的优点
4.1 数据封装与私有化
const bankAccount = (function() {
let balance = 1000 // 私有变量
return {
deposit(amount) {
if (amount > 0) {
balance += amount
return true
}
return false
},
withdraw(amount) {
if (amount > 0 && amount <= balance) {
balance -= amount
return amount
}
return 0
},
getBalance() {
return balance
}
}
})()
// 外部无法直接访问balance
console.log(bankAccount.balance) // undefined
console.log(bankAccount.getBalance()) // 1000
bankAccount.deposit(500)
console.log(bankAccount.getBalance()) // 1500
4.2 状态保持
function createLogger(prefix) {
let callCount = 0
return function(message) {
callCount++
console.log(`[${prefix}] 调用次数:${callCount},消息:${message}`)
}
}
const log1 = createLogger('系统')
log1('启动成功') // [系统] 调用次数:1,消息:启动成功
log1('运行正常') // [系统] 调用次数:2,消息:运行正常
const log2 = createLogger('用户')
log2('登录成功') // [用户] 调用次数:1,消息:登录成功
5. 闭包的缺点与注意事项
5.1 内存泄漏风险
function createLeakyClosure() {
const largeArray = new Array(1000000).fill('data') // 大数组
return function() {
// 即使外部不需要largeArray,闭包仍然保持引用
console.log('闭包执行')
}
}
const leakyFunc = createLeakyClosure()
// largeArray不会被垃圾回收,因为闭包保持着引用
// 解决方法:不再使用时解除引用
leakyFunc = null
5.2 性能考虑
// 避免在循环中创建不必要的闭包
function processItems(items) {
// 不好的做法:循环内创建闭包
for (let i = 0; i < items.length; i++) {
setTimeout(function() {
console.log(items[i])
}, 1000)
}
// 改进做法:使用IIFE或箭头函数
for (let i = 0; i < items.length; i++) {
(function(index) {
setTimeout(function() {
console.log(items[index])
}, 1000)
})(i)
}
// 更好的做法:使用let的块级作用域
for (let i = 0; i < items.length; i++) {
setTimeout(function() {
console.log(items[i])
}, 1000)
}
}
6. 闭包面试题解析
6.1 经典循环问题
for (var i = 1; i <= 5; i++) {
setTimeout(function() {
console.log(i) // 输出6个6
}, i * 1000)
}
// 解决方案1:使用IIFE创建闭包
for (var i = 1; i <= 5; i++) {
(function(j) {
setTimeout(function() {
console.log(j) // 输出1,2,3,4,5
}, j * 1000)
})(i)
}
// 解决方案2:使用let的块级作用域
for (let i = 1; i <= 5; i++) {
setTimeout(function() {
console.log(i) // 输出1,2,3,4,5
}, i * 1000)
}
// 解决方案3:setTimeout的第三个参数
for (var i = 1; i <= 5; i++) {
setTimeout(function(j) {
console.log(j) // 输出1,2,3,4,5
}, i * 1000, i)
}
6.2 多个闭包共享变量
function createClosures() {
let shared = 0
return [
function() {
shared++
return shared
},
function() {
shared *= 2
return shared
},
function() {
return shared
}
]
}
const [inc, double, get] = createClosures()
console.log(inc()) // 1
console.log(double()) // 2
console.log(get()) // 2
console.log(inc()) // 3
7. 闭包的内存管理
// 正确管理闭包内存
function createDataProcessor() {
const data = new Array(10000).fill('important data')
const processor = {
process() {
// 处理数据
console.log('Processing data...')
},
getData() {
return data[0]
},
cleanup() {
// 清理函数,断开对大数据的引用
data.length = 0
processor.process = null
processor.getData = null
}
}
return processor
}
const processor = createDataProcessor()
processor.process()
// 不再使用时调用cleanup
processor.cleanup()
processor = null // 帮助垃圾回收
8. 闭包与作用域链
function outer() {
const a = 1
function middle() {
const b = 2
function inner() {
const c = 3
console.log(a, b, c) // 可以访问所有外部变量
}
return inner
}
return middle
}
const middleFunc = outer()
const innerFunc = middleFunc()
innerFunc() // 输出:1 2 3
总结
闭包的本质:函数在执行时,会创建一个包含其变量对象的作用域链。闭包使得函数可以访问这个作用域链上的所有变量,即使外部函数已经执行完毕。
关键要点:
- 闭包是函数和其词法环境的组合
- 内部函数可以访问外部函数的作用域
- 闭包在创建时"记住"了当时的环境
- 每个闭包都是独立的,有自己的状态
- 合理使用闭包,注意内存管理
闭包是JavaScript的核心概念之一,理解闭包对于掌握JavaScript的高级特性至关重要。