前言
闭包是 JavaScript 中最基础也最容易被误用的概念之一。面试必问、实战常用,但很多人只停留在”函数里面返回函数”的表面理解。本文从底层原理到业务场景,帮你彻底搞定闭包。
一、什么是闭包
先看一个最简单的例子:
1 2 3 4 5 6 7 8 9 10 11 12
| function outer() { const name = '前端开发'
function inner() { console.log(name) }
return inner }
const fn = outer() fn()
|
闭包的定义:
函数 + 它能访问的外部变量(自由变量)的组合,就叫闭包。
更准确地说,闭包是一个函数绑定了它被创建时的词法作用域。
面试一句话:内部函数引用了外部函数的变量,且内部函数被拿到外部执行,形成了闭包。
二、底层原理:作用域链
理解闭包前先搞清楚 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) console.log(outerVar) console.log(global) }
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) }
function bar() { let x = 'bar' foo() }
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()) console.log(counter.increment()) console.log(counter.count)
|
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)) console.log(curriedAdd(1, 2)(3)) console.log(curriedAdd(1)(2, 3))
|
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
| for (var i = 0; i < 5; i++) { setTimeout(() => console.log(i), 0) }
for (let i = 0; i < 5; i++) { setTimeout(() => console.log(i), 0) }
for (var i = 0; i < 5; i++) { ;((j) => { setTimeout(() => console.log(j), 0) })(i) }
for (var i = 0; i < 5; i++) { setTimeout(console.log, 0, i) }
|
本质原因: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) } }
const fn = createHeap()
|
避免内存泄漏
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()
cachedFetch('user', fetchUser) cachedFetch('user', fetchUser)
|
六、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
| 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++
|
ref 内部不用 this,纯靠闭包保存状态,这也是为什么 setup 里不用关心 this。
2. Composables(组合式函数)
Composables 本质就是闭包模式的函数封装:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 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 } }
|
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, (newVal, oldVal) => { console.log(`搜索词从 "${oldVal}" 变为 "${newVal}"`) fetchResults(newVal) })
|
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
| 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 生成,仅供参考。本文发布于 码上学习,转载请注明出处。