JavaScript 中的闭包(Closure):从原理到实战,详解核心概念与应用场景

157 阅读6分钟

一、什么是闭包?通俗易懂地理解闭包的本质

闭包是指函数能够访问并记住其词法作用域,即使该函数在其作用域外执行。

换句话说:

  • 如果一个函数可以访问它定义时所在作用域中的变量,即使这个函数被返回并在其他地方调用,那它就是一个闭包函数
  • 这些变量被称为自由变量,即不是函数参数也不是局部变量,但被函数引用。

✅ 示例:最简单的闭包结构

function outer() {
    let count = 0;

    // inner 是闭包函数,它引用了外部的 count 变量
    function inner() {
        count++;
        console.log(count);
    }

    return inner;
}

const counter = outer(); // 返回 inner 函数
counter(); // 输出 1
counter(); // 输出 2

在这个例子中:

  • inner 函数是一个闭包;
  • 它“记住”了外部作用域中的变量 count
  • 即使 outer() 已经执行完毕,count 也没有被垃圾回收器清除。

二、闭包形成的条件(关键点)

闭包的形成通常需要满足两个条件:

  1. 嵌套函数结构 —— 函数内部定义另一个函数;
  2. 内部函数引用外部函数的变量 —— 内部函数使用了外部函数的局部变量。

这两个条件缺一不可。


三、闭包的核心特性(为什么我们喜欢用它)

特性描述
数据封装外部无法直接访问内部变量,只能通过返回的函数操作
延长变量生命周期变量不会被垃圾回收机制回收
实现私有变量模拟面向对象语言中的“私有属性”

四、闭包的实际应用场景详解(带注释代码 + 场景说明)

闭包不仅是一个理论概念,更是很多实际开发功能背后的关键实现机制。下面我们将结合实际场景,逐一讲解它们是如何利用闭包完成任务的。


1. 防抖(Debounce)

在一段时间内只执行一次函数,频繁触发时重置计时器。

常用于搜索框输入建议、窗口调整、滚动事件等场景。

🔍 实现原理:

  • 利用闭包保存定时器 ID;
  • 每次触发函数前清除之前的定时器,重新开始计时。
/**
 * 防抖函数
 * @param {Function} fn 要防抖执行的函数
 * @param {Number} delay 防抖时间间隔(毫秒)
 */
function debounce(fn, delay) {
    let timer; // 闭包变量,保存定时器ID
    return function (...args) {
        clearTimeout(timer); // 清除之前的定时器
        timer = setTimeout(() => {
            fn.apply(this, args); // 执行原函数
        }, delay);
    };
}

📌 使用示例:

// HTML 中有一个 input 元素
const searchInput = document.getElementById('search');

// 绑定防抖函数
searchInput.addEventListener('input', debounce(fetchSuggestions, 300));

function fetchSuggestions(keyword) {
    console.log('发送请求:', keyword);
}

📌 适用场景

  • 输入框实时搜索建议
  • 窗口大小变化监听
  • 表单提交限制

2. 节流(Throttle)

确保函数在一定时间间隔内只执行一次。

常用于滚动监听、拖拽、高频事件限制。

🔍 实现原理:

  • 利用闭包保存上一次执行的时间戳;
  • 只有超过指定时间间隔后才允许再次执行。
/**
 * 节流函数
 * @param {Function} fn 要节流执行的函数
 * @param {Number} delay 节流时间间隔(毫秒)
 */
function throttle(fn, delay) {
    let last = 0; // 上一次执行时间
    return function (...args) {
        const now = Date.now();
        if (now - last > delay) {
            fn.apply(this, args); // 执行函数
            last = now; // 更新最后一次执行时间
        }
    };
}

📌 使用示例:

window.addEventListener('scroll', throttle(checkScrollPosition, 1000));

function checkScrollPosition() {
    console.log('当前滚动位置:', window.scrollY);
}

📌 适用场景

  • 页面滚动监听
  • 动画帧控制
  • 技能冷却(CD)系统模拟

3. 绑定上下文(this)

JavaScript 中的 this 很容易出错,闭包可以帮助我们固定上下文。

方法一:使用 bind

const obj = {
    name: 'Qwen',
    greet: function () {
        console.log(`Hello, ${this.name}`);
    }
};

