007-闭包的概念和使用场景

闭包:概念解析与应用场景全解

闭包核心概念

定义

闭包(Closure) 是指能够访问其外部函数作用域中变量的函数,即使外部函数已经执行结束。闭包由两部分组成:

  1. 函数
  2. 创建该函数时所在的作用域环境

关键特性

特性 说明
作用域保留 闭包会保留其外部函数的作用域链,即使外部函数已执行完毕
变量持久化 外部函数的变量不会被垃圾回收,而是被闭包长期引用
状态封装 通过闭包可以创建私有变量,实现数据封装

闭包经典应用场景

场景1:模块化开发(私有变量封装)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 计数器模块
const counter = (function() {
// 私有变量
let count = 0;

// 公共接口
return {
increment() {
return ++count;
},
decrement() {
return --count;
},
getValue() {
return count;
}
};
})();

console.log(counter.getValue()); // 0
counter.increment();
counter.increment();
console.log(counter.getValue()); // 2
counter.decrement();
console.log(counter.getValue()); // 1

场景2:函数工厂(动态函数创建)

1
2
3
4
5
6
7
8
9
10
11
12
13
// 创建幂函数工厂
function powerFactory(exponent) {
return function(base) {
return Math.pow(base, exponent);
};
}

// 创建特定幂函数
const square = powerFactory(2);
const cube = powerFactory(3);

console.log(square(4)); // 16 (4^2)
console.log(cube(3)); // 27 (3^3)

场景3:事件处理(保留上下文)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 按钮列表事件处理
function setupButtons() {
const buttons = document.querySelectorAll('.btn');

for (var i = 0; i < buttons.length; i++) {
// 使用闭包保留每个按钮的索引
(function(index) {
buttons[index].addEventListener('click', function() {
console.log(`按钮 ${index + 1} 被点击`);
});
})(i);
}
}

// 替代方案(使用let块级作用域)
function setupButtonsModern() {
const buttons = document.querySelectorAll('.btn');

for (let i = 0; i < buttons.length; i++) {
buttons[i].addEventListener('click', function() {
console.log(`按钮 ${i + 1} 被点击`);
});
}
}

场景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);
} else {
return (...nextArgs) => curried.apply(this, [...args, ...nextArgs]);
}
};
}

// 使用柯里化
const add = curry((a, b, c) => a + b + c);
const addFive = add(2)(3);
console.log(addFive(5)); // 10 (2+3+5)

场景5:状态管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 状态管理器
function createState(initialValue) {
let state = initialValue;

return {
get() {
return state;
},
set(newValue) {
state = newValue;
},
// 高级功能:状态变更订阅
subscribe(callback) {
const listener = () => callback(state);
// 模拟订阅机制
document.addEventListener('stateChange', listener);

// 返回取消订阅函数
return () => {
document.removeEventListener('stateChange', listener);
};
}
};
}

// 使用状态管理器
const userState = createState({ name: 'Alice', age: 30 });
console.log(userState.get()); // {name: 'Alice', age: 30}
userState.set({ name: 'Bob', age: 25 });
console.log(userState.get()); // {name: 'Bob', age: 25}

场景6:性能优化(记忆化)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 记忆化函数
function memoize(fn) {
const cache = new Map();

return function(...args) {
// 创建缓存键
const key = JSON.stringify(args);

// 检查缓存
if (cache.has(key)) {
return cache.get(key);
}

// 计算并缓存结果
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}

// 使用记忆化
const fibonacci = memoize(function(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
});

console.log(fibonacci(10)); // 55(首次计算)
console.log(fibonacci(10)); // 55(从缓存读取)

React/Vue 中的闭包应用

React Hooks 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Counter() {
// useState 内部使用闭包保存状态
const [count, setCount] = useState(0);

// useEffect 使用闭包保留回调函数
useEffect(() => {
const timer = setInterval(() => {
// 闭包保留最新count值
setCount(c => c + 1);
}, 1000);

return () => clearInterval(timer);
}, []); // 依赖数组为空,只运行一次

return <div>计数: {count}</div>;
}

Vue Composition API 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';

// ref 使用闭包保存响应式状态
const count = ref(0);

// 定时器使用闭包访问最新值
onMounted(() => {
const timer = setInterval(() => {
count.value++;
}, 1000);

// 清理函数使用闭包
onUnmounted(() => {
clearInterval(timer);
});
});
</script>

<template>
<div>计数: {{ count }}</div>
</template>

闭包常见问题与解决方案

问题1:循环中的闭包陷阱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 错误示例
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i); // 全部输出5
}, 100);
}

