JavaScript 中的闭包到底是什么?

您可能在 JavaScript 开发或教程中听说过"闭包"这个术语,但它到底是什么呢?

什么是闭包?

用最简单的术语来说,闭包是一个函数 (严谨的说,闭包不是函数,所有函数都与其周围的作用域形成闭包,这里是为了简单说明而如此举例) ,它会记住定义它的变量,而不管它稍后在哪里执行。让我们用一个例子来分解它:

JavaScript 复制代码
function outerFunction() {
    let outerVariable = 'I am outside!';

    function innerFunction() {
        console.log(outerVariable);
    }

    return innerFunction;
}

const myInnerFunction = outerFunction();
myInnerFunction(); // Outputs: 'I am outside!'

在此示例中,innerFunction 是一个闭包。为什么?因为它是在另一个函数 (outerFunction) 中定义的函数,并且即使在outerFunction 执行完毕后,它也可以访问outerFunction 作用域的变量(在本例中为outerVariable)。

当outerFunction被调用时,它定义outerVariable,定义innerFunction,然后返回innerFunction。即使outerFunction已经完成执行并且outerVariable理论上会超出范围并被垃圾收集,innerFunction仍然可以访问outerVariable。

闭包有什么用?

数据封装:

您可以使用闭包来创建私有变量和方法。这对于代码的模块化设计特别有用。

JavaScript 复制代码
function createCounter() {
    let count = 0;
    return {
        increment: function() {
            count++;
            return count;
        },
        decrement: function() {
            count--;
            return count;
        },
    };
}

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

在此示例中,count 充当私有变量。它只能由递增和递减函数访问,而不能从外部访问。

维持状态

在 JavaScript 中,闭包通常用于维护异步操作、事件处理程序等中的状态。

JavaScript 复制代码
function setupAlertTimeout() {
    var message = 'Message after 3 seconds';
    setTimeout(function alertMessage() {
        alert(message);
    }, 3000);
}

setupAlertTimeout();

在这里,即使在 setupAlertTimeout 执行完毕后,alertMessage 函数仍保持对消息变量的访问。

柯里化

闭包允许使用柯里化,这是一种将具有多个参数的函数计算为具有单个参数的函数序列的技术。

JavaScript 复制代码
function multiply(a) {
    return function(b) {
        return a * b;
    };
}

const multiplyByTwo = multiply(2);
console.log(multiplyByTwo(4)); // 8

在本例中,multiplyByTwo 是一个记住 a 值(即 2)的闭包。

试试看

考虑以下代码片段:

ini 复制代码
function outer() {
    var count = 0;
    return function inner() {
        count++;
        return count;
    };
}
var counter1 = outer();
var counter2 = outer();
console.log(counter1());
console.log(counter1());
console.log(counter2());
console.log(counter2());

上述代码的输出是什么?为什么?

解决方案

在提供的代码中,通过两次调用 external() 创建两个单独的闭包。每个闭包都维护自己单独的词法环境,其中 count 变量在调用之间保留。以下是逐步发生的情况:

  1. var counter1 = 外部(); 创建第一个闭包。在这个闭包内,count 被初始化为 0。

  2. counter1() 第一次被调用。这会将第一个闭包内的 count 增加到 1 并返回 1。

  3. counter1() 再次被调用。这会将第一个闭包内的 count 增加到 2 并返回 2。

  4. var counter2 = 外部(); 创建第二个闭包。在这个闭包内,一个新的、单独的计数被初始化为 0。

  5. counter2() 第一次被调用。这会将第二个闭包中的计数增加到 1 并返回 1。

  6. counter2() 再次被调用。这会将第二个闭包中的计数增加到 2 并返回 2。因此,代码的输出将是:

    1
    2
    1
    2

一个实际的例子

scss 复制代码
function showDelayedMessage(message, delay) {
    setTimeout(function() {
        console.log(message);
    }, delay);
}

showDelayedMessage('Hello after 3 seconds', 3000);
showDelayedMessage('Hello after 5 seconds', 5000);

在不利用闭包的情况下编写像 showDelayedMessage 这样的函数通常会涉及使用全局变量

javascript 复制代码
var globalMessage; // Declare a global variable

function showDelayedMessageGlobal(message, delay) {
    globalMessage = message; // Assign the message to the global variable
    setTimeout(function() {
        console.log(globalMessage);
        globalMessage = null; // Clear the message after displaying it
    }, delay);
}

showDelayedMessageGlobal('Hello after 3 seconds', 3000);

这个示例说明,虽然您可以避免闭包,但这样做通常会导致不太理想的编码实践,例如全局名称空间污染,尤其是在处理异步事件或封装功能时。闭包提供了一种用函数封装数据的干净而有效的方法,这就是为什么尽管有其他方法可用,但闭包仍被如此广泛使用的原因。

相关推荐
尾善爱看海6 小时前
Vue 面试收官篇:SSR、性能优化落地、30 道高频面试题精讲(附标准答案)
前端·javascript·vue.js·面试·vue
驳是7 小时前
一个组件,提升 react-router 开发的幸福感
前端·preact
GreenTea7 小时前
OpenAI Agents API 上手实测:一次调用把整个 agent loop 甩给 OpenAI
前端·后端·算法
星云API技术支持7 小时前
企业微信二次开发:群权限设置、成员管理与群资料维护的接口组合实践
java·前端·企业微信
京东云开发者7 小时前
81.8 秒的视频,我们改了 15 个版本:一次纯 Codex 驱动的 AI-native 视频实践
前端·aigc
Csvn8 小时前
TypeScript 大型项目架构:从单体 tsconfig 到分层可扩展的工程
前端
计算机魔术师9 小时前
Dario Amodei 发文呼吁为前沿 AI 降速并提出三点计划
前端
计算机魔术师10 小时前
OpenAI的AI代理偷偷给RubyGems下毒,我们却毫无察觉
前端
甲维斯10 小时前
0代码,0建模,3句话开发一个3D游戏!
前端·游戏·游戏开发
IT_陈寒10 小时前
Vue的响应式更新把我坑惨了,原来问题出在这
前端·人工智能·后端