深入理解JavaScript闭包:从概念到防抖节流实战

67 阅读4分钟

🤯 闭包:JavaScript 的“薛定谔的猫”,懂了就开悟,不懂就挠头!


今天我们要聊一个让无数前端萌新从入门到放弃,又让老鸟从放弃到真香的 JavaScript 核心概念——闭包(Closure)

它就像薛定谔的猫:你不知道它有没有执行,但它已经在内存里“活”着了。 😼

别慌!今天我就用人话 + 代码段子,带你从“我是谁我在哪”到“我悟了!”的顿悟之旅。

准备好了吗?坐稳扶好,我们发车咯!🚀


🌪️ 第一章:闭包?不就是“包”嘛!

想象一下,你去奶茶店买了一杯芋圆波波奶茶🥤。

店员给你封了杯,盖了盖,还套了个袋子——这整个过程,就是“封装”。

而“闭包”,就是 JavaScript 的“奶茶封装术”!

🔐 什么是闭包?

官方定义:当一个函数能够访问其外部函数作用域中的变量时,就形成了闭包。

听起来像天书?来,上代码!

function outer() {
    let secret = "我是你的小秘密~ 😘";
    
    function inner() {
        console.log(secret); // inner 能访问 outer 的变量
    }
    
    return inner; // 返回 inner 函数
}

const reveal = outer(); // 调用 outer,得到 inner
reveal(); // 输出:我是你的小秘密~ 😘

你看!outer 执行完后,按理说它的变量 secret 应该被销毁了。

inner 却依然能访问它——因为 inner闭合”了 secret 这个变量,形成了闭包

🎯 关键点:函数 + 外部变量的引用 = 闭包!


🧠 第二章:闭包的“超能力”——私有变量

在传统编程中,我们总想把某些东西藏起来,比如:

  • 我的银行卡密码 💳
  • 我的体重 🏋️‍♂️
  • 我昨晚偷偷吃了三碗泡面 🍜

JavaScript 也一样!我们不想让外部随便修改内部变量。

于是——闭包登场!

function Book(title, author, year) {
    let _title = title;   // 私有变量,带下划线是约定
    let _author = author;
    let _year = year;

    // 公共方法,可以访问私有变量
    this.getTitle = () => _title;
    this.getAuthor = () => _author;
    this.getYear = () => _year;

    this.updateYear = (newYear) => {
        if (typeof newYear === 'number' && newYear > 0) {
            _year = newYear;
            console.log(`年份已更新为 ${newYear} 🎉`);
        } else {
            console.error("年份不合法!别乱来!🚫");
        }
    };
}

const book = new Book("JavaScript 高级程序设计", "Nicholas C. Zakas", 2011);

console.log(book.getTitle()); // ✅ 正常访问
// console.log(book._year);   // ❌ 无法直接访问(虽然技术上可以,但不推荐)

book.updateYear(2025); // ✅ 安全更新
book.updateYear("泡面年"); // ❌ 被拦截

看!_title_year 都是“私有”的,外部不能直接改,只能通过 updateYear 这种“安检通道”来修改。

🎉 这就是闭包的封装能力——像给变量穿上了“隐身衣”!


⚡ 第三章:防抖(Debounce)——别急,让我缓缓!

你有没有遇到过这种情况:

用户在搜索框疯狂打字:“j”→“ja”→“jav”→“java”→“javascript”……
每敲一个字,你就发一个请求?🤯
服务器:我谢谢你全家!

这时候,防抖(Debounce) 就该上场了!

🛑 防抖的核心思想:

“你尽管打字,等你停手 200ms 后,我再发请求。”

就像你妈催你吃饭:“你再玩 5 分钟手机就吃饭!”——但你每玩 1 分钟,她就重置一次倒计时。结果你玩了 30 分钟…… 😅

来,上代码:

function debounce(fn, delay) {
    let timer = null; // 闭包变量:定时器 ID

    return function (...args) {
        clearTimeout(timer); // 清除之前的定时器
        timer = setTimeout(() => {
            fn.apply(this, args); // 执行函数
        }, delay);
    };
}

// 模拟搜索请求
function search(keyword) {
    console.log(`🔍 搜索中:${keyword}`);
}

const debouncedSearch = debounce(search, 300);

// 模拟用户输入
debouncedSearch("j");     // 清除,重新计时
debouncedSearch("ja");    // 清除,重新计时
debouncedSearch("jav");   // 清除,重新计时
debouncedSearch("java");  // ✅ 300ms 后执行:搜索 "java"

🎯 防抖 = 频繁触发 → 只执行最后一次

应用场景:

  • 搜索建议(Google Suggest)
  • 窗口 resize
  • 按钮防重复点击

🕹️ 第四章:节流(Throttle)——我得喘口气!

防抖是“等你停手”,节流是“我不管你怎么疯,我每隔 1s 才执行一次”。

