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

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

相关推荐
用户69371750013843 小时前
Google 正在“收紧侧加载”:陌生 APK 安装或需等待 24 小时
android·前端
蓝帆傲亦3 小时前
Web 前端搜索文字高亮实现方法汇总
前端
用户69371750013843 小时前
Room 3.0:这次不是升级,是重来
android·前端·google
漫随流水4 小时前
旅游推荐系统(view.py)
前端·数据库·python·旅游
踩着两条虫5 小时前
VTJ.PRO 核心架构全公开!从设计稿到代码,揭秘AI智能体如何“听懂人话”
前端·vue.js·ai编程
jzlhll1236 小时前
kotlin Flow first() last()总结
开发语言·前端·kotlin
蓝冰凌7 小时前
Vue 3 中 defineExpose 的行为【defineExpose暴露ref变量】详解:自动解包、响应性与实际使用
前端·javascript·vue.js
奔跑的呱呱牛7 小时前
generate-route-vue基于文件系统的 Vue Router 动态路由生成工具
前端·javascript·vue.js
柳杉8 小时前
从动漫水面到赛博飞船:这位开发者的Three.js作品太惊艳了
前端·javascript·数据可视化