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

news2025/3/10 12:56:09

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 或立即执行函数解决。掌握这些技巧,开发者能更高效地利用闭包,构建健壮的应用。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Element使用

Element(美化网页&#xff09; ElementUI的使用注意事项&#xff1a; Element.ui的使用基于Vue环境&#xff0c;于是Element相关组件的使用必须放在Vue对象绑定的视图中去 ElementUI的JS库的引入必须放在vue.js库的后面 <!-- 引入样式 --><link rel"styleshee…

基于YOLO11深度学习的电瓶车进电梯检测与语音提示系统【python源码+Pyqt5界面+数据集+训练代码】

《------往期经典推荐------》 一、AI应用软件开发实战专栏【链接】 项目名称项目名称1.【人脸识别与管理系统开发】2.【车牌识别与自动收费管理系统开发】3.【手势识别系统开发】4.【人脸面部活体检测系统开发】5.【图片风格快速迁移软件开发】6.【人脸表表情识别系统】7.【…

R语言的基础命令及实例操作

> T & F [1] FALSE > T & T [1] TRUE > T | F [1] TRUE > F | F [1] FALSE > a <- c(T,F,T) > b <- c(F,F,T) > a & b [1] FALSE FALSE TRUE > a | b [1] TRUE FALSE TRUE 在 R 中&#xff0c;大小写是敏感的&#xff0c;也就是说…

知识蒸馏综述Knowledge Distillation: A Survey解读

论文链接&#xff1a;Knowledge Distillation: A Survey 摘要&#xff1a;近年来&#xff0c;深度神经网络在工业界和学术界都取得了成功&#xff0c;尤其是在计算机视觉任务方面。深度学习的巨大成功主要归功于它能够扩展以对大规模数据进行编码&#xff0c;并且能够处理数十…

第十五届蓝桥杯省赛电子类单片机学习过程记录(客观题)

客观试题: 01.典型的BUCK电源电路包含哪些关键器件(ABCD) A. 电容 B. 二极管 C. 电感 D. MOSFET 解析: 典型的 BUCK 电源电路是一种降压型的直流-直流转换电路,它包含以下关键器件: A.电容:电容在电路中起到滤波的作用。输入电容用于平滑输入电压的波动,减少电源噪声对…

【c++】平移字符串

说明 实现字符串的左移与右移 示例代码 #include <iostream> #include <string> using namespace std;int main() {string str1 "12345";//左移2位string str2 str1.substr(2) str1.substr(0, 2);cout << str2 << endl;//右移2位&…

为什么DDPG需要目标网络而A2C不需要?

在强化学习中&#xff0c;DDPG需要目标网络而A2C不需要的主要原因在于算法架构、更新方式和目标稳定性需求的差异&#xff1a; Q值估计的稳定性需求不同 DDPG的Critic网络需要估计状态-动作值函数 Q ( s , a ) Q(s,a) Q(s,a)&#xff0c;其目标值的计算涉及下一个状态的最大Q值…

蓝桥杯 C++ b组 统计子矩阵深度解析

题目大意&#xff1a;给定一个 NM 的矩阵 A&#xff0c;请你统计有多少个子矩阵 (最小11&#xff0c;最大NM) 满足子矩阵中所有数的和不超过给定的整数 K&#xff1f; 前言&#xff1a;这题很容易想到二维前缀和优化&#xff0c;然后枚举子矩阵&#xff0c;但这样时间复杂度为…

YOLOv12本地部署教程——42%速度提升,让高效目标检测触手可及

YOLOv12 是“你只看一次”&#xff08;You Only Look Once, YOLO&#xff09;系列的最新版本&#xff0c;于 2025 年 2 月发布。它引入了注意力机制&#xff0c;提升了检测精度&#xff0c;同时保持了高效的实时性能。在保持速度的同时&#xff0c;显著提升了检测精度。例如&am…

认识Event Loop【1】

前言 这应该是一个系列文章&#xff0c;因为我觉得Event Loop&#xff08;事件循环&#xff09;是一件很抽象也很重要的一个机制。eventloop这个知识点处于非常杂糅的位置&#xff0c;和很多其他知识&#xff0c;如运行时、浏览器、渲染流程、数据结构、线程等等&#xff0c;也…

《Linux栈破坏了,如何还原》

【栈破坏导读】栈破坏有了解过吗&#xff1f;何为栈破坏&#xff0c;栈破坏了&#xff0c;程序会立刻引发崩溃&#xff0c;我们通过gdb去调试coredump&#xff0c;栈被破坏的栈帧是没法被恢复的&#xff0c;这也给我们调试程序带来很大的困难&#xff0c;那如何还原栈破坏的第一…

环形链表问题的探究与代码实现

在数据结构与算法的学习中&#xff0c;环形链表是一个经典的问题。它不仅考察对链表这种数据结构的理解&#xff0c;还涉及到指针操作和逻辑推理。本文将结合代码和图文&#xff0c;深入分析如何判断链表中是否有环以及如何找到环的入口点。 目录 一、判断链表中是否有环 …

【CSS3】筑基篇

目录 复合选择器后代选择器子选择器并集选择器交集选择器伪类选择器 CSS 三大特性继承性层叠性优先级 背景属性背景色背景图背景图平铺方式背景图位置背景图缩放背景图固定背景复合属性 显示模式显示模式块级元素行内元素行内块元素 转换显示模式 结构伪类选择器结构伪类选择器…

React:类组件(上)

kerwin老师我来了 类组件的创建 class组件&#xff0c;js里的类命名首字符大写&#xff0c;类里面包括构造函数&#xff0c;方法 组件类要继承React.Component才有效 必须包含render方法 import React from react class App extends React.Component{render() {return <…

Spring Cloud之注册中心之Nacos的使用

目录 Naacos 服务注册/服务发现 引⼊Spring Cloud Alibaba依赖 引入Nacos依赖 引入Load Balance依赖 配置Nacos地址 服务端调用 启动服务 Naacos Nacos是Spring Cloud Alibaba的组件, Spring Cloud Alibaba遵循Spring Cloud中定义的服务注册, 服务发现规范. 因此使⽤Na…

字符串相乘——力扣

给定两个以字符串形式表示的非负整数 num1 和 num2&#xff0c;返回 num1 和 num2 的乘积&#xff0c;它们的乘积也表示为字符串形式。 注意&#xff1a;不能使用任何内置的 BigInteger 库或直接将输入转换为整数。 示例 1: 输入: num1 "2", num2 "3" …

基于OpenCV的车牌识别系统(源码+论文+部署教程)

运行环境 基于OpenCV的车牌识别系统运行环境如下&#xff1a; • Python: ≥ 3.5 • OpenCV: ≥ 4.0 • IDE工具&#xff1a;Visual Studio Code&#xff08;可自行选择&#xff09; • 技术栈&#xff1a;Python OpenCV Tkinte 主要功能 基于OpenCV的车牌识别系统主要…

MySQL:CRUD(增删查改)

目录 一、准备工作 二、Create 新增 1、语法 2、单行数据全列插入 3、单行数据指定列插入 4、多行数据指定列插入 5、多行数据全列插入 三、Retrieve 检索 1、语法 2、全列查询 3、指定列查询 4、查询字段为表达式 &#xff08;1&#xff09;常量表达式 &…

【git】【网络】【项目配置运行】HTTP 协议的微型简易 Web 服务器---tinyEasyMuduoWebServer

【git】【网络】【项目配置运行】HTTP 协议的微型简易 Web 服务器—tinyEasyMuduoWebServer csdn项目&#xff1a; 原文链接&#xff1a;https://blog.csdn.net/weixin_45178775/article/details/122257814 github链接&#xff1a;https://github.com/wyewyewye/tinyEasyMuduo…

Python入门———条件、循环

目录 语句 顺序语句 条件语句 缩进和代码块 判断年份是否是闰年 空语句 pass 循环 while 循环 求5的阶乘&#xff1a; 求1&#xff01;2&#xff01;3&#xff01;4&#xff01;5&#xff01; for循环 打印1-10 打印2&#xff0c;4&#xff0c;6&#xff0c;8&#x…