大家好,我是你们的老朋友技术博主FogLetter。今天我们来聊聊 JavaScript 中那个让人又爱又恨的特性------闭包。相信很多小伙伴在面试中被问过,在工作中用过,但可能还没有真正"吃透"它。
一、什么是闭包?一个背包的比喻
一句话概括:闭包就是能够访问自由变量的函数。
什么叫自由变量?不是免费的变量,而是在函数外部定义,但在函数内部被引用的变量。
让我用一个生动的比喻来解释:想象你有一个背包(闭包),你要去远方旅行(函数执行完毕)。你在家里(外部函数作用域)放了一些零食和物品(变量),然后你把它们装进背包里。即使你离开了家,这些零食依然在你的背包里,你随时可以拿出来享用。
专业定义:《你不知道的 JavaScript》中说到,闭包 = 函数 + 词法作用域。这才是本质!
javascript
function createCounter() {
let count = 0; // 这个变量就是我们的"零食"
return function() {
count++; // 即使createCounter执行完毕,我依然能访问count
return count;
};
}
const counter = createCounter(); // 旅行开始,把零食装进背包
console.log(counter()); // 1 → 从背包里拿出零食吃
console.log(counter()); // 2 → 再吃一个
二、闭包的形成条件:不是所有函数都能成为"背包客"
要形成闭包,需要满足两个条件:
- 函数嵌套函数:内部函数需要被外部函数包裹
- 内部函数暴露到外界:通过返回函数、挂载到全局对象等方式
javascript
// 条件1:函数嵌套
function outer() {
const secret = "我是秘密";
// 条件2:内部函数暴露
return function inner() {
return secret;
};
}
const getSecret = outer();
console.log(getSecret()); // "我是秘密" ← 成功访问!
立即执行函数(IIFE) 是创建闭包的常见方式:
javascript
const uniqueId = (function() {
let id = 0;
return function() {
return id++;
};
})();
console.log(uniqueId()); // 0
console.log(uniqueId()); // 1
三、闭包的底层原理:作用域链的魔法
词法作用域:你的代码会"记住"它出生时的环境
JavaScript 采用的是词法作用域,也就是说,函数在定义的时候就已经确定了它能访问哪些变量,而不是在执行的时候。
javascript
const globalVar = "全局";
function outer() {
const outerVar = "外部";
function inner() {
const innerVar = "内部";
console.log(innerVar); // 自己的变量
console.log(outerVar); // 外部函数的变量 ← 闭包!
console.log(globalVar); // 全局变量
}
return inner;
}
作用域链:向上查找的"电梯"
当 inner
函数需要访问 outerVar
时,JS 引擎会沿着作用域链这个"电梯"向上查找:
- 先在自己的楼层(局部作用域)找
- 找不到就去上一楼层(外部函数作用域)找
- 还找不到就去顶楼(全局作用域)找
垃圾回收机制:为什么变量不会消失?
正常情况下,当一个函数执行完毕,它的局部变量就会被垃圾回收机制清理。但是!如果这些变量被闭包引用着,GC 机制就会认为:"这些变量还有人用,不能回收!"
这就导致了变量的持久化,也是闭包能够"记住"状态的原因。
四、闭包的实际应用:从理论到实战
1. 数据私有化:打造你的"保险箱"
在 ES6 之前,JavaScript 没有真正的私有属性,闭包帮我们实现了这个功能:
javascript
function createBankAccount(initialBalance) {
let balance = initialBalance; // 私有变量,外部无法直接访问
return {
deposit: function(amount) {
balance += amount;
return `存款成功,当前余额:${balance}`;
},
withdraw: function(amount) {
if (amount > balance) {
return "余额不足";
}
balance -= amount;
return `取款成功,当前余额:${balance}`;
},
getBalance: function() {
return `当前余额:${balance}`;
}
};
}
const myAccount = createBankAccount(1000);
console.log(myAccount.getBalance()); // "当前余额:1000"
console.log(myAccount.withdraw(200)); // "取款成功,当前余额:800"
// console.log(balance); // 错误!balance是私有的
2. 防抖与节流:性能优化的利器
防抖(Debounce):防止函数被频繁调用,比如搜索框输入
javascript
function debounce(fn, delay) {
let timer = null; // 闭包保存定时器
return function(...args) {
clearTimeout(timer); // 清除之前的定时器
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
const searchInput = document.getElementById('search');
searchInput.addEventListener('input', debounce(function(e) {
console.log('发送搜索请求:', e.target.value);
}, 500));
节流(Throttle):保证函数在一定时间内只执行一次,比如滚动事件
javascript
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(function() {
console.log('处理滚动事件');
}, 100));
3. 循环中的闭包:经典面试题剖析
先看一个经典问题:
javascript
for (var i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i); // 输出什么?
}, 1000);
}
// 答案:5, 5, 5, 5, 5
为什么?因为 var
没有块级作用域,等到定时器执行时,循环已经结束,i
的值已经是 5 了。
解决方案1:使用 IIFE 创建闭包
javascript
for (var i = 0; i < 5; i++) {
(function(j) {
setTimeout(() => {
console.log(j); // 0, 1, 2, 3, 4
}, 1000);
})(i);
}
解决方案2:使用 let 的块级作用域(ES6+推荐)
javascript
for (let i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i); // 0, 1, 2, 3, 4
}, 1000);
}
4. 缓存记忆:提升函数性能
javascript
function memoize(fn) {
const cache = {}; // 闭包保存缓存结果
return function(key) {
if (cache[key]) {
console.log('从缓存中获取');
return cache[key];
}
console.log('计算新结果');
const result = fn(key);
cache[key] = result;
return result;
};
}
// 昂贵的计算函数
function expensiveCalculation(n) {
console.log(`计算 ${n} 的阶乘...`);
let result = 1;
for (let i = 1; i <= n; i++) {
result *= i;
}
return result;
}
const memoizedCalc = memoize(expensiveCalculation);
console.log(memoizedCalc(5)); // 计算新结果
console.log(memoizedCalc(5)); // 从缓存中获取 ← 性能提升!
5. 函数柯里化:灵活的参数处理
javascript
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function(...args2) {
return curried.apply(this, args.concat(args2));
};
}
};
}
// 示例函数
function multiply(a, b, c) {
return a * b * c;
}
const curriedMultiply = curry(multiply);
console.log(curriedMultiply(2)(3)(4)); // 24
console.log(curriedMultiply(2, 3)(4)); // 24
console.log(curriedMultiply(2, 3, 4)); // 24
五、深入探讨:setTimeout 的回调是闭包吗?
这是一个很好的问题!让我们深入分析:
从定义上来说,setTimeout 的回调通常会形成闭包,因为它捕获了外层函数的变量:
javascript
function createTimer() {
const message = "时间到!"; // 自由变量
setTimeout(function() {
console.log(message); // 闭包:访问外部变量
}, 1000);
}
但严格来说,setTimeout 的回调本身不一定是闭包:
javascript
setTimeout(function() {
console.log("Hello"); // 没有访问外部变量 → 不是闭包
}, 1000);
关键判断标准:是否引用了外部作用域的变量。
再看循环定时器的例子:
javascript
// 使用 var + IIFE(显式闭包)
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(function() {
console.log(j); // 闭包:捕获了j
}, 1000);
})(i);
}
// 使用 let(隐式闭包,块级作用域)
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 闭包:每个块作用域的i
}, 1000);
}
六、闭包的注意事项:内存泄漏问题
闭包虽然强大,但使用不当会导致内存泄漏:
javascript
// 潜在的内存泄漏示例
function createHeavyObject() {
const bigData = new Array(1000000).fill('*'); // 大数据
return {
useData: function() {
console.log(bigData.length);
}
};
}
const heavyObject = createHeavyObject();
// 即使我们不再需要heavyObject,bigData依然存在内存中
解决方案:及时释放引用
javascript
function registerHandler() {
let bigData = new Array(1000000).fill('*');
document.getElementById('btn').addEventListener('click', function() {
console.log(bigData.length);
// 使用完后手动释放
bigData = null;
});
}
或者使用现代的事件处理:
javascript
function registerHandler() {
const bigData = new Array(1000000).fill('*');
function handler() {
console.log(bigData.length);
}
const btn = document.getElementById('btn');
btn.addEventListener('click', handler);
// 5秒后移除监听器,闭包引用断开
setTimeout(() => {
btn.removeEventListener('click', handler);
}, 5000);
}
七、总结
闭包是 JavaScript 中极其重要的概念,理解它对于写出高质量的代码至关重要:
- 本质:函数 + 词法作用域的组合
- 原理:作用域链和垃圾回收机制的共同作用
- 应用:数据封装、性能优化、函数式编程等
- 注意事项:注意内存管理,及时释放不再需要的引用
在我的实际项目中,闭包帮助我实现了:
- 模块化的代码组织
- 性能优化的工具函数
- 状态管理的封装
- 避免全局变量污染
记住,闭包就像是一把双刃剑,用得好能让你的代码更加优雅和强大,用得不好可能会导致难以调试的内存问题。掌握闭包,是每个 JavaScript 开发者成长的必经之路!
希望这篇笔记对你有帮助,如果觉得不错,欢迎点赞收藏~我们下期再见!
互动话题:你在项目中遇到过哪些有趣的闭包应用场景?或者被闭包"坑过"的经历?欢迎在评论区分享!