← JavaScript布尔值 JavaScript箭头函数 →

JavaScript闭包

原创 2026-03-14 JavaScript 已有人查阅

在 JavaScript 的学习过程中,闭包(Closure)是一个让人既困惑又着迷的概念。很多初学者觉得它难以捉mō,但在项目中,你已经在不知不觉中使用它了。

闭包就是函数能够记住并访问它定义时的作用域,即使这个函数在定义它的作用域之外执行。这就好比一个背包,函数离开原处时,还背着原作用域里的变量。

闭包的形成条件

闭包的形成需要两个条件:

  1. 函数内部定义了另一个函数(内嵌函数)

  2. 内嵌函数引用了外部函数的变量

来看一个最基本的例子:

// 代码号学习编程:基础闭包示例
function createGreeting(name) {
    // 外部函数的局部变量
    let message = "欢迎,";
    
    // 内部函数使用了外部函数的变量
    function greet() {
        return message + name + "!";
    }
    
    return greet; // 返回内部函数本身
}

const greetRohit = createGreeting("Rohit");
console.log(greetRohit()); // 输出: 欢迎,Rohit!

代码执行后,createGreeting 函数已经运行完毕,按理说它的内部变量 message 和参数 name 应该被销毁了。但当我们调用 greetRohit() 时,依然能访问到 "欢迎," 和 "Rohit"。这就是闭包的作用——greet 函数把它定义时的环境“打包”带走了。

词法作用域:闭包的基石

理解闭包,要明白词法作用域(Lexical Scoping)。它的核心规则是:函数的作用域由函数定义的位置决定,而不是由调用位置决定

function outer() {
    const courseName = "JavaScript 高级教程";
    
    function inner() {
        // inner 在这里定义,所以它能访问 outer 里的变量
        console.log("课程名称: " + courseName);
    }
    
    inner(); // 调用位置在 outer 内部
}

outer(); // 输出: 课程名称: JavaScript 高级教程

即使把 inner 函数拿到外面去调用,它依然记得 courseName

function outer() {
    const courseName = "闭包专题讲解";
    
    function inner() {
        console.log("课程名称: " + courseName);
    }
    
    return inner; // 把 inner 返回出去
}

const innerFunc = outer();
innerFunc(); // 调用位置在全局,但依然能访问 courseName
// 输出: 课程名称: 闭包专题讲解

这就是词法作用域的体现——作用域链在函数定义时就确定了,闭包则负责在函数执行时维持这条链。

闭包的实用场景

闭包不是理论概念,它在日常开发中有很多实际用途。

1. 私有变量(数据封装)

在项目中,有些数据不希望被随意修改,这时可以利用闭包创建私有变量。

// 代码号学习编程:创建计数器模块
function createCounter() {
    // count 是私有变量,外部无法直接访问
    let count = 0;
    
    return {
        increment: function() {
            count++;
            console.log("当前计数: " + count);
        },
        decrement: function() {
            count--;
            console.log("当前计数: " + count);
        },
        getCount: function() {
            return count;
        }
    };
}

const counter = createCounter();
counter.increment(); // 输出: 当前计数: 1
counter.increment(); // 输出: 当前计数: 2
console.log(counter.count); // 输出: undefined (无法直接访问)
console.log(counter.getCount()); // 输出: 2 (通过方法获取)

这种方式比使用全局变量安全得多。团队协作时,如果有人不小心写了 counter.count = 100,也不会影响内部真正的 count 值。

2. 函数柯里化(Currying)

柯里化是把接受多个参数的函数,转换成一系列接受单个参数的函数。闭包在这里的作用是“记住”之前传入的参数。

// 普通加法函数
function add(a, b) {
    return a + b;
}

// 使用闭包实现柯里化
function curriedAdd(a) {
    return function(b) {
        return a + b;
    };
}

const addFive = curriedAdd(5); // 固定第一个参数为 5
console.log(addFive(3)); // 输出: 8
console.log(addFive(10)); // 输出: 15

const addTen = curriedAdd(10);
console.log(addTen(20)); // 输出: 30

这种模式在配置函数、延迟计算等场景中很有用,能提高代码的复用性。

3. 异步操作中的状态保持

在处理 setTimeout、事件监听或 AJAX 请求时,闭包能帮我们记住异步操作发生时的状态。

function delayedMessage(message, delay) {
    setTimeout(function() {
        // 这个内部函数形成了闭包,记住了 message 参数
        console.log("延迟消息: " + message);
    }, delay);
}

delayedMessage("你好,2026年!", 2000); // 2秒后输出
delayedMessage("闭包依然有效", 3000); // 3秒后输出

