JS中的闭包(closures)一种强大但易混淆的概念

JavaScript 中的闭包(closures)被认为是一种既强大又易混淆的概念。闭包允许函数访问其外部作用域的变量,即使外部函数已执行完毕,这在状态维护和回调函数中非常有用。但其复杂性可能导致开发者的误解,尤其在变量捕获和作用域管理上。本文将详细探讨闭包的定义、强大之处、易混淆的原因,并结合实际案例和最佳实践,为读者提供全面指导

闭包是 JavaScript 中一个函数与其外部作用域的组合,即使外部函数已执行完毕,内部函数仍能访问外部函数的变量。这使得闭包在状态维护(如计数器)和回调函数(如事件处理)中非常强大。

在 JavaScript 语言中,闭包一直被任务是一个既强大又易混淆的概念

什么是闭包?

闭包简单来说就是 “函数与其引用的词法环境的组合”。当一个函数被定义时,他的作用域链就被确定下来,即使这个函数在定义时的作用域已经销毁,闭包依然能够让函数访问这些被捕获的变量。

简单定义

闭包就是一个函数以及创建该函数时所处的词法作用域,确保函数能够持续访问这些变量。

备注:这种特性使得函数不仅仅是一个代码块,而是携带了其执行上下文的完整信息。

根据 MDN Web Docs: Closures,闭包是“一个函数与其定义时的词法环境(lexical environment)的组合”。换句话说,闭包允许内部函数访问外部函数的变量,即使外部函数已返回。

在 JavaScript 中,函数是第一类公民,可以作为参数传递或返回,这使得闭包成为语言的重要特性。闭包的形成依赖于词法作用域,即函数的作用域由其定义位置决定,而不是调用位置。

function outer() {  
    var a = 1;  
    function inner() {  
        console.log(a); // 输出 1  
    }  
    return inner;  
}  
const fn = outer();  
fn(); // 输出 1  

这里,inner 函数在 outer 返回后仍能访问 a,这就是闭包。

闭包的强大之处

闭包的强大在于其能维护状态和捕获外部变量,适用于多种场景。以下是两个主要用例:

1. 状态维护

闭包允许创建具有记忆功能的函数,例如计数器:

function makeCounter() {  
    var count = 0;  
    return function() {  
        count++;  
        console.log(count);  
    }  
}  
const counter = makeCounter();  
counter(); // 输出 1  
counter(); // 输出 2  

这里,counter 函数记住 count 的值,即使 makeCounter 已返回。这是闭包维护状态的典型例子,适合实现私有变量或计数器。

2. 事件处理和回调

闭包在事件驱动编程中非常有用,特别是在需要捕获当前状态的场景。例如:

for (var i = 0; i < 3; i++) {  
    (function(index) {  
        document.getElementById('button' + index).onclick = function() {  
            console.log('点击了按钮 ' + index);  
        }  
    })(i);  
}  

这里,使用立即执行函数(IIFE)创建闭包,确保每个按钮的点击事件处理程序捕获正确的 index 值。如果用 var i,所有按钮会输出 3,这是常见误解(详见后文)。

3. 封装

闭包还可用于创建私有变量,实现封装:

function Person(name) {  
    var privateName = name;  
    return {  
        getName: function() {  
            return privateName;  
        },  
        setName: function(newName) {  
            privateName = newName;  
        }  
    }  
}  
const person = Person('John');  
console.log(person.getName()); // John  
person.setName('Jane');  
console.log(person.getName()); // Jane  

这里,privateName 被封装在闭包中,仅通过 getName 和 setName 方法访问,这是 JavaScript 中实现私有成员的经典方式。

闭包的易混淆原因

尽管闭包强大,但其复杂性可能导致开发者的误解。以下是主要原因:

1. 变量按引用捕获

闭包捕获的是变量的引用而非值,这可能导致意外行为,尤其在循环中。例如:

for (var i = 0; i < 3; i++) {  
    document.getElementById('button' + i).onclick = function() {  
        console.log('点击了按钮 ' + i); // 所有按钮输出 3  
    };  
}  

