JavaScript 闭包:从入门到精通

JavaScript 闭包:从入门到精通 🎯

用最简单的语言,带你彻底搞懂闭包这个"神秘"概念。


一、先讲个故事 📖

想象你有一个书包🎒。

你走出了教室,甚至走出了学校大门,但你的书包里还装着教室里老师发的试卷

虽然你已经"离开"了教室,但你依然可以随时打开书包,看到那张试卷。

这,就是闭包的核心思想。

一个函数,即使离开了它被创建的地方,依然能"记住"并访问那个地方的变量。


二、在理解闭包之前,先搞懂"作用域" 🏠

什么是作用域?

作用域就像房间的墙壁

复制代码
🏠 大房子(全局作用域)
│
│   let name = "小明";
│
│   ┌─────────────────────┐
│   │ 🚪 小房间(函数作用域) │
│   │                       │
│   │   let age = 14;       │
│   │                       │
│   │   // ✅ 在小房间里,    │
│   │   // 既能看到 age,    │
│   │   // 也能看到外面的 name │
│   └─────────────────────┘
│
│   // ❌ 在大房子里,
│   // 看不到小房间里的 age

规则很简单:

  • 📌 里面的房间 → 可以看到外面的东西
  • 📌 外面 → 看不到里面的东西

用代码来说就是:

javascript 复制代码
let name = "小明";

function sayHello() {
    let age = 14;
    console.log(name); // ✅ 能访问外面的 name → "小明"
    console.log(age);  // ✅ 能访问自己的 age → 14
}

sayHello();
console.log(age); // ❌ 报错!外面看不到里面的 age

三、终于要讲闭包了! 🎉

最简单的闭包

javascript 复制代码
function 妈妈() {
    let 零花钱 = 100;

    function 孩子() {
        console.log("我有" + 零花钱 + "元零花钱");
    }

    return 孩子; // 妈妈把孩子"送出去"了
}

let 小明 = 妈妈(); // 妈妈函数已经执行完毕了
小明(); // "我有100元零花钱" ------ 居然还能访问"零花钱"!

等一下,这里发生了什么? 🤔

  1. 妈妈() 函数执行完了,按理说里面的 零花钱 变量应该"消失"了
  2. 但是!孩子 函数被 return 出来了,赋值给了 小明
  3. 小明() 执行时,依然记得 零花钱 = 100

🎒 还记得开头的书包故事吗?孩子函数就像那个书包,它"装着"对 零花钱 的记忆,走到哪里都忘不掉。

用正经的话重新说一遍

javascript 复制代码
function outer() {
    let count = 0;

    function inner() {
        count++;
        console.log(count);
    }

    return inner;
}

let fn = outer();
fn(); // 1
fn(); // 2
fn(); // 3

这就是闭包:

闭包 = 一个函数 + 它能访问的外部变量的环境

inner 函数 + 它记住的 count 变量 = 一个闭包。


四、闭包到底有什么用? 🔧

用途一:创建"私有变量" 🔒

在现实中,银行账户的余额不应该被随便修改,只能通过存钱、取钱操作。

javascript 复制代码
function 创建银行账户(初始余额) {
    let 余额 = 初始余额; // 外面谁也碰不到这个变量!

    return {
        查询余额() {
            console.log(`当前余额:${余额}元`);
        },
        存钱(数额) {
            if (数额 > 0) {
                余额 += 数额;
                console.log(`成功存入${数额}元`);
            }
        },
        取钱(数额) {
            if (数额 > 余额) {
                console.log("余额不足!");
            } else {
                余额 -= 数额;
                console.log(`成功取出${数额}元`);
            }
        }
    };
}

let 我的账户 = 创建银行账户(500);

我的账户.查询余额(); // 当前余额:500元
我的账户.存钱(200);  // 成功存入200元
我的账户.查询余额(); // 当前余额:700元
我的账户.取钱(1000); // 余额不足!

// 想直接改余额?没门!
console.log(我的账户.余额); // undefined ------ 根本访问不到 🔒

闭包让 余额 变成了"私有变量",只有内部的几个函数才能操作它,外界无法直接篡改。


用途二:函数工厂 🏭

闭包可以像工厂一样,批量制造功能类似但有差异的函数。

javascript 复制代码
function 创建乘法器(倍数) {
    return function(数字) {
        return 数字 * 倍数;
    };
}

let 双倍 = 创建乘法器(2);
let 三倍 = 创建乘法器(3);
let 十倍 = 创建乘法器(10);

console.log(双倍(5));  // 10
console.log(三倍(5));  // 15
console.log(十倍(5));  // 50

