闭包
2025年6月25日大约 5 分钟
闭包
JS 中的作用域
在 JavaScript 中 作用域(Scope)是指变量、函数和对象在代码中的可访问范围。
作用域主要解决了以下几点问题:
- 变量冲突和污染
var name = 'Alice'
function greet() {
var name = 'Bob' // 与全局变量同名,但不冲突
console.log(name) // Bob
}
// 作用域让变量在“自己的范围内活动”,避免互相干扰。
- 数据隔离和封装
function secretBox() {
let secret = '1234'
return {
check: (pwd) => pwd === secret,
}
}
const box = secretBox()
console.log(box.secret) // undefined
- 内存释放(生命周期管理)
function temp() {
let tempData = new Array(10000).fill(0)
}
// 在函数调用结束后,局部变量所在作用域被销毁,内存可被释放。
// tempData 会被 GC 回收(如果没有闭包引用)
- 作用域链(Scope Chain)查找变量
// 当你访问一个变量时,JS 引擎会沿着作用域链从当前作用域向上查找:
let a = 1
function outer() {
let b = 2
function inner() {
let c = 3
console.log(a, b, c) // 都能访问
}
inner()
}
作用域主要有以下几种:
- 全局作用域
全局变量,整个文件中都可以被访问,容易受到污染 - 函数作用域
函数中有效 - 块级作用域
{} 内部有效,更安全、推荐 - 词法作用域
在 ES6 之前,函数作用域和全局作用域。用 var 声明的变量要么属于函数作用域要么属于全局作用域,这取决于变量是在函数内声明的还是在函数外声明的。花括号块不为 var 创建作用域
在 ES6 之后,JavaScript 引入了 let 和 const 声明,会创建块级作用域的变量。
闭包的概念
在 MDN 上,对于闭包的描述是
闭包是由捆绑起来(封闭的)的函数和函数周围状态(词法环境)的引用组合而成。换言之,闭包让函数能访问它的外部作用域。在 JavaScript 中,闭包会随着函数的创建而同时创建。
function init() {
const name = 'Test' // name 是 init 创建的局部变量
function displayName() {
console.log(name) // 使用在父函数中声明的变量
}
displayName()
}
init()
// 这是一个词法作用域的示例,。
// 词法一词是指词法作用域使用源代码内变量声明的位置决定变量可用的位置。嵌套函数能访问在其外部作用域中声明的变量。
在一些编程语言中,函数内的局部变量只会存在于函数执行时(c/c++),
在 js 中,JavaScript 中的函数创建了闭包。 闭包是由函数以及函数声明所在的词法环境组合而成的。该环境包含了这个闭包创建时作用域内的任何局部变量。
function makeFunc() {
const name = 'Mozilla'
function displayName() {
console.log(name)
}
return displayName
}
const myFunc = makeFunc()
myFunc()
//可以正常执行
闭包的使用场景
闭包(Closure)是 JavaScript 中最强大、最常用的特性之一。在实际开发过程中,闭包可以用来用来封装私有变量,还可以用于实现缓存、柯里化、模块模式、异步控制、事件监听、函数节流/防抖、惰性函数定义实现一些高阶的函数。
- 私有变量封装
// 实现“class”的效果,私有化变量
function createCounter() {
let count = 0
return {
increment: () => ++count,
decrement: () => --count,
get: () => count,
}
}
const counter = createCounter()
counter.increment() // 1
counter.get() // 1
// 在ES2022+,新增里真正的私有字段 #filed,在运行时实现真正的私有化
缓存优化
记忆化函数,提高性能
function memoize(fn: Fn): Fn {
const cache = new Map()
return function (...args) {
const key = JSON.stringify(args)
if (cache.has(key)) {
return cache.get(key)
}
const result = fn(...args)
cache.set(key, result)
return result
}
}
type Fn = (...args: any[]) => any
// 将执行函数通过记忆函数包装,缓存入参,在命中缓存时直接返回结果,避免重复执行函数。
// 这种方法在函数计算量巨大的场景下,能够显著提高性能
柯里化函数
参数分步传递,提升灵活性
/**
* 将一个多参数函数转换为一系列只接收一个参数的函数,
* 每次调用返回一个新的函数,直到接收所有参数为止,最后执行原始函数逻辑。
*/
function log(module: string) {
return function (level: string) {
return function (message: string) {
console.log(`[${module}] [${level}] ${message}`)
}
}
}
const curriedLog = curry(log)
// 利用柯里化进行参数预设
const infoLog = curriedLog('App')('INFO')
infoLog('A')
infoLog('B')
节流 / 防抖函数(Throttle / Debounce)
闭包维护 timer 状态,实现控制函数调用频率:
/**
* 防抖
*/
function debounce(fn, delay) {
let timer = null
return function (...args) {
clearTimeout(timer)
timer = setTimeout(() => fn.apply(this, args), delay)
}
}
/**
* 节流
*/
function throttle(fn, delay) {
let last = 0
return function (...args) {
const now = Date.now()
if (now - last >= delay) {
last = now
fn.apply(this, args)
}
}
}
函数工厂(Function Factory)
生成行为相似但独立的函数:
function createMultiplier(factor) {
return function (num) {
return num * factor
}
}
const double = createMultiplier(2)
const triple = createMultiplier(3)
console.log(double(5)) // 10
console.log(triple(5)) // 15
- 惰性函数
function foo() {
if (/* 条件 */) {
// 第一次执行的逻辑
foo = function() {
// 新的函数体
}
} else {
// 第一次执行的逻辑
foo = function() {
// 新的函数体
}
}
return foo(); // 递归调用新的函数体
}
- 异步闭包(循环变量绑定)
for (var i = 0; i < 3; i++) {
;(function (j) {
setTimeout(() => console.log(j), j * 1000)
})(i)
}
/**
* 在es6中,这种使用let解决赋值问题
* let 是 块级作用域,
* 每次循环都会创建一个新的 i 实例(闭包捕获的是当前块作用域中的 i),
* 所以不会有经典的 “循环结束才统一输出 3” 的问题
*/
- 模块模式
const UserModule = (function () {
let username = 'admin'
function getName() {
return username
}
function setName(name) {
username = name
}
return {
getName,
setName,
}
})()
UserModule.setName('Alice')
console.log(UserModule.getName()) // Alice