闭包是 JavaScript 中既强大又危险的概念。它能让我们写出优雅的代码,也可能悄无声息地吞噬系统内存。本篇文章将深入探讨闭包的每一个角落。
前言:闭包不止是返回函数
实际开发中,很多人理解的闭包是这样的:
function outer() {
let count = 0;
return function() {
count++;
return count;
};
}
但实际上,闭包无处不在,甚至闭包产生的结果和我们预想的结果大相庭径:
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i); // 5,5,5,5,5
}, 100);
}
闭包形成的必要条件
什么是闭包?
所谓闭包 ,是指那些能够访问自由变量的函数。自由变量是指在函数中使用的,但既不是函数参数,也不是函数局部变量的变量。
更通俗的理解是:闭包是函数和声明该函数的词法环境的组合。即使函数在其词法环境之外执行,它仍然可以访问该环境中的变量。
let globalFunc;
function outer() {
const secret = "闭包的秘密";
globalFunc = function() {
console.log(secret);
};
}
outer();
globalFunc(); // "闭包的秘密"
// 即使outer执行完毕,globalFunc仍然能访问secret
闭包形成的三个必要条件
- 条件1:嵌套函数
- 条件2:内部函数引用外部变量
- 条件3:内部函数在外部作用域可访问
// 条件1:嵌套函数
function outer() {
// 条件2:内部函数引用外部变量
const outerVar = "外部变量";
// 条件3:内部函数在外部作用域可访问
return function inner() {
console.log(outerVar); // 引用外部变量
};
}
const closureFunc = outer(); // 执行outer,返回inner函数
closureFunc(); // 调用inner,仍然可以访问outerVar
不形成闭包的情况
内部函数没有引用外部变量
function noClosure() {
let local = "局部变量";
// 内部函数没有引用外部变量
return function inner() {
console.log("没有引用外部变量");
}
}
内部函数引用了外部变量,但内部函数没有被返回或传递出去
function noClosure() {
let local = "局部变量";
function inner() {
console.log(local); // 引用了外部变量
}
// 但内部函数没有被返回或传递出去
inner(); // 直接在内部调用
// 函数执行完,local被销毁
}
闭包的内存模型
闭包的内存结构
我们先通过一个复杂例子理解闭包的内存结构:
function createComplexClosure() {
// 这些变量会被闭包捕获
const config = { max: 100, min: 0 };
let privateCounter = 0;
const secretKey = "ABC-123";
// 辅助函数 - 也会形成闭包
function validate(value) {
return value >= config.min && value <= config.max;
}
// 返回的对象方法都形成闭包
return {
setValue: function(value) {
if (validate(value)) {
privateCounter = value;
return true;
}
return false;
},
increment: function() {
if (validate(privateCounter + 1)) {
privateCounter++;
}
return privateCounter;
},
getInfo: function() {
// 注意:这个方法没有使用secretKey,但secretKey仍然被保留!
return {
current: privateCounter,
config: { ...config } // 返回副本,不暴露引用
};
},
// 这个函数使用了所有被捕获的变量
debug: function() {
return {
counter: privateCounter,
config,
key: secretKey
};
}
};
}
const obj = createComplexClosure();
内存结构分析:
- 每个方法都有自己的函数对象
- 但它们共享同一个词法环境(闭包)
- 这个词法环境包含:
config,privateCounter,secretKey,validate()
闭包的"隐藏"成本
在实际开发中,闭包捕获的变量可能比我们预想的要多得多:
function createHeavyClosure() {
// 一个大对象
const bigData = {
items: new Array(10000).fill(null).map((_, i) => ({
id: i,
data: `Item ${i}`,
meta: { created: Date.now(), tags: ['test'] }
})),
config: { /* ... */ }
};
// 一些原始值
let counter = 0;
const name = "Closure";
// 返回的函数只用了counter
return function() {
counter++;
console.log(`${name}: ${counter}`);
// 注意:这里没有使用bigData!
};
}
const lightFunc = createHeavyClosure();
上述代码中,虽然 lightFunc 只使用了 counter 和 name,但 bigData 也被闭包捕获了,无法被垃圾回收!
多个闭包共享环境
在同一个作用域中创建的多个函数,会共享闭包环境:
function createSharedClosure() {
let sharedState = 0;
const messages = [];
function addMessage(msg) {
messages.push(`${new Date().toISOString()}: ${msg}`);
// 只保留最近10条
if (messages.length > 10) {
messages.shift();
}
}
return {
// 这两个方法共享同一个闭包环境
increment: function() {
sharedState++;
addMessage(`Incremented to ${sharedState}`);
return sharedState;
},
decrement: function() {
sharedState--;
addMessage(`Decremented to ${sharedState}`);
return sharedState;
},
getLog: function() {
return [...messages]; // 返回副本
},
// 这个方法会创建新的闭包
createAction: function(actionName) {
// 这个函数有自己的闭包(捕获actionName)
return function() {
sharedState = 0;
addMessage(`Reset by ${actionName}`);
return `Reset by ${actionName}`;
};
}
};
}
const manager = createSharedClosure();
manager.increment(); // 1
manager.increment(); // 2
manager.decrement(); // 1
const resetAction = manager.createAction("Admin");
resetAction(); // sharedState被重置为0
console.log(manager.getLog());
常见闭包模式
模块模式(Module Pattern)
const Calculator = (function() {
// 私有变量
let memory = 0;
const version = '1.0.0';
// 私有函数
function validateNumber(num) {
return typeof num === 'number' && !isNaN(num);
}
// 公共接口
return {
add: function(a, b) {
if (!validateNumber(a) || !validateNumber(b)) {
throw new Error('Invalid numbers');
}
const result = a + b;
memory = result; // 更新内存
return result;
},
subtract: function(a, b) {
if (!validateNumber(a) || !validateNumber(b)) {
throw new Error('Invalid numbers');
}
const result = a - b;
memory = result;
return result;
},
getMemory: function() {
return memory;
},
clearMemory: function() {
memory = 0;
},
getVersion: function() {
return version;
}
};
})();
// 使用
console.log(Calculator.add(5, 3)); // 8
console.log(Calculator.getMemory()); // 8
console.log(Calculator.getVersion()); // 1.0.0
// 无法直接访问私有成员
// Calculator.memory // undefined
// Calculator.validateNumber // undefined
柯里化(Currying)
function curry(fn) {
return function curried(...args) {
// 如果参数数量足够,调用原函数
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
// 参数不够,返回新函数继续接收参数
return function(...args2) {
// 这里形成了闭包,args被记住了
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
防抖(Debounce)
防抖:在事件触发后等待一段时间再执行,如果在此期间再次触发则重新计时:
function debounce(fn, delay) {
let timer = null;
return function(...args) {
// 清除之前的定时器
clearTimeout(timer);
// 设置新的定时器
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, delay);
};
}
节流(Throttle)
节流:在规定的一段时间内只执行一次:
function throttle(fn, interval) {
let lastTime = 0;
let timer = null;
return function(...args) {
const now = Date.now();
const remaining = interval - (now - lastTime);
if (remaining <= 0) {
// 时间到了,立即执行
if (timer) {
clearTimeout(timer);
timer = null;
}
fn.apply(this, args);
lastTime = now;
} else if (!timer) {
// 设置定时器,在剩余时间后执行
timer = setTimeout(() => {
fn.apply(this, args);
lastTime = Date.now();
timer = null;
}, remaining);
}
};
}
函数记忆(Memoization)
function memoize(fn) {
const cache = new Map();
return function(...args) {
// 创建缓存键
const key = JSON.stringify(args);
// 如果缓存中有,直接返回
if (cache.has(key)) {
console.log('从缓存获取:', key);
return cache.get(key);
}
// 否则计算并缓存
console.log('计算并缓存:', key);
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
闭包的性能考量
内存泄漏的常见模式
模式1:意外的全局变量引用
function createLeakyModule() {
const hugeData = new Array(1000000).fill("data");
// 这个事件监听器形成了闭包,引用了hugeData
document.addEventListener('click', function() {
// 即使回调没有使用hugeData,它也被闭包捕获了!
console.log('clicked');
});
// 解决方案:在不需要时移除事件监听器
// 或者避免在包含大数据的函数中定义事件处理器
}
模式2:循环引用
function createCircularReference() {
const elements = [];
for (let i = 0; i < 1000; i++) {
const element = {
data: new Array(1000).fill('data'),
onClick: function() {
// 这个函数引用了elements数组
console.log('Element clicked', elements.length);
}
};
elements.push(element);
}
return elements;
// elements数组和每个onClick函数相互引用,无法被回收
}
模式3:定时器未清理
function startPolling() {
const data = fetchData(); // 大数据
setInterval(function() {
// 这个闭包捕获了data
processData(data);
}, 1000);
// 如果没有clearInterval,data永远不会被释放
}
性能优化技巧
技巧1:避免不必要的闭包
function processItems(items) {
// 不好的做法:在循环中创建闭包
items.forEach(function(item) {
// 这个函数创建了闭包,捕获了items
processItem(item);
});
// 好的做法:使用箭头函数或避免创建函数
for (let i = 0; i < items.length; i++) {
processItem(items[i]); // 没有创建闭包
}
}
技巧2:分离数据和逻辑
function createEfficientClosure() {
// 大数据
const largeData = fetchLargeData();
// 提取需要的数据,而不是保留整个大数据对象
const processedData = processData(largeData);
// 立即释放对大数据的引用
// largeData = null; // 如果不再需要
return function() {
// 只使用处理后的数据
return operateOnData(processedData);
};
}
技巧3:使用WeakMap/WeakSet
const weakCache = new WeakMap();
function getExpensiveValue(obj) {
if (weakCache.has(obj)) {
return weakCache.get(obj);
}
const value = computeExpensiveValue(obj);
weakCache.set(obj, value);
return value;
}
// 当obj不再被引用时,WeakMap中的条目会自动被垃圾回收
技巧4:模块模式的优化
const OptimizedModule = (function() {
// 私有数据,但避免创建大对象
const privateData = (function() {
// 这里初始化私有数据
const data = {};
// ... 初始化逻辑
return data;
})();
// 公共方法
function publicMethod() {
// 使用privateData
}
// 清理方法
function cleanup() {
// 清理私有数据
for (const key in privateData) {
delete privateData[key];
}
}
return {
publicMethod,
cleanup
};
})();
结语
闭包是JavaScript的强大特性,但需要谨慎使用,对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!