[JS Daily] JavaScript 闭包原理 - 函数的"记忆"能力 #JavaScript #闭包 #作用域 #教程

JavaScript 闭包原理 - 函数的"记忆"能力

#JavaScript #闭包 #作用域 #教程

闭包(Closure)是 JavaScript 最核心的概念之一,也是面试必考题。理解闭包,就理解了 JavaScript 的作用域机制。本文将带你从原理到实践,彻底搞懂闭包。

一、什么是闭包?

定义:闭包是指有权访问另一个函数作用域中变量的函数。简单说,函数"记住"了它被创建时的词法环境。

核心要点:

1. 函数嵌套函数

2. 内部函数引用外部函数的变量

3. 外部函数返回内部函数

二、最简单的闭包示例

// 最经典的闭包例子
function createCounter() {
let count = 0; // 局部变量
 
return function() { // 内部函数
count++; // 引用外部变量
console.log(count);
};
}
 
const counter = createCounter();
counter(); // 输出: 1
counter(); // 输出: 2
counter(); // 输出: 3

神奇之处:createCounter 执行完毕后,count 变量并没有被销毁,而是被内部函数"记住"了。这就是闭包的力量!

三、闭包的工作原理

理解闭包需要先理解作用域链词法作用域

1. 词法作用域

函数的作用域在定义时就确定了,而不是调用时。内部函数天生就能访问外部函数的变量。

2. 作用域链

当访问一个变量时,JavaScript 引擎会沿着作用域链向外查找,直到全局作用域。

3. 变量的生命周期

正常情况下,函数执行完毕后,局部变量会被销毁。但如果内部函数引用了这些变量,它们就会被保留在闭包中。

四、闭包的经典应用场景

1. 数据私有化(模块模式)

// 创建"私有"变量
function createWallet(initialBalance) {
let balance = initialBalance; // 私有变量
 
return {
deposit(amount) {
balance += amount;
console.log(`存入 ${amount},余额: ${balance}`);
},
withdraw(amount) {
if (amount > balance) {
console.log('余额不足!');
return;
}
balance -= amount;
console.log(`取出 ${amount},余额: ${balance}`);
},
getBalance() {
return balance;
}
};
}
 
const myWallet = createWallet(100);
myWallet.deposit(50); // 存入 50,余额: 150
myWallet.withdraw(30); // 取出 30,余额: 120
console.log(myWallet.balance); // undefined - 无法直接访问

2. 函数工厂

// 创建乘法器工厂
function createMultiplier(multiplier) {
return function(number) {
return number * multiplier;
};
}
 
const double = createMultiplier(2);
const triple = createMultiplier(3);
 
console.log(double(5)); // 10
console.log(triple(5)); // 15

3. 柯里化(Currying)

// 柯里化:将多参数函数转换为单参数函数链
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return function(...moreArgs) {
return curried.apply(this, args.concat(moreArgs));
};
};
}

4. 防抖与节流

// 防抖函数
function debounce(fn, delay) {
let timer = null;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
 
// 节流函数
function throttle(fn, limit) {
let inThrottle = false;
return function(...args) {
if (!inThrottle) {
fn.apply(this, args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}

五、闭包的经典陷阱

陷阱 1:循环中的闭包(最经典!)

// ❌ 错误写法 - 所有按钮都输出 5
for (var i = 1; i <= 5; i++) {
setTimeout(() => console.log(i), 1000);
}
 
// ✅ 正确写法 1 - 使用 let
for (let i = 1; i <= 5; i++) {
setTimeout(() => console.log(i), 1000);
}
 
// ✅ 正确写法 2 - 使用 IIFE
for (var i = 1; i <= 5; i++) {
((j) => {
setTimeout(() => console.log(j), 1000);
})(i);
}

原因:var 是函数作用域,循环结束后 i = 6,所有闭包共享同一个 i。let 是块级作用域,每次循环创建新的 i。

陷阱 2:内存泄漏

// ❌ 可能导致内存泄漏
function createHandler() {
const largeData = new Array(1000000).fill('x');
 
return function() {
console.log(largeData.length); // 持有引用
};
}
 
// ✅ 及时释放不需要的数据
function createHandlerFixed() {
const largeData = new Array(1000000).fill('x');
const length = largeData.length; // 只保存需要的
 
return function() {
console.log(length);
};
}

六、速查对比表

概念 说明
闭包本质 函数 + 词法环境
创建条件 函数嵌套 + 内部函数引用外部变量
核心用途 数据私有化、函数工厂、柯里化、回调
注意事项 循环陷阱、内存泄漏、性能影响

七、最佳实践总结

✅ 推荐做法:

1. 优先使用 let/const 替代 var

2. 只闭包真正需要的变量

3. 不再使用的闭包及时置 null

4. 循环中用 let 或 IIFE 隔离作用域

❌ 避免做法:

1. 在循环中用 var + 闭包(经典陷阱)

2. 闭包中持有大对象的引用

3. 过度使用闭包(能用参数传递就别用闭包)

八、一句话总结

闭包 = 函数 + 它能访问的外部变量

函数"记住"了定义时的环境,即使定义时的作用域已经销毁,变量依然存在。

📌 下期预告:原型链与继承 - JavaScript 面向对象的根基

评论

此博客中的热门博文

OpenClaw 救援机器人建设与演进全记录 - 从单点故障到双实例自愈体系

Lossless Claw:无损上下文管理插件分析报告

[Hello-Agents] Day 2: 第一章 初识智能体