源本科技 | 码上会

JavaScript 闭包

2025/12/30
20
0

学习目标

  • 理解闭包的定义与核心机制

  • 掌握闭包如何实现私有变量数据封装

  • 了解闭包在异步编程、模块化、函数柯里化中的实际应用

  • 识别闭包使用中的常见陷阱(如 this 绑定问题、内存泄漏)

  • 能够在项目中合理利用闭包提升代码质量


什么是闭包

闭包是指一个函数能够访问并“记住”其词法作用域,即使该函数在其原始作用域之外被调用。

通俗理解:闭包 = 函数 + 其创建时的环境(外层变量)

function outer() {
    let outerVar = "I'm in the outer scope!";
    
    function inner() {
        console.log(outerVar); 
        outerVar = "Updated";
    }
    
    return inner; // 返回内部函数
}

const closure = outer(); // outer() 执行完毕,但 inner 仍能访问 outerVar
closure(); // 输出:I'm in the outer scope!
closure(); // 输出:Updated

关键点

  • inner 函数形成了闭包

  • 即使 outer() 已执行结束,inner 仍能读写 outerVar

  • 这是因为 inner 捕获了 outer 的作用域链


闭包的核心机制

词法作用域

JavaScript 使用词法作用域

  • 函数的作用域由定义位置决定,而非调用位置

  • 内部函数可以访问外层函数的所有变量

因此,inner 在任何地方调用,都能“回溯”到 outer 的作用域。


闭包的典型应用场景

1. 实现私有变量

数据封装

function counter() {
    let count = 0; // 私有变量,外部无法直接访问
    
    return function () {
        count++;
        return count;
    };
}

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

优势count 被安全封装,只能通过返回的函数修改,避免全局污染或意外篡改。


2. 模块模式

利用立即执行函数表达式创建模块:

const counter = (function () {
    let count = 0;
    
    return {
        increment: function () {
            count++;
            console.log(count);
        },
        reset: function () {
            count = 0;
            console.log("Counter reset");
        },
    };
})();

counter.increment(); // 1
counter.increment(); // 2
counter.reset();     // Counter reset

这是早期 JavaScript 实现“类”和“私有成员”的常用模式。


3. 异步编程中的状态保持

setTimeout

function createTimers() {
    for (let i = 1; i <= 3; i++) {
        setTimeout(function () {
            console.log(`Timer ${i}`);
        }, i * 1000);
    }
}
createTimers();
// 输出:
// Timer 1
// Timer 2
// Timer 3

为什么能正确输出 1, 2, 3

  • 因为使用了 let,每次循环创建新的块级作用域

  • 每个 setTimeout 回调形成闭包,捕获各自循环中的 i

若改用 var,所有回调共享同一个 i,最终输出 Timer 4(三次)。


4. 函数柯里化

柯里化:将多参数函数转换为一系列单参数函数,依赖闭包保存已传入的参数。

function add(a) {
    return function(b) {
        return a + b; // 闭包记住 a
    };
}

const addTwo = add(2);
console.log(addTwo(3)); // 5
console.log(addTwo(4)); // 6

应用:配置预设参数、创建专用工具函数(如 fetchUserById = fetchUser.bind(null, 'user')


闭包中的 this 陷阱

闭包本身不绑定 this,而 this调用方式决定,容易导致意外行为:

function Person(name) {
    this.name = name;
    
    this.sayName = function () {
        console.log(this.name); // 正常:this 指向实例
    };

    // 问题:普通函数中的 this 指向全局对象(非严格模式)或 undefined(严格模式)
    setTimeout(function () {
        console.log(this.name); // undefined!
    }, 1000);
}

const p = new Person("Coder");
p.sayName(); // Coder
// 1秒后:undefined

解决方案

  1. 使用 bind() 绑定 this

    setTimeout(function () {
        console.log(this.name);
    }.bind(this), 1000);
  2. 使用箭头函数(继承外层 this):

    setTimeout(() => {
        console.log(this.name); // 正确!
    }, 1000);

闭包的潜在风险

1. 内存泄漏

闭包会阻止外层变量被垃圾回收,如果长期持有大对象引用,可能导致内存占用过高。

function problematicClosure() {
    const largeData = new Array(1000000).fill('*');
    
    return function() {
        // 即使不使用 largeData,它也不会被释放!
        console.log('Still alive');
    };
}

const fn = problematicClosure();
// largeData 一直存在于内存中,直到 fn 被销毁

建议:及时解除不必要的引用,或在不需要时将变量设为 null


2. 性能开销

每个闭包都会保留其作用域链,大量闭包可能增加内存消耗。

权衡:闭包带来强大能力,但也需谨慎使用,避免过度嵌套或无意义的闭包。


重点总结

特性

说明

本质

函数 + 创建时的词法环境

作用

实现私有变量、状态保持、模块化、柯里化

依赖机制

词法作用域

this 注意

闭包不改变 this,需手动绑定或用箭头函数

性能注意

避免长期持有大对象,防止内存泄漏


思考题

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

    for (var i = 0; i < 3; i++) {
        setTimeout(() => console.log(i), 100);
    }

    如何修改使其输出 0, 1, 2?(提供至少两种方法)

  2. 为什么下面的代码中,secret 变量无法从外部访问?这体现了闭包的什么特性?

    const bank = (function() {
        let secret = '123456';
        return { getSecret: () => secret };
    })();