欢迎光临
我们一直在努力

13-闭包

闭包:函数记住它的诞生环境

闭包是 JavaScript 中最强大也最容易让人困惑的概念之一。理解了闭包,你就理解了函数如何"记住"并访问它的诞生环境。


学习目标

读完本文,你将学会:

  • 什么是闭包,以及闭包形成的条件
  • 闭包的常见应用场景
  • 闭包与内存的关系
  • 用闭包解决循环中的常见问题

一、什么是闭包

闭包(Closure) 是指一个函数能够记住并访问它的词法作用域,即使这个函数是在它的词法作用域之外执行的。

用更简单的话说:函数"携带"着它的诞生环境一起离开。

function outer() {
const secret = "我是 outer 的变量";

function inner() {
console.log(secret); // inner 访问了 outer 的变量
}

return inner; // 把 inner 返回到 outer 外部
}

const fn = outer(); // outer 执行完毕,理论上 outer 内的变量应该消失
fn(); // 但这里仍然能访问到 secret:"我是 outer 的变量"

fn 就是一个闭包——它是在 outer 内部定义的 inner 函数,即使 outer 已经执行完毕,fn 仍然能访问 outer 中的变量。

1.1 闭包形成的条件

要形成闭包,必须同时满足三个条件:

  • 函数嵌套:一个函数内部定义了另一个函数
  • 内部函数引用了外部函数的变量
  • 内部函数被返回到外部,或以某种方式暴露出去
  • function createCounter() {
    let count = 0; // 外部函数的变量

    return { // 返回的对象包含内部函数
    increment() { // 内部函数 1
    count++;
    return count;
    },
    decrement() { // 内部函数 2
    count;
    return count;
    },
    getCount() { // 内部函数 3
    return count;
    }
    };
    }

    const counter = createCounter();
    console.log(counter.increment()); // 1
    counter.increment();
    console.log(counter.getCount()); // 2


    二、闭包的常见应用

    2.1 私有变量

    JavaScript 没有原生的私有变量机制(ES2022 之前),闭包是实现私有变量的经典方式。

    function createPerson(name) {
    // _age 是"私有"变量,外部无法直接访问
    let _age = 0;

    return {
    getName() {
    return name;
    },
    getAge() {
    return _age;
    },
    growOlder() {
    _age++;
    }
    };
    }

    const person = createPerson("小明");
    console.log(person.getName()); // "小明"
    console.log(person.getAge()); // 0
    person.growOlder();
    console.log(person.getAge()); // 1

    // 外部无法直接修改 _age
    console.log(person._age); // undefined

    2.2 计数器

    function createCounter(start = 0) {
    let count = start;

    return function() {
    count++;
    return count;
    };
    }

    const counter1 = createCounter(0);
    const counter2 = createCounter(100);

    console.log(counter1()); // 1
    console.log(counter1()); // 2
    console.log(counter2()); // 101
    console.log(counter2()); // 102

    // 两个计数器互不影响,各自维护自己的 count

    2.3 函数工厂(根据不同参数创建不同函数)

    function makeMultiplier(factor) {
    return function(number) {
    return number * factor;
    };
    }

    const double = makeMultiplier(2);
    const triple = makeMultiplier(3);

    console.log(double(5)); // 10
    console.log(triple(5)); // 15

    2.4 延迟执行 / 回调函数

    function delayMessage(message, delay) {
    setTimeout(function() {
    console.log(message); // message 被闭包记住
    }, delay);
    }

    delayMessage("3秒后输出", 3000);
    // 即使 delayMessage 已经执行完毕,回调函数仍然能访问 message


    三、闭包与循环:经典面试题

    3.1 var 导致的问题

    for (var i = 0; i < 3; i++) {
    setTimeout(function() {
    console.log(i); // 预期:0, 1, 2 实际:3, 3, 3
    }, 100);
    }

    原因:var 没有块级作用域,三个回调函数共享同一个 i。当回调执行时,循环已经结束,i 的值是 3。

    3.2 解决方案一:用 let(推荐)

    for (let i = 0; i < 3; i++) {
    setTimeout(function() {
    console.log(i); // 0, 1, 2
    }, 100);
    }

    let 有块级作用域,每次循环 i 都是独立的变量,每个回调捕获了各自的 i。

    3.3 解决方案二:用闭包(ES6 之前的做法)

    for (var i = 0; i < 3; i++) {
    (function(capturedI) {
    setTimeout(function() {
    console.log(capturedI); // 0, 1, 2
    }, 100);
    })(i);
    }

    立即执行函数创建了一个新的作用域,capturedI 是每次循环时 i 的副本。


    四、闭包的内存注意事项

    4.1 闭包会阻止垃圾回收

    闭包引用的外部变量不会被垃圾回收,因为内部函数仍然持有对这些变量的引用。

    function createHeavyData() {
    const hugeArray = new Array(1000000).fill("data"); // 大数组

    return function() {
    console.log(hugeArray.length); // 闭包引用了 hugeArray
    };
    }

    const fn = createHeavyData();
    // hugeArray 不会被回收,因为 fn 还引用着它

    4.2 释放闭包

    如果不小心创建了闭包但不再使用,可以手动释放引用:

    let fn = createHeavyData();
    fn(); // 使用完毕

    fn = null; // 释放引用,让 hugeArray 可以被垃圾回收

    4.3 意外的闭包(常见内存泄漏)

    function setupButton() {
    const bigData = fetchLotsOfData();

    document.getElementById('btn').addEventListener('click', function() {
    console.log('clicked');
    // 这个回调没有使用 bigData,但因为闭包机制,bigData 仍然被持有
    });
    }

    如果回调函数不需要外部变量,考虑把它移到外面,或者只引用需要的数据。


    五、常见误区与注意点

    误区正确理解
    闭包是 JavaScript 独有的特性 很多语言都有闭包概念,但 JavaScript 中特别常用
    闭包会影响性能,应该避免使用 闭包是正常特性,合理使用不会导致问题
    闭包只是嵌套函数 必须满足"内部函数被返回到外部"才形成闭包
    闭包引用的所有变量都会被保留 只有被内部函数实际引用的变量才会保留
    用 IIFE 就是闭包 IIFE 可能创建闭包,但本身不等同于闭包

    六、动手练习

    练习 1:实现私有计数器

    编写一个函数,创建一个有"步长"功能的计数器:

    const counter = createStepCounter(10); // 从 10 开始
    console.log(counter.next()); // 11(默认步长 1)
    console.log(counter.next()); // 12
    console.log(counter.reset()); // 10
    console.log(counter.setStep(5)); // 设置步长为 5
    console.log(counter.next()); // 15

    参考答案
    function createStepCounter(start = 0) {
    let count = start;
    let step = 1;

    return {
    next() {
    count += step;
    return count;
    },
    reset() {
    count = start;
    return count;
    },
    setStep(newStep) {
    step = newStep;
    return step;
    }
    };
    }

    练习 2:只执行一次的函数

    编写一个 once 函数,确保传入的函数只执行一次,后续调用返回第一次的结果。

    const initialize = once(() => {
    console.log("初始化中…");
    return { db: "connected" };
    });

    initialize(); // "初始化中…" 返回 { db: "connected" }
    initialize(); // 不输出,直接返回第一次的结果

    参考答案
    function once(fn) {
    let called = false;
    let result;

    return function(args) {
    if (!called) {
    called = true;
    result = fn(args);
    }
    return result;
    };
    }

    练习 3:解释输出

    以下代码输出什么?为什么?

    function createFunctions() {
    const functions = [];

    for (var i = 0; i < 3; i++) {
    functions.push(function() {
    return i;
    });
    }

    return functions;
    }

    const fns = createFunctions();
    console.log(fns[0]());
    console.log(fns[1]());
    console.log(fns[2]());

    参考答案

    全部输出 3。

    原因:var 没有块级作用域,三个函数共享同一个 i。当 createFunctions 执行完毕时,i 的值已经是 3。三个函数都闭包引用了同一个 i。

    修复方法:把 var 改成 let。


    七、AI 辅助学习

    7.1 本节知识点的 AI 提问模板

    【背景】我是 JavaScript 初学者,正在学习第 13 篇"闭包"。
    我已经了解作用域和作用域链的概念。
    【问题】我理解闭包是函数记住并访问外部变量的能力,但我不清楚:
    1. 闭包在内存中是如何工作的?
    2. 为什么有时候闭包会导致内存泄漏?
    3. 现代 JavaScript 中,class 的私有字段(#private)能替代闭包实现私有变量吗?
    【期望】请解释闭包的内存模型,
    给出 2 个闭包导致内存泄漏的实际场景和解决方案,
    对比闭包实现私有变量和 class 私有字段的优劣。

    7.2 用 AI 验证你的理解

    • 问 AI:“以下代码是否形成闭包?解释原因”(给出嵌套函数代码)
    • 让 AI 出 3 道闭包相关的面试题
    • 问 AI:“React 的 useState 是否用到了闭包?解释原理。”

    7.3 警惕 AI 的常见错误

    • AI 可能在解释闭包时引入过于复杂的术语,要求用简单例子解释
    • AI 可能把"闭包"和"作用域"混淆,注意区分
    • AI 可能推荐用 var 解决循环闭包问题,坚持用 let

    八、配套代码

    本文示例代码位于:CODE/13-闭包/

    文件名说明
    closure-playground.html 闭包练习场:创建计数器、函数工厂,观察闭包的内存行为

    九、本章小结

    • 闭包:函数记住并访问其词法作用域的能力
    • 形成条件:函数嵌套 + 内部引用外部变量 + 内部函数暴露到外部
    • 常见应用:私有变量、计数器、函数工厂、回调函数
    • 循环问题:var 导致共享变量,用 let 或 IIFE 解决
    • 内存注意:闭包引用的变量不会被垃圾回收,不需要时释放引用

    十、下篇预告

    下一篇进入 JavaScript 最 tricky 的概念之一:《this 关键字:指向谁,由谁决定》,你将学到:

    • this 的四种绑定规则
    • 箭头函数中的 this 行为
    • call、apply、bind 的用法
    • 实际开发中 this 相关问题的解决方案

    如果本文对你有帮助,欢迎点赞、收藏、关注专栏。有任何问题可以在评论区交流!

    赞(0)
    未经允许不得转载:171主机测评 » 13-闭包
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址