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

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

相关推荐
维生素C++18 分钟前
【可变模板参数】
linux·服务器·c语言·前端·数据结构·c++·算法
vah10124 分钟前
python队列操作
开发语言·前端·python
项目題供诗24 分钟前
尚品汇-H5移动端整合系统(五十五)
java·服务器·前端
DT——6 小时前
Vite项目中eslint的简单配置
前端·javascript·代码规范
学习ing小白8 小时前
JavaWeb - 5 - 前端工程化
前端·elementui·vue
真的很上进8 小时前
【Git必看系列】—— Git巨好用的神器之git stash篇
java·前端·javascript·数据结构·git·react.js
胖虎哥er8 小时前
Html&Css 基础总结(基础好了才是最能打的)三
前端·css·html
qq_278063719 小时前
css scrollbar-width: none 隐藏默认滚动条
开发语言·前端·javascript
.ccl9 小时前
web开发 之 HTML、CSS、JavaScript、以及JavaScript的高级框架Vue(学习版2)
前端·javascript·vue.js
小徐不会写代码9 小时前
vue 实现tab菜单切换
前端·javascript·vue.js