这里,所有按钮点击时输出 3,因为 var i 的作用域是函数级别的,闭包捕获的是同一个 i,在循环结束后值为 3。这是 Stack Overflow: Common pitfalls with JavaScript closures 中提到的常见问题。

解决方法是使用 let(ES6 引入,块级作用域)或立即执行函数:

for (let i = 0; i < 3; i++) {  
    document.getElementById('button' + i).onclick = function() {  
        console.log('点击了按钮 ' + i); // 每个按钮输出正确值  
    };  
}  

或:

for (var i = 0; i < 3; i++) {  
    (function(index) {  
        document.getElementById('button' + index).onclick = function() {  
            console.log('点击了按钮 ' + index);  
        }  
    })(i);  
}  

这确保每个闭包捕获不同的值。

2. 作用域理解困难

开发者可能不熟悉 JavaScript 的词法作用域,导致误解哪些变量可访问。例如:

function outer() {  
    var a = 1;  
    function inner() {  
        var a = 2;  
        console.log(a); // 输出 2  
    }  
    inner();  
    console.log(a); // 输出 1  
}  
outer();  

这里,inner 有自己的 a,遮蔽了外部的 a,这是词法作用域的体现。初学者可能误以为 inner 会访问外部的 a,这是 SitePoint: Understanding JavaScript Closures: Common Mistakes 中提到的误解。

3. 内存管理

闭包可能保留变量,造成内存泄漏,尤其当闭包引用大型对象时。例如:

function createLargeData() {  
    var largeArray = new Array(1000000).fill(0);  
    return function() {  
        console.log(largeArray.length);  
    };  
}  
const fn = createLargeData();  
fn(); // largeArray 仍被引用,内存未释放  

这里,largeArray 被闭包引用,即使 createLargeData 返回后,内存仍占用。现代 JavaScript 引擎(如 V8)有垃圾回收机制,但长期保留闭包可能影响性能,这是 Medium: JavaScript Closures: Common Misconceptions 中提到的潜在问题。

词法作用域与执行上下文

理解闭包需要先掌握的两个重要概念

词法作用域
  • 定义:词法作用域是指变量的作用域在代码编写时就已经确定,而不是在运行时动态决定的。也就是说,函数内部能访问哪些外部变量由函数定义时的位置决定。

function outer(){
    let a = 10;
    function inner() {
        console.log(a) // inner 函数可以访问 outer 中的变量a
    }
    inner()
}
outer()

备注:由于词法作用域的存在,函数在被定义时就已经携带了它所能访问的变量信息,这为闭包的形成奠定了基础

 

执行上下文
  • 定义:执行上下文是 JavaScript 中代码执行时所处的环境。它包含了变量对象、作用域链、this指向等信息。
  • 作用:当一个函数被调用时,会创建一个新的执行上下文,并将其压入执行栈中。闭包正是利用了这些执行上下文中的变量。

备注:当函数返回后,其执行上下文通常会被销毁,但如果返回的函数仍然引用了这个上下文中的变量,那么这些变量就不会被垃圾回收,形成闭包。

 

闭包的实现原理

闭包的核心在于: 函数内部定义的子函数可以访问外部函数中的局部变量,即使外部函数已经执行完毕。

实现过程
  • 定义一个函数,并在其中声明局部变量。
  • 在该函数内部定义另一个函数,该内部函数可以访问外部函数的变量。
  • 将内部函数返回到外部,使其在外部执行时仍然能够访问原有的变量。
示例代码
function createCount() {
    let count = 0; // 外部函数的局部变量
    return function() { // 返回的内部函数构成闭包
        count++
        console.log(count)
    }
}

const counter = createCount()
counter() // 1
counter() // 2

备注:上面的例子中,内部函数一直可以访问createCount中的变量count,即使 createCount 已经执行完毕。这就是闭包的实际表现。

 

闭包的应用场景

数据封装和私有变量

闭包可以用来模拟私有变量,实现数据封装。

function Person(name) {
    let _name = name; // 私有变量
    
    return {
        getName: function() {
            return _name;
        },
        setName: function(newName) {
            _name = newName;
        }
    }
}

const person = Person('Alice');
console.log(person.getName()); // Alice
person.setName('Bob');
console.log(person.getName()); // Bob

