闭包的理解和应用

前言

闭包是 JavaScript 中最基础也最容易被误用的概念之一。面试必问、实战常用,但很多人只停留在”函数里面返回函数”的表面理解。本文从底层原理到业务场景,帮你彻底搞定闭包。

一、什么是闭包

先看一个最简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
function outer() {
const name = '前端开发'

function inner() {
console.log(name) // inner 能访问 outer 的变量
}

return inner
}

const fn = outer()
fn() // '前端开发' — outer 执行完了,但 name 还在

闭包的定义:
函数 + 它能访问的外部变量(自由变量)的组合,就叫闭包。

更准确地说,闭包是一个函数绑定了它被创建时的词法作用域。

面试一句话:内部函数引用了外部函数的变量,且内部函数被拿到外部执行,形成了闭包。

二、底层原理:作用域链

理解闭包前先搞清楚 JS 的作用域机制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const global = 'global'

function outer() {
const outerVar = 'outer'

function inner() {
const innerVar = 'inner'
console.log(innerVar) // 自己的 AO
console.log(outerVar) // outer 的 AO
console.log(global) // 全局 GO
}

inner()
}

outer()

执行上下文与作用域链

每次函数调用都会创建执行上下文,包含:

1
2
3
4
执行上下文 = {
AO(活动对象): { 当前函数的变量 },
Scope: [AO, 外层函数的AO, ..., GO(全局对象)]
}

Scope 就是作用域链,它在函数定义时就确定了,不是调用时才确定。

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

function foo() {
console.log(x) // 定义时确定:x 指向 GO 中的 x
}

function bar() {
let x = 'bar'
foo() // 打印 'global',不是 'bar'
}

bar()

闭包的本质:引用了外层作用域变量的函数 + 外层被引用的变量无法被 GC 回收。

三、闭包的应用场景

1. 数据私有化(模块模式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function createCounter() {
let count = 0 // 私有变量,外部无法直接访问

return {
increment() { return ++count },
decrement() { return --count },
getCount() { return count }
}
}

const counter = createCounter()
console.log(counter.getCount()) // 0
console.log(counter.increment()) // 1
console.log(counter.count) // undefined — 访问不到

2. 防抖(debounce)

1
2
3
4
5
6
7
8
9
10
11
12
function debounce(fn, delay) {
let timer = null // 闭包保存定时器
return function (...args) {
clearTimeout(timer)
timer = setTimeout(() => fn.apply(this, args), delay)
}
}

// 搜索输入框
input.addEventListener('input', debounce(() => {
console.log('发送请求')
}, 500))

3. 节流(throttle)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function throttle(fn, interval) {
let lastTime = 0 // 闭包保存上次执行时间
return function (...args) {
const now = Date.now()
if (now - lastTime >= interval) {
fn.apply(this, args)
lastTime = now
}
}
}

// 滚动事件
window.addEventListener('scroll', throttle(() => {
console.log('滚动中...')
}, 200))

4. 函数柯里化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args)
}
return (...next) => curried(...args, ...next)
}
}

function add(a, b, c) { return a + b + c }
const curriedAdd = curry(add)

console.log(curriedAdd(1)(2)(3)) // 6
console.log(curriedAdd(1, 2)(3)) // 6
console.log(curriedAdd(1)(2, 3)) // 6

5. 缓存函数(记忆化)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function memoize(fn) {
const cache = {} // 闭包保存缓存结果
return function (arg) {
if (cache[arg] !== undefined) {
console.log('命中缓存')
return cache[arg]
}
const result = fn(arg)
cache[arg] = result
return result
}
}

const memoizedFib = memoize(function fib(n) {
if (n <= 1) return n
return memoizedFib(n - 1) + memoizedFib(n - 2)
})

console.log(memoizedFib(40)) // 大幅加速

6. 循环中的经典问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// ❌ 错误写法 — 打印全是 6
for (var i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 0)
}

// ✅ 方案一:用 let(块级作用域,最推荐)
for (let i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 0) // 0 1 2 3 4
}

// ✅ 方案二:闭包 + IIFE
for (var i = 0; i < 5; i++) {
;((j) => {
setTimeout(() => console.log(j), 0) // 0 1 2 3 4
})(i)
}

// ✅ 方案三:setTimeout 第三个参数
for (var i = 0; i < 5; i++) {
setTimeout(console.log, 0, i) // 0 1 2 3 4
}

本质原因:var 没有块级作用域,所有回调共享同一个 i。闭包为每次迭代创建了独立的变量副本。

四、闭包的代价:内存泄漏

闭包引用外部变量,导致这些变量不会被垃圾回收。

1
2
3
4
5
6
7
8
9
function createHeap() {
const bigData = new Array(1000000).fill('●') // 大内存
return function () {
console.log(bigData.length) // 引用了 bigData,无法 GC
}
}

const fn = createHeap()
// bigData 永远留在内存中,直到 fn = null

避免内存泄漏