就像游戏里的技能冷却:

“冲锋!” → 冷却 5s → “冲锋!” → 冷却 5s → ……

🚦 节流的核心思想:

“不管你按多快,我最多每 1s 响应一次。”

function throttle(fn, delay) {
    let last = 0; // 上次执行时间

    return function (...args) {
        const now = Date.now();
        if (now - last >= delay) {
            fn.apply(this, args);
            last = now;
        }
    };
}

function reportPosition() {
    console.log(` 当前位置:${Math.random().toFixed(4)}`);
}

const throttledReport = throttle(reportPosition, 1000);

// 模拟高频触发(比如 scroll 事件)
setInterval(throttledReport, 100); // 每 100ms 调用一次
// 但节流后,每 1000ms 才真正执行一次

输出:

 当前位置:0.1234
 当前位置:0.5678
 当前位置:0.9012

🎯 节流 = 高频触发 → 固定频率执行

应用场景:

  • 滚动事件(scroll)
  • 鼠标移动(mousemove)
  • 游戏技能冷却

💔 第五章:this 的“背叛”与闭包的“救赎”

JavaScript 中最让人头疼的,莫过于 this 的指向问题。

看这个经典例子:

const person = {
    name: "小明",
    sayHello: function () {
        setTimeout(function () {
            console.log(`${this.name} 说:你好!`); // ❌ this 指向 window!
        }, 1000);
    }
};

person.sayHello(); // 输出:undefined 说:你好! 😭

setTimeout 里的 thiswindow,不是 personthis 背叛了我们!💔

✅ 解法一:that = this

sayHello: function () {
    const that = this; // 保存 this
    setTimeout(function () {
        console.log(`${that.name} 说:你好!`); // ✅
    }, 1000);
}

✅ 解法二:箭头函数(推荐)

sayHello: function () {
    setTimeout(() => {
        console.log(`${this.name} 说:你好!`); // ✅ 箭头函数不绑定 this
    }, 1000);
}

✅ 解法三:bind

sayHello: function () {
    setTimeout(function () {
        console.log(`${this.name} 说:你好!`);
    }.bind(this), 1000); // ✅ 强制绑定
}

🎯 闭包在这里的作用that = this 利用了闭包保存了 this 的引用!


🎭 第六章:IIFE —— 立即执行的“一次性烟花”

IIFE(Immediately Invoked Function Expression):立即执行函数表达式

就像放烟花🧨:点火 → 爆炸 → 结束,全程不保留变量。

const Counter = (function () {
    let count = 0; // 私有计数器

    return function () {
        return {
            getCount: () => count,
            increment: () => ++count,
            reset: () => count = 0
        };
    };
})();

const c1 = Counter();
const c2 = Counter();

c1.increment(); // count = 1
c2.increment(); // count = 1(独立计数器!)

console.log(c1.getCount()); // 1
console.log(c2.getCount()); // 1

🎉 IIFE + 闭包 = 创建私有作用域 + 模块化


🧩 第七章:闭包的“超能力”大集合

能力说明示例
🛡️ 私有变量外部无法直接访问let _private
🧠 记忆函数缓存计算结果斐波那契记忆化
⏸️ 防抖延迟执行,只执行最后一次搜索框
🕹️ 节流固定频率执行滚动事件
🎭 柯里化函数预配置add(1)(2)
🔗 偏函数固定部分参数partial(fn, arg1)
🎪 事件监听保存上下文addEventListener

🚨 第八章:闭包的“副作用”——内存泄漏!

闭包虽好,但用不好会吃内存

因为闭包会“留住”外部变量,导致它们无法被垃圾回收。

function heavyTask() {
    const bigData = new Array(1000000).fill("💀"); // 100万条数据

    return function () {
        console.log("我还在用 bigData!"); // 闭包引用 bigData
    };
}

const task = heavyTask(); // bigData 一直存在!

⚠️ 建议

  • 及时解除引用:task = null
  • 避免在闭包中引用大型对象
  • 使用 WeakMap / WeakSet 优化

🧠 总结:闭包,是“魔法”也是“责任”

闭包是 JavaScript 的灵魂特性之一。

它让你可以:

  • 封装私有变量 ✅
  • 实现防抖节流 ✅
  • 保存上下文 ✅
  • 构建模块系统 ✅

但也要小心:

  • 内存泄漏 ❌
  • this 指向问题 ❌
  • 调试困难 ❌

🎯 一句话总结
闭包 = 函数 + 外部变量的引用 + 内存驻留


🙌 最后的话

闭包,就像一位隐身的守护者,默默保存着变量,控制着执行节奏,守护着代码的私密性。

当你第一次真正理解闭包时,那种“我悟了! ”的感觉,就像打通了任督二脉,瞬间从“前端小白”进化为“闭包大师”!