备注:通过闭包,可以避免直接访问对象内部的私有数据,只能 通过特定的方法进行操作。

 

创建函数工厂

利用闭包可以创建灵活的工厂函数,生成带有特定状态的函数实例。

function makeAdder(x) {
    return function(y) {
        return x + y;
    }
}

const add5 = makeAdder(5);
console.log(add5(10)); // 15

备注:工厂函数利用闭包保存了参数 x 的值,使得返回的函数能够“记住”这个值

 

常见问题和注意事项

内存泄漏

由于闭包会持有外部函数的变量引用,若使用不当,可能导致内存无法及时释放。

  • 解决办法
    • 避免在不必要时创建过多闭包。
    • 在合适的时机手动清除闭包中不再需要的变量引用。
循环中的闭包问题

在循环中使用闭包时,由于变量捕获可能导致意外的结果。传统使用 var 定义变量时,所有闭包共享同一个变量。

for (var i = 0; i < 3; i++){
    setTimeout(function() {
        console.log(i); // 循环结束后,i 的值为3,因此每次输出 3
    }, 100)
}

 

解决方法:

  • 使用 let 替换 var,因为let块级作用域使每次循环都有独立的变量。

    for (let i = 0; i < 3; i++){
        setTimeout(function() {
            console.log(i); // 0, 1, 2
        }, 100)
    }
  • 使用 IIFE (立即调用函数表达式)来捕获变量

    for (var i = 0; i < 3; i++) {
        (function(j){
            setTimeout(function() {
                console.log(j); // 0, 1, 2
            }, 100)
        })(i)
    }

备注:在使用闭包时,务必注意作用域问题,合理选择变量声明方式,避免意外捕获相同的变量。

 

常见误解
  1. 所有函数都是闭包:技术上,每个函数都有闭包(函数与其词法环境),但通常我们指捕获外部变量的函数为闭包。
  2. 闭包只用于状态维护:闭包不仅用于状态,还用于事件处理、封装等场景。
  3. 闭包不会影响性能:不当使用可能导致内存泄漏,需注意资源释放。
最佳实践
  • 使用 let 或 const:避免 var 的函数级作用域问题,确保闭包捕获正确值。
  • 避免不必要的闭包:若无状态需求,尽量不使用闭包,减少内存占用。
  • 监控内存使用:使用开发者工具(如 Chrome DevTools)检查内存泄漏,及时优化。

其他相关知识点

高阶函数
  • 定义:高阶函数是指能够接受函数作为参数或返回函数的函数。闭包常用于实现高阶函数,使得函数可以保存和操作状态。

  • 示例

    function multiplier(factor) {
        return function(number) {
            return number + factor;
        }
    }
    
    const double = multiplier(2);
    console.log(double(2)); // 10

 

IIFE(立即调用函数表达式)
  • 用途:IIFE 可以创建一个独立的作用域,常用于避免变量污染全局作用域,并借助闭包保存局部变量。

  • 示例

    (function() {
        let message == 'Hello, World!';
        console.log(message);
    })();

 

块级作用域与 let/const

区别: var声明的变量具有函数作用域,而 letconst则具有块级作用域,这在使用闭包时尤为重要,能避免因变量共享而产生的问题。

备注:掌握不同变量声明方式的作用域规则,是正确使用闭包的前提

结论

在前端开发日益复杂的今天,闭包的普及反映了 JavaScript 功能性编程的趋势。就像年轻人热衷“不好好说话”的梗文化,开发者也在追求“偷懒的艺术”——通过闭包简化代码,减少全局变量的使用,体现了现代开发对效率和模块化的追求。尤其在 React、Vue 等框架中,闭包常用于钩子函数和状态管理,成为开发者的必备技能。

JavaScript 闭包是一种强大但易混淆的概念,允许函数访问外部作用域的变量,适合状态维护和回调函数。其复杂性源于变量按引用捕获和作用域理解困难,需注意循环和内存管理。意料之外的是,变量捕获方式可能导致意外行为,需用 let 或立即执行函数解决。掌握这些技巧,开发者能更高效地利用闭包,构建健壮的应用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/983898.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