1
2
3
4
5
6
7
8
9
10
11
12
function better() {
const bigData = new Array(1000000).fill('●')
const length = bigData.length
return function () {
console.log(length) // 只留一个数字,不占内存
}
}

// 不需要时断开引用
let fn = createHeap()
// ... 使用完后
fn = null // 释放闭包

五、实际开发中使用闭包的场景

场景 示例
表单校验 debounce 防抖输入检查
滚动加载 throttle 节流加载更多
状态管理 useState、ref 等 hooks 本质是闭包
事件管理 addEventListener → removeEventListener 存回调引用
请求去重 缓存 Promise 实例,防止重复请求
中间件模式 Koa/Redux 的洋葱模型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 请求去重 — 防止同一接口并发重复请求
function createRequestCache() {
const cache = new Map()
return (key, fetcher) => {
if (cache.has(key)) return cache.get(key)
const promise = fetcher().finally(() => cache.delete(key))
cache.set(key, promise)
return promise
}
}

const cachedFetch = createRequestCache()
// 同一 key 多次调用,只有第一次会真正发请求
cachedFetch('user', fetchUser)
cachedFetch('user', fetchUser) // 复用上面的 promise

六、Vue3 中闭包的应用

Vue3 内部大量依赖闭包实现响应式和逻辑封装,理解闭包能让你更懂框架源码。

1. ref 的 getter/setter

ref 返回一个带 value 属性的对象,value 的读写依赖闭包保存实际值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Vue3 ref 简化实现
function ref(val) {
const wrapper = {
get value() {
track(wrapper, 'value') // 依赖收集
return val // 闭包返回原始值
},
set value(newVal) {
val = newVal // 闭包更新原始值
trigger(wrapper, 'value') // 触发更新
}
}
return wrapper
}

const count = ref(0)
count.value++ // 闭包中的 val 从 0 变为 1

ref 内部不用 this,纯靠闭包保存状态,这也是为什么 setup 里不用关心 this。

2. Composables(组合式函数)

Composables 本质就是闭包模式的函数封装:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// useMouse.js — 鼠标位置追踪
import { ref, onMounted, onUnmounted } from 'vue'

export function useMouse() {
const x = ref(0)
const y = ref(0)

function update(e) {
x.value = e.pageX
y.value = e.pageY
}

onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))

return { x, y } // 闭包保持 refs 存活
}
1
2
3
4
5
6
7
8
<script setup>
const { x, y } = useMouse()
// x, y 在多次组件渲染中始终是同一个闭包内的 ref
</script>

<template>
<p>鼠标位置:{{ x }}, {{ y }}</p>
</template>

3. watch / computed 中的副作用函数

1
2
3
4
5
6
7
8
9
import { ref, watch } from 'vue'

const keyword = ref('')

// watch 回调是一个闭包,持续引用 keyword
watch(keyword, (newVal, oldVal) => {
console.log(`搜索词从 "${oldVal}" 变为 "${newVal}"`)
fetchResults(newVal) // 闭包保留了 fetchResults 的引用
})

4. defineComponent 中的 setup 闭包

1
2
3
4
5
6
7
8
9
10
11
12
<script setup>
import { ref } from 'vue'

const count = ref(0)
const msg = 'Hello'

// template 编译后,所有变量通过闭包传递给 render 函数
// 组件每次更新,render 函数访问的都是 setup 闭包中的同一个 ref
function increment() {
count.value++ // 闭包修改 setup 作用域中的 count
}
</script>

<script setup> 编译后,整个 setup 函数就是一个大闭包。模板中使用的变量和函数都被这个闭包包裹,这也是 Vue3 不需要 .value 解包(模板自动处理)的原因。

5. 自定义 Hook:防抖搜索

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// useDebounce.js
import { ref, watch } from 'vue'

export function useDebounce(value, delay = 300) {
const debounced = ref(value.value)
let timer = null // 闭包保存定时器

watch(value, (newVal) => {
clearTimeout(timer)
timer = setTimeout(() => {
debounced.value = newVal
}, delay)
})

return debounced
}
1
2
3
4
5
6
7
8
<script setup>
import { ref } from 'vue'
import { useDebounce } from './useDebounce'

const keyword = ref('')
const debouncedKeyword = useDebounce(keyword, 500)
// debouncedKeyword 是防抖后的值,只有用户停止输入 500ms 后才更新
</script>

总结

  • 闭包 = 函数 + 能访问的外部变量,本质是作用域链在函数定义时就确定
  • 函数外部无法访问闭包内的变量,天然实现数据私有化
  • 核心应用:防抖/节流、柯里化、缓存、模块模式、hooks
  • 代价:变量不会被 GC → 用完后记得 = null 释放
  • let 解决循环中闭包问题比 IIFE 更优雅
  • 真正理解闭包 = 理解 JS 执行上下文 + 作用域链 + GC

内容由 AI 生成,仅供参考。本文发布于 码上学习,转载请注明出处。