如果不用闭包,等到 setTimeout 的回调执行时,变量 message 早就被他值覆盖了。

4. 使用 IIFE 创建独立作用域

立即执行函数表达式(IIFE)结合闭包,可以创建独立的作用域,避免变量污染全局。

// 创建一个模块,内部数据对外不可见
const userModule = (function() {
    // 私有变量
    let username = "游客";
    let loginCount = 0;
    
    // 返回公共 API
    return {
        setUser: function(name) {
            username = name;
            loginCount++;
        },
        getUserInfo: function() {
            return `用户: ${username}, 登录次数: ${loginCount}`;
        }
    };
})();

userModule.setUser("Alan");
console.log(userModule.getUserInfo()); // 输出: 用户: Alan, 登录次数: 1
console.log(userModule.username); // 输出: undefined (无法访问私有变量)

这种方式在封装插件、工具库时很常见,可以只暴露需要的方法,隐藏内部实现细节。

闭包和 this 的微妙关系

闭包内部使用 this 时,会遇到意料之外的情况,因为 this 的指向取决于函数的调用方式,而不是定义位置。

function Student(name) {
    this.name = name;
    
    // 方法1: 使用普通函数,this 会丢失
    setTimeout(function() {
        console.log("普通函数中的 this.name:", this.name);
    }, 1000);
    
    // 方法2: 使用箭头函数,继承外部的 this
    setTimeout(() => {
        console.log("箭头函数中的 this.name:", this.name);
    }, 1000);
    
    // 方法3: 使用 bind 绑定 this
    setTimeout(function() {
        console.log("bind 后的 this.name:", this.name);
    }.bind(this), 1000);
}

const student = new Student("Rohit");
// 1秒后输出:
// 普通函数中的 this.name: undefined
// 箭头函数中的 this.name: Rohit
// bind 后的 this.name: Rohit

从代码运行结果可以看出,普通函数里的 this 不再指向 student 实例。解决这个问题,可以用箭头函数(它不绑定自己的 this),或者在外部把 this 保存到一个变量里(比如 const self = this),然后用闭包去访问那个变量。

闭包的注意事项

内存管理

闭包会让外部函数的变量一直活在内存里,不会被垃圾回收机制清理。如果滥用闭包,尤是在循环中创建大量闭包,会占用较多内存。

function processItems(items) {
    // 每个 item 的处理函数都是一个闭包
    let handlers = [];
    for (let i = 0; i < items.length; i++) {
        // 注意:这里用 let,如果用 var 会有不同结果
        handlers.push(function() {
            console.log("处理第 " + i + " 项: " + items[i]);
        });
    }
    return handlers;
}

let handlers = processItems(["A", "B", "C"]);
handlers[0](); // 输出: 处理第 0 项: A
handlers[1](); // 输出: 处理第 1 项: B

在这个例子中,每个处理函数都形成了一个闭包,记住了对应的 i 和 items。如果 items 是一个很大的数组,这些闭包会一直持有对它的引用,导致内存无法释放。用完闭包后,如果不再需要,可以手动解除引用(如 handlers = null),帮助垃圾回收。

调试复杂度

闭包增加了作用域链的长度,调试时不容易看清变量来自哪里。在 Chrome 开发者工具的 Sources 面板里,可以通过查看 Scope 区域来观察闭包中保存的变量。

本节课程知识要点

  1. 闭包的定义:函数与词法环境的组合,使得内层函数能访问外层函数的变量,即使外层函数已执行完毕。

  2. 形成条件:函数嵌套 + 内层函数引用外层变量。

  3. 词法作用域:作用域由函数定义位置决定,闭包依赖此规则维持变量访问。

  4. 核心用途

    • 数据封装:创建私有变量,实现模块化

    • 状态保持:让异步操作记住当时的上下文

    • 函数柯里化:固定部分参数,生成更具体的函数

    • 事件处理:在回调中访问组件实例的数据

  5. 注意事项

    • 闭包造成内存占用,用后及时清理无用引用

    • 闭包内 this 的指向需要留意,可用箭头函数或 bind 解决

    • 适度使用,避免作用域嵌套过深,影响代码可读性

闭包是 JavaScript 函数式编程的基础,也是理解许多框架内部机制的关键。刚开始接触觉得抽象,多写几个例子,慢慢就能体会到它的用处和设计思路。在开发中,当你需要“记住”某个状态、或者创建独立的作用域时,不妨想想闭包能否帮上忙。

← JavaScript布尔值 JavaScript箭头函数 →
分享笔记 (共有 篇笔记)
验证码:
微信公众号