每次调用 创建乘法器,都会产生一个新的闭包 ,每个闭包"记住"了不同的 倍数


用途三:计数器 🔢

javascript 复制代码
function 创建计数器() {
    let count = 0;

    return {
        加一() { count++; },
        归零() { count = 0; },
        获取() { return count; }
    };
}

let 计数器A = 创建计数器();
let 计数器B = 创建计数器(); // 完全独立!

计数器A.加一();
计数器A.加一();
计数器A.加一();

console.log(计数器A.获取()); // 3
console.log(计数器B.获取()); // 0 ------ 互不影响!

五、经典面试题:循环中的闭包 🎯

这是无数人踩过的坑,看看你能不能答对。

❌ 问题代码

javascript 复制代码
for (var i = 0; i < 3; i++) {
    setTimeout(function() {
        console.log(i);
    }, 1000);
}

你觉得输出什么?

很多人以为是 0, 1, 2

实际输出:3, 3, 3 😱

为什么?

用一个比喻来解释:

想象有 3 个同学,老师说"1秒后看黑板上的数字并大声念出来"。

但在这1秒内,老师一直在擦黑板重新写:先写0,擦掉写1,擦掉写2,擦掉写3。

1秒后,3个同学抬头一看 ------ 黑板上是 3。所以都念出了 3。

var 声明的 i 只有一个(全局共享的黑板),循环结束后 i 变成了 3,三个定时器读到的都是同一个 i

✅ 用闭包修复

javascript 复制代码
for (var i = 0; i < 3; i++) {
    (function(j) {        // 每次循环都创建一个"小房间"
        setTimeout(function() {
            console.log(j); // 每个小房间有自己的 j
        }, 1000);
    })(i);                // 把当前的 i 传进去,"锁"在小房间里
}
// 输出:0, 1, 2 ✅

相当于每个同学拿到了一张自己的纸条📝,上面写着各自的数字,不管黑板怎么变,纸条上的数字不会变。

✅ 更现代的解决方案:用 let

javascript 复制代码
for (let i = 0; i < 3; i++) {
    setTimeout(function() {
        console.log(i);
    }, 1000);
}
// 输出:0, 1, 2 ✅

let 在每次循环时会自动创建一个新的变量副本(自带小房间),本质上和闭包方案一样。


六、进阶:闭包的高级应用 🚀

6.1 函数柯里化(Currying)

把一个接受多个参数的函数,变成一系列接受单个参数的函数。

javascript 复制代码
// 普通写法
function add(a, b, c) {
    return a + b + c;
}
add(1, 2, 3); // 6

// 柯里化写法(闭包链)
function curriedAdd(a) {
    return function(b) {      // 闭包,记住了 a
        return function(c) {  // 闭包,记住了 a 和 b
            return a + b + c;
        };
    };
}
curriedAdd(1)(2)(3); // 6

// 实际妙用:
let addFrom10 = curriedAdd(10);
let addFrom10And20 = addFrom10(20);
console.log(addFrom10And20(5));  // 35
console.log(addFrom10And20(30)); // 60

6.2 实现防抖(Debounce)

搜索框输入时,不希望每按一个键就发请求,而是等用户停下来后再发。

javascript 复制代码
function debounce(fn, delay) {
    let timer = null; // 闭包变量,记住定时器

    return function(...args) {
        clearTimeout(timer);           // 每次调用,清除上一次的定时器
        timer = setTimeout(() => {     // 重新设一个定时器
            fn.apply(this, args);
        }, delay);
    };
}

// 使用
let 智能搜索 = debounce(function(keyword) {
    console.log("搜索:" + keyword);
}, 500);

// 模拟快速输入
智能搜索("J");
智能搜索("Ja");
智能搜索("Jav");
智能搜索("Java");
智能搜索("JavaScript");
// 只会输出最后一次:"搜索:JavaScript" ✅

timer 被闭包保护起来,外部无法干扰,但每次调用返回的函数都能访问并修改它。

6.3 实现缓存/记忆化(Memoization)

让函数"记住"之前的计算结果,避免重复计算。

javascript 复制代码
function createMemo(fn) {
    let cache = {}; // 闭包变量,缓存结果

    return function(n) {
        if (cache[n] !== undefined) {
            console.log(`📦 从缓存读取 ${n} 的结果`);
            return cache[n];
        }
        console.log(`🔨 计算 ${n} 的结果`);
        let result = fn(n);
        cache[n] = result; // 存入缓存
        return result;
    };
}

// 一个耗时的函数
function 斐波那契(n) {
    if (n <= 1) return n;
    return 斐波那契(n - 1) + 斐波那契(n - 2);
}