// 解决方案1:IIFE创建闭包
for (var i = 0; i < 5; i++) {
(function(j) {
setTimeout(function() {
console.log(j); // 0,1,2,3,4
}, 100);
})(i);
}

// 解决方案2:使用let块级作用域
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i); // 0,1,2,3,4
}, 100);
}

问题2:内存泄漏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 大型对象保留在闭包中
function createHeavyObjectHolder() {
const largeObject = new Array(1000000).fill('data');

return {
getData() {
return largeObject[0];
}
};
}

// 解决方案:不再需要时解除引用
function createSafeHolder() {
let largeObject = new Array(1000000).fill('data');

const api = {
getData() {
return largeObject[0];
}
};

// 提供清理方法
api.cleanup = () => {
largeObject = null;
};

return api;
}

// 使用
const holder = createSafeHolder();
// 使用完成后清理
holder.cleanup();

闭包性能优化技巧

  1. 避免不必要的闭包:只在需要封装状态时使用
  2. 最小化闭包范围:减少闭包中引用的变量数量
  3. 及时解除引用:不再需要的闭包手动设置为null
  4. 使用模块模式:替代多个小型闭包
  5. 避免循环中创建闭包:改用块级作用域变量

闭包与垃圾回收

1
2
3
4
5
6
graph LR
A[函数执行完毕] --> B{内部变量是否被引用}
B -->|被闭包引用| C[保留在内存中]
B -->|未被引用| D[标记为可回收]
C --> E[闭包不再被引用]
E --> D

经典面试题解析

题目1:闭包输出问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function createFunctions() {
var result = [];

for (var i = 0; i < 3; i++) {
result[i] = function() {
return i;
};
}

return result;
}

var funcs = createFunctions();
console.log(funcs[0]()); // 3
console.log(funcs[1]()); // 3
console.log(funcs[2]()); // 3

解析:所有闭包共享同一个变量i,循环结束时i=3

题目2:闭包计数器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function createCounter() {
let count = 0;
return {
increment: () => count++,
get: () => count,
reset: () => { count = 0; }
};
}

const c = createCounter();
c.increment();
c.increment();
console.log(c.get()); // 2
c.reset();
console.log(c.get()); // 0

解析:通过闭包实现私有变量count的封装

总结

闭包核心价值

应用方向 价值体现
数据封装 创建私有变量,实现信息隐藏
状态保持 长期保存数据,不受外部函数执行结束影响
函数工厂 动态生成特定配置的函数
模块化 构建独立作用域,避免全局污染
高阶函数 实现函数组合、柯里化等高级功能
异步编程 在回调中保持上下文一致性

使用原则

  1. 必要性原则:只在需要封装状态时使用闭包
  2. 最小化原则:保持闭包作用域尽可能小
  3. 可控性原则:提供清理机制避免内存泄漏
  4. 可维护性原则:避免过度嵌套的闭包结构

闭包是JavaScript最强大的特性之一,合理使用闭包可以:

  • ✅ 创建优雅的模块化代码
  • ✅ 实现高效的状态管理
  • ✅ 封装复杂的业务逻辑
  • ✅ 构建可复用的函数工厂

但同时要注意:

  • ❌ 避免不必要的内存占用
  • ❌ 防止意外的变量共享
  • ❌ 警惕循环引用导致的内存泄漏