深入理解 JavaScript 闭包:从原理到实战
闭包是 JavaScript 中最核心也最容易让人困惑的概念之一。本文通过通俗的语言和实际例子,帮你彻底搞懂它。
什么是闭包?
闭包(Closure) 是指一个函数能够访问其外部作用域中变量的能力------即使这个外部函数已经执行完毕。
简单来说:函数 + 它所记住的外部变量 = 闭包。
一个最简单的例子
javascript
function makeCounter() {
let count = 0;
return function() {
count++;
return count;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
makeCounter 执行完后,count 变量并没有消失。内部的匿名函数"记住"了它所在的作用域,每次调用都能访问并修改 count,这就是闭包。
闭包的核心原理:作用域链
JavaScript 采用词法作用域(Lexical Scoping),函数的作用域在定义时就确定了,而不是调用时。每个函数在创建时都会保存一个对其外部作用域的引用,这个引用就是闭包的本质。
常见使用场景
1. 数据私有化(模拟私有变量)
javascript
function createAccount(initialBalance) {
let balance = initialBalance; // 外部无法直接访问
return {
deposit(amount) {
balance += amount;
},
withdraw(amount) {
if (amount <= balance) balance -= amount;
else console.log('余额不足');
},
getBalance() {
return balance;
}
};
}
const account = createAccount(1000);
account.deposit(500);
console.log(account.getBalance()); // 1500
console.log(account.balance); // undefined,无法直接访问
2. 函数工厂(批量创建相似函数)
javascript
function multiplier(factor) {
return (number) => number * factor;
}
const double = multiplier(2);
const triple = multiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
3. 防抖与节流
javascript
// 防抖:用户停止输入 300ms 后再执行
function debounce(fn, delay) {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
const handleInput = debounce((e) => {
console.log('搜索:', e.target.value);
}, 300);
document.querySelector('#search').addEventListener('input', handleInput);
4. 模块模式(封装功能)
javascript
const TodoList = (() => {
const todos = [];
return {
add(item) { todos.push(item); },
remove(item) {
const index = todos.indexOf(item);
if (index > -1) todos.splice(index, 1);
},
getAll() { return [...todos]; }
};
})();
TodoList.add('学习闭包');
TodoList.add('写一篇博客');
console.log(TodoList.getAll()); // ['学习闭包', '写一篇博客']
经典陷阱:循环中的闭包
错误写法(for + var)
javascript
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 输出:3 3 3(不是期望的 0 1 2)
原因:var 声明的变量共享同一个 i,循环结束后 i 为 3。
正确写法 1:使用 let(推荐)
javascript
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 输出:0 1 2
正确写法 2:使用立即执行函数(IIFE)
javascript
for (var i = 0; i < 3; i++) {
((j) => {
setTimeout(() => console.log(j), 100);
})(i);
}
// 输出:0 1 2
注意:闭包与内存
闭包会持有外部作用域变量的引用,只要闭包存在,这些变量就不会被垃圾回收。如果大量创建闭包且不及时释放,可能导致内存泄漏。
合理使用,用完后将引用置为 null 即可:
javascript
let fn = makeCounter();
// 使用...
fn = null; // 释放引用,GC 可以回收了
总结
闭包是 JavaScript 中"函数是一等公民"理念的完美体现。理解了闭包,你就掌握了 JS 中许多高级模式的底层原理------从 React Hooks 到 Promises,背后都有闭包的影子。
你有什么关于闭包的疑问或有趣案例?欢迎在评论区交流!