// bind 返回一个新的函数,this 绑定为 obj
setTimeout(obj.greet.bind(obj), 1000); // Hello, Qwen

方法二:箭头函数(不绑定自己的 this)

const obj = {
    name: 'Qwen',
    greet: () => {
        console.log(`Hello, ${this.name}`); // this 是外层作用域(如 window)
    }
};

obj.greet(); // Hello, undefined (注意 this 的指向问题)

方法三:手动缓存 this

const obj = {
    name: 'Qwen',
    greet: function () {
        const that = this; // 用闭包保存 this
        setTimeout(function () {
            console.log(`Hello, ${that.name}`); // 正确输出 Qwen
        }, 1000);
    }
};

📌 适用场景

  • 异步回调中保持 this 指向
  • 对象方法中嵌套函数的 this 控制

4. 事件监听器

闭包可用于创建带状态的事件处理器。

/**
 * 创建一个点击次数统计器
 * @param {Number} init 初始化点击数
 */
function createClickHandler(init) {
    let count = init || 0;

    return function (event) {
        count++;
        console.log(`按钮点击次数:${count}`);
    };
}

const button = document.getElementById('btn');
button.addEventListener('click', createClickHandler(0));

📌 适用场景

  • 按钮点击计数
  • 表单交互记录
  • 用户行为追踪

5. 柯里化(Currying)

将多参数函数转换为一系列单参数函数的过程。

闭包在这里的作用是:保存已经传入的参数,直到所有参数收集完成再执行原始函数。

/**
 * 柯里化函数
 * @param {Function} fn 要柯里化的函数
 */
function curry(fn) {
    return function curried(...args) {
        if (args.length >= fn.length) {
            return fn(...args);
        } else {
            return (...newArgs) => curried(...args, ...newArgs);
        }
    };
}

📌 使用示例:

function add(a, b, c) {
    return a + b + c;
}

const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 6

📌 适用场景

  • 参数逐步收集(如表单分步填写)
  • 构建灵活的函数接口
  • 函数式编程基础

6. 记忆函数(Memoization)

缓存函数的执行结果,避免重复计算。

闭包在这里用来保存缓存数据。

/**
 * 记忆函数:缓存执行结果
 * @param {Function} fn 要记忆的函数
 */
function memoize(fn) {
    const cache = {}; // 闭包变量,保存缓存数据
    return function (...args) {
        const key = JSON.stringify(args); // 将参数转为字符串作为键
        if (key in cache) {
            return cache[key];
        }
        const result = fn(...args);
        cache[key] = result;
        return result;
    };
}

📌 使用示例:

const fib = memoize(function (n) {
    if (n <= 1) return n;
    return fib(n - 1) + fib(n - 2);
});

console.log(fib(10)); // 55
console.log(fib(10)); // 从缓存中读取

📌 适用场景

  • 递归优化
  • 图片加载缓存
  • API 请求结果缓存

7. 立即执行函数(IIFE)

创建私有作用域,防止变量污染全局环境。

虽然 IIFE 本身不是闭包,但如果在其中返回函数,这个函数就形成了闭包。

const getSecret = (function () {
    const secret = 'top_secret'; // 私有变量
    return function () {
        return secret;
    };
})();

console.log(getSecret()); // top_secret
console.log(secret);      // ReferenceError: secret is not defined

📌 适用场景

  • 模块封装
  • 防止命名冲突
  • 提供模块对外接口

五、闭包的注意事项(潜在陷阱)

尽管闭包非常强大,但也有一些需要注意的地方:

问题原因解决方法
内存泄漏闭包保留外部变量,导致无法释放内存使用完及时解除引用
性能问题过度使用闭包可能导致性能下降合理使用,避免在循环中创建大量闭包
上下文混乱在异步或回调中容易丢失 this使用 bind、箭头函数或手动绑定

六、总结:闭包的应用地图

应用场景使用闭包的目的
防抖(debounce)保存定时器ID
节流(throttle)保存上次执行时间
上下文绑定(this)保存 this 引用
事件监听器保存状态和数据
柯里化逐步收集参数
记忆函数缓存计算结果
立即执行函数创建私有作用域

闭包是 JavaScript 的灵魂之一,它让函数不再只是“执行一段代码”,而是可以携带状态、记忆信息、控制执行时机的强大工具。