闭包是 JavaScript 最强大的特性之一。函数可以”记住”并访问其定义时的词法作用域,即使在该作用域之外执行。
🎯 什么是闭包#
闭包 = 函数 + 函数定义时的词法环境
function outer() { const message = '你好'
function inner() { console.log(message) // 访问外层变量 }
return inner}
const fn = outer()fn() // "你好"
// inner 函数被返回后,message 变量仍然可以访问// 这就是闭包理解闭包#
function createCounter() { let count = 0 // 这个变量被闭包"捕获"
return { increment() { count++ return count }, decrement() { count-- return count }, getCount() { return count }, }}
const counter = createCounter()console.log(counter.increment()) // 1console.log(counter.increment()) // 2console.log(counter.decrement()) // 1console.log(counter.getCount()) // 1
// count 变量对外部不可见,但函数可以访问// console.log(count) // ReferenceError每次调用创建新的闭包#
function createGreeter(name) { return function () { console.log(`你好,${name}`) }}
const greetZhang = createGreeter('张三')const greetLi = createGreeter('李四')
greetZhang() // "你好,张三"greetLi() // "你好,李四"
// 两个函数各自有独立的闭包,保存不同的 name闭包的应用场景#
1. 数据封装#
function createBankAccount(initialBalance) { let balance = initialBalance // 私有变量
return { deposit(amount) { if (amount <= 0) throw new Error('金额必须大于0') balance += amount return balance },
withdraw(amount) { if (amount <= 0) throw new Error('金额必须大于0') if (amount > balance) throw new Error('余额不足') balance -= amount return balance },
getBalance() { return balance }, }}
const account = createBankAccount(1000)account.deposit(500) // 1500account.withdraw(200) // 1300console.log(account.getBalance()) // 1300// account.balance // undefined(无法直接访问)2. 函数工厂#
// 创建乘法函数function createMultiplier(factor) { return function (number) { return number * factor }}
const double = createMultiplier(2)const triple = createMultiplier(3)const tenTimes = createMultiplier(10)
console.log(double(5)) // 10console.log(triple(5)) // 15console.log(tenTimes(5)) // 50
// 创建验证函数function createValidator(min, max) { return function (value) { return value >= min && value <= max }}
const isValidAge = createValidator(0, 150)const isValidScore = createValidator(0, 100)
console.log(isValidAge(25)) // trueconsole.log(isValidScore(101)) // false3. 模块模式#
const Calculator = (function () { // 私有变量 let result = 0
// 私有方法 function validate(n) { if (typeof n !== 'number') throw new Error('必须是数字') }
// 公开接口 return { add(n) { validate(n) result += n return this },
subtract(n) { validate(n) result -= n return this },
multiply(n) { validate(n) result *= n return this },
getResult() { return result },
reset() { result = 0 return this }, }})()
Calculator.add(10).subtract(3).multiply(2)console.log(Calculator.getResult()) // 144. 记忆化(缓存)#
function memoize(fn) { const cache = {} // 闭包保存缓存
return function (...args) { const key = JSON.stringify(args) if (key in cache) { console.log('从缓存获取') return cache[key] } console.log('计算中...') const result = fn.apply(this, args) cache[key] = result return result }}
// 计算斐波那契const fibonacci = memoize(function (n) { if (n <= 1) return n return fibonacci(n - 1) + fibonacci(n - 2)})
console.log(fibonacci(40)) // 快速计算console.log(fibonacci(40)) // 从缓存获取5. 柯里化#
function curry(fn) { return function curried(...args) { if (args.length >= fn.length) { return fn.apply(this, args) } return function (...moreArgs) { return curried.apply(this, [...args, ...moreArgs]) } }}
function add(a, b, c) { return a + b + c}
const curriedAdd = curry(add)
console.log(curriedAdd(1)(2)(3)) // 6console.log(curriedAdd(1, 2)(3)) // 6console.log(curriedAdd(1)(2, 3)) // 6console.log(curriedAdd(1, 2, 3)) // 6
// 实际应用const log = curry((level, message, data) => { console.log(`[${level}] ${message}`, data)})
const logError = log('ERROR')const logInfo = log('INFO')
logError('请求失败', { code: 500 })logInfo('用户登录', { user: '张三' })6. 防抖与节流#
// 防抖:延迟执行,重复调用会重置计时器function debounce(fn, delay) { let timer = null
return function (...args) { clearTimeout(timer) timer = setTimeout(() => { fn.apply(this, args) }, delay) }}
// 节流:固定频率执行function throttle(fn, interval) { let lastTime = 0
return function (...args) { const now = Date.now() if (now - lastTime >= interval) { lastTime = now fn.apply(this, args) } }}
// 使用const debouncedSearch = debounce((query) => { console.log('搜索:', query)}, 300)
const throttledScroll = throttle(() => { console.log('滚动位置:', window.scrollY)}, 100)7. once 函数#
function once(fn) { let called = false let result
return function (...args) { if (called) return result called = true result = fn.apply(this, args) return result }}
const initialize = once(() => { console.log('初始化完成') return { initialized: true }})
initialize() // 打印"初始化完成",返回对象initialize() // 直接返回之前的结果,不再执行initialize() // 同上循环中的闭包#
经典问题#
// 问题代码for (var i = 0; i < 3; i++) { setTimeout(function () { console.log(i) }, 100)}// 输出:3, 3, 3
// 为什么?// 1. var 没有块级作用域,i 是同一个变量// 2. setTimeout 回调在循环结束后执行// 3. 此时 i 已经是 3解决方案#
// 方案1:使用 let(推荐)for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100)}// 输出:0, 1, 2
// 方案2:IIFE 创建闭包for (var i = 0; i < 3; i++) { ;(function (j) { setTimeout(() => console.log(j), 100) })(i)}
// 方案3:创建函数返回函数for (var i = 0; i < 3; i++) { setTimeout( (function (j) { return function () { console.log(j) } })(i), 100 )}
// 方案4:bind 预设参数for (var i = 0; i < 3; i++) { setTimeout( function (j) { console.log(j) }.bind(null, i), 100 )}闭包与内存#
内存泄漏风险#
// 🔶 闭包会保持对外部变量的引用function createHeavyClosure() { const largeData = new Array(1000000).fill('x') // 大数组
return function () { // 即使只使用 length,整个 largeData 都会被保留 console.log(largeData.length) }}
const fn = createHeavyClosure()// largeData 无法被垃圾回收
// ✅ 只保存需要的数据function createLightClosure() { const largeData = new Array(1000000).fill('x') const length = largeData.length // 只保存需要的
return function () { console.log(length) }}DOM 引用#
// 🔶 闭包引用 DOM 可能导致内存泄漏function attachHandler() { const button = document.getElementById('myButton')
button.addEventListener('click', function () { // 这个闭包引用了 button console.log(button.id) })
// 即使 button 从 DOM 移除,闭包仍持有引用}
// ✅ 使用事件委托��在移除时解绑function attachHandlerSafe() { const button = document.getElementById('myButton')
function handleClick() { console.log(this.id) }
button.addEventListener('click', handleClick)
// 提供清理方法 return function cleanup() { button.removeEventListener('click', handleClick) }}解除闭包#
function createClosure() { let data = { heavy: new Array(10000) }
return { getData() { return data }, // 提供释放方法 dispose() { data = null }, }}
const obj = createClosure()console.log(obj.getData()) // 使用数据obj.dispose() // 释放引用闭包的本质#
作用域链#
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 middle = outer()const inner = middle()inner() // 1, 2, 3
// inner 函数保存了完整的作用域链:// inner [[Scope]] -> middle 变量环境 -> outer 变量环境 -> 全局变量生命周期#
// 普通情况:函数执行完,局部变量销毁function normal() { const x = 1 console.log(x)} // x 被销毁
// 闭包情况:被引用的变量不会销毁function closure() { const x = 1 return function () { console.log(x) }}
const fn = closure()// x 不会被销毁,因为 fn 引用了它fn() // 1常见面试题#
🙋 实现 add(1)(2)(3)#
function add(a) { return function (b) { return function (c) { return a + b + c } }}
console.log(add(1)(2)(3)) // 6
// 通用版本function sum(a) { return function (b) { if (b === undefined) return a return sum(a + b) }}
sum(1)(2)(3)() // 6sum(1)(2)(3)(4)() // 10🙋 实现私有变量#
function Person(name) { // 私有变量 let _age = 0
this.name = name
this.getAge = function () { return _age }
this.setAge = function (age) { if (age < 0 || age > 150) { throw new Error('无效年龄') } _age = age }}
const p = new Person('张三')p.setAge(25)console.log(p.getAge()) // 25// console.log(p._age) // undefined🙋 解释输出#
function fun(n, o) { console.log(o) return { fun: function (m) { return fun(m, n) }, }}
var a = fun(0) // undefineda.fun(1) // 0a.fun(2) // 0a.fun(3) // 0
var b = fun(0).fun(1).fun(2).fun(3)// undefined, 0, 1, 2
var c = fun(0).fun(1)c.fun(2) // 1c.fun(3) // 1总结#
| 应用场景 | 说明 |
|---|---|
| 数据封装 | 创建私有变量和方法 |
| 函数工厂 | 生成预配置的函数 |
| 模块模式 | 实现模��化,暴露公开接口 |
| 记忆化 | 缓存计算结果 |
| 柯里化 | 参数复用和延迟执行 |
| 防抖节流 | 控制函数执行频率 |
核心要点:
- 闭包 = 函数 + 函数定义时的词法环境
- 闭包让函数可以访问定义时的作用域
- 注意闭包可能导致的内存泄漏
- 循环中使用
let避免闭包陷阱 - 闭包是实现数据封装的重要手段