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);

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

相关推荐
理想不理想v6 分钟前
使用JS实现文件流转换excel?
java·前端·javascript·css·vue.js·spring·面试
惜.己26 分钟前
Jmeter中的配置原件(四)
java·前端·功能测试·jmeter·1024程序员节
EasyNTS27 分钟前
无插件H5播放器EasyPlayer.js网页web无插件播放器vue和react详细介绍
前端·javascript·vue.js
guokanglun1 小时前
Vue.js动态组件使用
前端·javascript·vue.js
Go4doom1 小时前
vue-cli3+qiankun迁移至rsbuild
前端
-seventy-1 小时前
Ajax 与 Vue 框架应用点——随笔谈
前端
我认不到你1 小时前
antd proFromSelect 懒加载+模糊查询
前端·javascript·react.js·typescript
集成显卡1 小时前
axios平替!用浏览器自带的fetch处理AJAX(兼容表单/JSON/文件上传)
前端·ajax·json
焚琴煮鹤的熊熊野火2 小时前
前端垂直居中的多种实现方式及应用分析
前端
我是苏苏2 小时前
C# Main函数中调用异步方法
前端·javascript·c#