let 快速斐波那契 = createMemo(斐波那契);

快速斐波那契(30); // 🔨 计算 30 的结果(较慢)
快速斐波那契(30); // 📦 从缓存读取 30 的结果(瞬间)

七、闭包的"副作用":内存泄漏 ⚠️

闭包会让变量一直待在内存里不被回收。

javascript 复制代码
function 创建大数据() {
    let 巨大数组 = new Array(1000000).fill("🔥");

    return function() {
        console.log(巨大数组.length);
    };
}

let fn = 创建大数据();
// 巨大数组 一直在内存中,因为 fn 还"记着"它

fn = null; // ✅ 手动解除引用,让垃圾回收器回收

比喻:闭包就像你一直不清理的书包 🎒,里面的东西越装越多。记得定期清理不需要的闭包引用!

避免内存泄漏的建议

javascript 复制代码
// ✅ 不再需要时,将引用设为 null
let counter = 创建计数器();
// ...使用完毕后
counter = null;

// ✅ 只在闭包中引用必要的变量
function good() {
    let needed = "我需要";
    let huge = new Array(1000000); // 大对象

    return function() {
        // 只使用 needed,不使用 huge
        // 但某些引擎可能仍保留 huge,取决于优化
        console.log(needed);
    };
}

八、一张图总结闭包 🗺️

复制代码
 ┌─────────────────────────────────────────────┐
 │              闭包 (Closure)                   │
 │                                               │
 │   ┌──────────────┐     ┌──────────────────┐  │
 │   │   外部函数     │     │  被记住的变量环境   │  │
 │   │              │────▶│                  │  │
 │   │  function外() │     │  let x = 10     │  │
 │   │  {           │     │  let y = 20     │  │
 │   │    return 内  │     │                  │  │
 │   │  }           │     └───────┬──────────┘  │
 │   └──────────────┘             │              │
 │                                │ 引用         │
 │   ┌──────────────┐            │              │
 │   │   内部函数     │◀───────────┘              │
 │   │              │                            │
 │   │  function内() │  即使外部函数执行完毕,       │
 │   │  {           │  内部函数依然能访问           │
 │   │    用 x 和 y  │  x 和 y                    │
 │   │  }           │                            │
 │   └──────────────┘                            │
 └─────────────────────────────────────────────┘

九、三句话记住闭包 💡

  1. 函数套函数,内部函数使用了外部函数的变量
  2. 内部函数被传到外面使用后,依然记得那些变量
  3. 那些变量不会被销毁,像被锁在保险箱里,只有这个内部函数有钥匙 🔑

十、最后的最后 🎬

闭包不是什么"高深莫测"的东西,你可能早就在不知不觉中用过了:

  • 给按钮添加事件监听?闭包
  • setTimeout + 外部变量?闭包
  • React 的 useState?底层也是闭包
javascript 复制代码
// 你可能天天在写闭包,只是不知道而已
document.getElementById("btn").addEventListener("click", function() {
    let clickCount = 0; // 不对,这样每次都重置了
});

// 正确的闭包写法 ✅
let clickCount = 0;
document.getElementById("btn").addEventListener("click", function() {
    clickCount++; // 这个回调函数 + 外部的 clickCount = 闭包!
    console.log(`点击了 ${clickCount} 次`);
});

闭包就像呼吸一样自然 ------ 你一直在做,只是现在知道了它的名字。 😄


后记

2026年4月16日14点29分于上海,在opus 4.6辅助下完成。

相关推荐
qq_12084093711 小时前
Three.js 性能实战:大场景从 15FPS 到 60FPS 的工程化优化路径
开发语言·前端·javascript
guhy fighting2 小时前
使用vue-virtual-scroller导致打包报错
前端·javascript·vue.js·webpack
小张同学8242 小时前
[特殊字符]Python 进阶实战指南(PyCharm 专属优化):从高效编码到工程化落地,告别新手低效写法
开发语言·python·pycharm
lly2024062 小时前
PHP Math
开发语言
Cecilialana2 小时前
同域名、同项目、仅 hash 变化,window.location.href 不跳转
前端·javascript
Hello--_--World2 小时前
DOM事件流与事件委托、判断数据类型、深浅拷贝、对象遍历方式
前端·javascript
李日灐2 小时前
<1>Linux基础指令:Linux 高频指令详解 + 文件与目录认知
linux·运维·服务器·开发语言·后端·命令
喜欢流萤吖~2 小时前
SpringBoot 异步处理与线程池实战
java·开发语言
c++逐梦人2 小时前
C++ RAII流式日志库实现
开发语言·c++