根据指定 Excel 模板将 Excel 明细数据生成新的 Excel 文档

在日常工作中&#xff0c;我们常常需要生成 Excel 文档&#xff0c;例如将 Excel 数据 自动转换为独立的 Excel 文件。在这种需求下&#xff0c;如何高效地将 Excel 表格 中的每一条数据生成一个对应的 Excel 文档&#xff0c;并且让文档中的内容根据 Excel 数据动态变化&#…

前端杂的学习笔记

什么是nginx Nginx (engine x) 是一个高性能的HTTP和反向代理web服务器 Nginx是一款轻量级的Web 服务器/反向代理服务器&#xff0c;处理高并发能力是十分强大的&#xff0c;并且支持热部署&#xff0c;启动简单&#xff0c;可以做到7*24不间断运行 正代和反代 学习nginx&a…

C#控制台应用程序学习——3.8

一、语言概述 1、平台相关性 C# 主要运行在.NET 平台上。.NET 提供了一个庞大的类库&#xff0c;C# 程序可以方便地调用这些类库来实现各种功能&#xff0c;如文件操作、数据库访问、网络通信等。 2、语法风格 C# 的语法与 C、C 和 Java 有一定的相似性。例如&#xff0c;它使用…

【数据结构与算法】Java描述:第三节:栈与队列

一、 栈(Stack) 1.1 概念 栈&#xff1a; 一种特殊的线性表&#xff0c;其只允许在固定的一端进行插入和删除元素操作。 进行数据插入和删除操作的一端称为栈顶&#xff0c;另一端称为栈底。栈中的数据元素遵守先进后出的原则。 压栈&#xff1a;栈的插入操作叫做进栈/压栈/…

MATLAB中movmax函数用法

目录 语法 说明 示例 向量的中心移动最大值 向量的尾部移动最大值 矩阵的移动最大值 包括缺失值的移动最大值 基于样本点计算移动最大值 仅返回满窗最大值 movmax函数的功能是求得移动最大值。 语法 M movmax(A,k) M movmax(A,[kb kf]) M movmax(___,dim) M mov…

linux学习(五)(服务器审查,正常运行时间负载,身份验证日志,正在运行的服务,评估可用内存)

服务器审查 在 Linux 中审查服务器的过程包括评估服务器的性能、安全性和配置&#xff0c;以确定需要改进的领域或任何潜在问题。审查的范围可以包括检查安全增强功能、检查日志文件、审查用户帐户、分析服务器的网络配置以及检查其软件版本。 Linux 以其稳定性和安全性而闻名…

Java中的栈的实现

Java中的栈的实现--双端队列&#xff08;Deque&#xff09; 1. 解释代码2.为什么不用 Stack<Character>&#xff1f;3.使用示例4.Deque 的常用方法5.LinkedList<> 和 ArrayDeque<> 的区别和联系6. 总结 1. 解释代码 Deque<Character> st new ArrayDe…

【Andrej Karpathy 神经网络从Zero到Hero】--2.语言模型的两种实现方式 (Bigram 和 神经网络)

目录 统计 Bigram 语言模型质量评价方法 神经网络语言模型 【系列笔记】 【Andrej Karpathy 神经网络从Zero到Hero】–1. 自动微分autograd实践要点 本文主要参考 大神Andrej Karpathy 大模型讲座 | 构建makemore 系列之一&#xff1a;讲解语言建模的明确入门&#xff0c;演示…

Go_zero学习笔记

<!-- go-zero --> 安装配置 go-zero_github go-zero文档 go install github.com/zeromicro/go-zero/tools/goctllatest goctl --version // goctl version 1.7.2 windows/amd64 gopath/bin/会生成goctl的执行进程(%GOPATH%\bin设置到path环境变量中) 安装protoc&pr…

nats jetstream server code 分析

对象和缩写 jetstream导入两个对象&#xff1a;stream and consumer&#xff0c;在stream 之上构造jetstreamapi。在nats代码中&#xff0c;以下是一些常见的缩写 1.mset is stream 2.jsX is something of jetstream 3.o is consumer 代码分析 对于producer &#xff0c;发送…

高效编程指南:PyCharm与DeepSeek的完美结合

DeepSeek接入Pycharm 前几天DeepSeek的充值窗口又悄悄的开放了&#xff0c;这也就意味着我们又可以丝滑的使用DeepSeek的API进行各种辅助性工作了。本文我们来聊聊如何在代码编辑器中使用DeepSeek自动生成代码。 注&#xff1a;本文适用于所有的JetBrains开发工具&#xff0c…

豆包大模型 MarsCode AI 刷题专栏 004

007.创意标题匹配问题 难度&#xff1a;易 问题描述 在广告平台中&#xff0c;为了给广告主一定的自由性和效率&#xff0c;允许广告主在创造标题的时候以通配符的方式进行创意提交。线上服务的时候&#xff0c;会根据用户的搜索词触发的 bidword 对创意中的通配符&#xff…

Blueprint —— Blueprint Editor(二)

目录 一&#xff0c;Blueprint Header View 二&#xff0c;Blueprint Bookmarks 三&#xff0c;Blueprint Editor Defaults Tab 获取类默认值 一&#xff0c;Blueprint Header View Blueprint Header View 可将虚幻引擎蓝图类和蓝图结构体转换为C代码&#xff1b;在转换过程…

JVM组成面试题及原理

Java Virtual Machine&#xff08;JVM&#xff09;是Java程序的运行环境&#xff08;java二进制字节码的运行环境&#xff09; 好处&#xff1a; 一次编写&#xff0c;到处运行自动内存管理&#xff0c;垃圾回收机制 JVM由哪些部分组成&#xff0c;运行流程是什么&#xff1f;…

解决在windows中docker拉取镜像出现的问题

解决在windows中docker拉取镜像出现的问题 docker pull minio/minio 出现报错&#xff1a; Error response from daemon: Get "https://registry-1.docker.io/v2/": net/http: request canceled while waiting for connection (Client.Timeout exceeded while await…

MySQL基本建表操作

目录 1&#xff0c;创建数据库db_ck 1.1创建表 1.2 查看创建好的表 2,创建表t_hero 2.1 先进入数据库Db_Ck 2.1.1 这里可以看是否进入数据库: 2.2 创建表t_Hero 2.2.1 我们可以先在文本文档里面写好然后粘贴进去&#xff0c;因为直接写的话&#xff0c;错了要重新开始 …

使用Arduino和ESP8266进行基于物联网的垃圾箱监控

使用 Arduino 和 ESP8266 的基于 IOT 的垃圾箱监控系统 在这个 DIY 中,我们将制作一个基于 IOT 的垃圾箱/垃圾监控系统,该系统将通过网络服务器告诉我们垃圾桶是空的还是满的,并且您可以通过互联网从世界任何地方了解“垃圾桶”或“垃圾箱”的状态。它将非常有用,可以安装…

【Academy】HTTP 请求走私 ------ HTTP request smuggling

HTTP 请求走私 ------ HTTP request smuggling 1. 什么是 HTTP 请求走私&#xff1f;2. HTTP 请求走私漏洞是如何产生的&#xff1f;3. 如何执行 HTTP 请求走私攻击3.1 CL.TE 漏洞3.2 TE.CL 漏洞3.3 TE.TE 行为&#xff1a;混淆 TE 标头 4. 如何识别和确认 HTTP 请求走私漏洞4.…

元脑服务器的创新应用:浪潮信息引领AI计算新时代

浪潮信息的元脑 R1 服务器现已全面支持开源框架 SGLang&#xff0c;能够在单机环境下实现 DeepSeek 671B 模型的高并发性能&#xff0c;用户并发访问量超过1000。通过对 SGLang 最新版本的深度适配&#xff0c;元脑 R1 推理服务器在运行高性能模型时&#xff0c;展现出卓越的处…

蓝桥备赛(13)- 链表和 list(下)

一、动态链表 - list (了解) new 和 delete 是非常耗时的操作 在算法比赛中&#xff0c;一般不会使使用 new 和 delete 去模拟实现一个链表。 而且STL 里面的 list 的底层就是动态实现的双向循环链表&#xff0c;增删会涉及 new 和 delete&#xff0c;效率不高&#xff0c;竞赛…