《作用域大冒险:从闭包到内存泄漏的终极探索》

“爱自有天意,天有道自不会让有情人分离”
大家好,关于闭包问题其实实际上是js作用域的问题,那么js有几种作用域呢?
| 作用域类型 | 关键字/场景 | 作用域范围 | 示例 |
|---|---|---|---|
| 全局作用域 | var(无声明) | 整个程序 | var x = 10; |
| 函数作用域 | var 在函数内 | 函数内部 | function foo() { var x; } |
| 块级作用域 | let、const | {} 代码块内 | if (true) { let x; } |
| 模块作用域 | ES6 模块 | 单个模块文件 | export const x = 1; |
| 词法作用域 | 函数定义时 | 定义时的外层作用域链 | 闭包 |
我们常见的就是 全局作用域,函数作用域和块级作用域了。
闭包叫做词法作用域,我没听说过这个词,总而言之,闭包是一个作用域问题
什么是闭包?
闭包(Closure)是 JavaScript 中的一个核心概念,它指的是 函数能够记住并访问其定义时的作用域(词法环境),即使该函数在其作用域之外执行。
用人话来讲就是:闭包是可以访问到另一个函数作用域中变量的函数
在循环嵌套的函数结构中,闭包就很容易理解了。内部函数可以访问到外部函数中的变量,但是外部函数不能访问到内部函数中的变量。
我来举一个例子:
function outerFunction(outerParam) {// 外部函数的变量let outerVar = "我是外部变量";const outerConst = "我是外部常量";function innerFunction(innerParam) {// 内部函数的变量let innerVar = "我是内部变量";// 内部函数可以访问:// 1. 自己的变量console.log("内部函数访问自己的变量:", innerVar);console.log("内部函数访问自己的参数:", innerParam);// 2. 外部函数的变量和参数console.log("内部函数访问外部变量:", outerVar);console.log("内部函数访问外部常量:", outerConst);console.log("内部函数访问外部参数:", outerParam);return innerVar;}console.log("\n----- 分割线 -----\n");// 外部函数尝试访问内部函数的变量(会失败)console.log("外部函数可以访问自己的变量:", outerVar);console.log("外部函数可以访问自己的参数:", outerParam);// 下面这行如果取消注释会报错// console.log("外部函数无法访问内部变量:", innerVar); // ReferenceError: innerVar is not defined// 调用内部函数const result = innerFunction("内部参数");console.log("只能通过内部函数的返回值来获取内部变量:", result);return innerFunction;
}// 测试
const innerFn = outerFunction("外部参数");
console.log("\n----- 分割线 -----\n");
innerFn("新的内部参数");
输出结果:
----- 分割线 -----外部函数可以访问自己的变量: 我是外部变量
外部函数可以访问自己的参数: 外部参数
内部函数访问自己的变量: 我是内部变量
内部函数访问自己的参数: 内部参数
内部函数访问外部变量: 我是外部变量
内部函数访问外部常量: 我是外部常量
内部函数访问外部参数: 外部参数
只能通过内部函数的返回值来获取内部变量: 我是内部变量----- 分割线 -----内部函数访问自己的变量: 我是内部变量
内部函数访问自己的参数: 新的内部参数
内部函数访问外部变量: 我是外部变量
内部函数访问外部常量: 我是外部常量
内部函数访问外部参数: 外部参数
这个代码展示的是:
-
内部函数可以访问:
- 自己的变量(innerVar)和参数(innerParam)
- 外部函数的变量(outerVar)、常量(outerConst)和参数(outerParam)
-
外部函数只能访问:
- 自己的变量(outerVar)和参数(outerParam)
- 无法直接访问内部函数的变量(innerVar)
- 只能通过内部函数的返回值来间接获取内部变量的值
这就是所谓的"作用域链",内部函数可以向上访问外部作用域的变量,但外部作用域不能访问内部作用域的变量。
闭包能干什么?
闭包能干的事情有:变量私有化、回调函数、函数柯里化。
变量私有化
什么是变量私有化?
变量私有化是一种编程技术,目的是限制变量的访问范围,使其只能在特定的作用域或模块内被访问和修改,外部代码无法直接操作。这样可以提高代码的安全性、可维护性,并减少命名冲突的风险。
通过闭包实现一下变量私有化
我们来做一个计数器案例,外部不能修改count,只能通过 increment() 和 getCount() 操作。
function createCounter() {let count = 0; // 私有变量,外部无法直接访问return {increment() {count++;},getCount() {return count;},};
}const counter = createCounter();
counter.increment();
console.log(counter.getCount()); // 1
console.log(count); // 报错:count is not defined(无法直接访问私有变量)
我们利用闭包创建了一个私有变量count,无法在外部访问,只有通过我们的increment() 和 getCount() 操作才能操作和访问。
回调函数
回调函数想必就不用介绍了,在任何语言中都有出现和应用。
闭包可以让回调函数记住并访问其定义时的作用域变量,即使回调在异步操作(如
setTimeout、fetch、事件监听)中被调用。
介绍一个例子:
setTimeout 回调
问题:直接使用循环变量 i 会导致所有回调输出相同的值(var 没有块级作用域)。
解决:用闭包保存每次循环的 i 值。
// ❌ 错误写法(输出 3 个 3)
for (var i = 0; i < 3; i++) {setTimeout(function() {console.log(i); // 输出 3, 3, 3}, 100);
}// ✅ 正确写法(闭包保存 i 的值)
for (var i = 0; i < 3; i++) {(function(j) { // 立即执行函数(IIFE)创建闭包setTimeout(function() {console.log(j); // 输出 0, 1, 2}, 100);})(i); // 传入当前 i 的值
}
监听事件中的闭包
function setupButtons() {const buttons = document.querySelectorAll('button');for (var i = 0; i < buttons.length; i++) {(function(index) { // 闭包保存当前按钮的索引let count = 0; // 每个按钮独立的计数器buttons[index].addEventListener('click', function() {count++;console.log(`按钮 ${index} 被点击了 ${count} 次`);});})(i);}
}setupButtons();
我们发现在回调函数场景中闭包的作用很多是帮我们留下或者说是记住作用域变量,可以让我们的逻辑更加简单。
函数柯里化
wow,好高级的词!
什么是函数柯里化
函数柯里化(Currying)是一种将 多参数函数 转换为 一系列单参数函数 的技术。
它的核心思想是:每次只接受一个参数,并返回一个新函数,直到所有参数收集完毕,才执行最终计算。
总而言之就是:分布传参。
刚才我们在回调函数中了解到:“我们发现在回调函数场景中闭包的作用很多是帮我们留下或者说是记住作用域变量,可以让我们的逻辑更加简单。”
那么:函数柯里化是指将一个多参数函数转换为一系列单参数函数的过程。那么闭包刚好利用它能记住函数定义时的作用域这一特点就可以实现柯里化;
用闭包做函数柯里化
简单例子:
// 普通函数(3个参数)
function sum(a, b, c) {return a + b + c;
}// 手动柯里化(闭包实现)
function curriedSum(a) {return function(b) {return function(c) {return a + b + c;};};
}// 调用方式
console.log(curriedSum(1)(2)(3)); // 6
闭包带来的危害
1. 内存泄漏(Memory Leaks)
问题描述
闭包会长期持有外部函数的变量,阻止垃圾回收(GC),导致内存无法释放。
示例
function createHeavyObject() {const bigData = new Array(1000000).fill("X"); // 占用大量内存的变量return function() {console.log(bigData.length); // 闭包引用 bigData,即使外部函数执行完毕};
}const holdClosure = createHeavyObject(); // bigData 无法被回收!
解决方法
- 在不需要闭包时手动解除引用:
holdClosure = null; // 释放闭包持有的内存 - 避免在闭包中保存不必要的变量(如 DOM 元素、大对象)。
2. 性能损耗(Performance Overhead)
问题描述
- 闭包会创建额外的作用域链,访问外部变量比访问局部变量稍慢。
- 在频繁调用的函数(如动画、滚动事件)中使用闭包可能导致性能下降。
示例
// 每次触发 scroll 都会访问闭包变量
window.addEventListener("scroll", function() {const cached = heavyCompute(); // 闭包可能持有 heavyCompute 的结果console.log(cached);
});
解决方法
- 对于高频操作,尽量使用局部变量而非闭包变量。
- 用
debounce/throttle限制触发频率。
3. 意外的变量共享(Unexpected Shared State)
问题描述
循环中创建的闭包可能共享同一个变量(尤其是用 var 时)。
示例
// ❌ 错误写法:所有按钮都输出 3
for (var i = 0; i < 3; i++) {setTimeout(function() {console.log(i); // 输出 3, 3, 3(i 是共享的)}, 100);
}// ✅ 正确写法:用 IIFE 或 let 隔离变量
for (let i = 0; i < 3; i++) {setTimeout(function() {console.log(i); // 输出 0, 1, 2}, 100);
}
解决方法
- 使用
let/const替代var(块级作用域)。 - 用 IIFE(立即执行函数)隔离变量:
for (var i = 0; i < 3; i++) {(function(j) {setTimeout(function() {console.log(j); // 正确输出 0, 1, 2}, 100);})(i); }
4. 调试困难(Debugging Challenges)
问题描述
闭包的作用域链可能让变量来源难以追踪,增加调试复杂度。
示例
function outer() {const secret = 42;return function inner() {debugger; // 在这里查看作用域链,可能有多层闭包console.log(secret);};
}
const mystery = outer();
mystery();
解决方法
- 在 Chrome DevTools 中使用 Scope 面板查看闭包变量。
- 避免过度嵌套闭包,保持函数简洁。
5. 闭包与 this 的混淆
问题描述
闭包中的 this 可能丢失预期指向(尤其是嵌套函数中)。
示例
const obj = {name: "Alice",greet: function() {return function() {console.log(this.name); // ❌ 输出 undefined(this 指向全局或 undefined)};}
};
obj.greet()(); // 调用内部函数
解决方法
- 使用箭头函数(继承外层
this):greet: function() {return () => console.log(this.name); // ✅ 正确输出 "Alice" } - 提前绑定
this:greet: function() {const self = this;return function() {console.log(self.name); // ✅ 正确输出 "Alice"}; }

闭包是一把双刃剑,它既可以:创建私有变量,避免全局变量污染 也会:闭包会导致内存泄漏,如果不销毁闭包,他引用的外部变量就会一直保存在内存当中,无法被释放,从而导致内存泄漏 。
就像她对你一样,既能在恋爱中让你开心幸福,也会在吵架时让你痛苦不堪
但是,只要我们珍惜这些幸福,勇敢面对好好处理这些痛苦就能让我们的感情历久弥新。闭包也是一样啊,只要我们利用好它的优点,规避全局变量污染就能让我们变成大佬。
所以,面对再多的困难,再多的误会也要拉紧她的手,会幸福的!
相关文章:
《作用域大冒险:从闭包到内存泄漏的终极探索》
“爱自有天意,天有道自不会让有情人分离” 大家好,关于闭包问题其实实际上是js作用域的问题,那么js有几种作用域呢? 作用域类型关键字/场景作用域范围示例全局作用域var(无声明)整个程序var x 10;函数作用…...
android Stagefright框架
作为Android音视频开发人员,学习Stagefright框架需要结合理论、源码分析和实践验证。以下是系统化的学习路径: 1. 基础准备 熟悉Android多媒体体系 掌握MediaPlayer、MediaCodec、MediaExtractor等核心API的用法。 理解Android的OpenMAX IL(…...
Shell脚本-变量的分类
在Shell脚本编程中,变量是存储数据的基本单位。它们可以用来保存字符串、数字甚至是命令的输出结果。正确地定义和使用变量能够极大地提高脚本的灵活性与可维护性。本文将详细介绍Shell脚本中变量的不同分类及其应用场景,帮助你编写更高效、简洁的Shell脚…...
<C#>.NET WebAPI 的 FromBody ,FromForm ,FromServices等详细解释
在 .NET 8 Web API 中,[FromBody]、[FromForm]、[FromHeader]、[FromKeyedServices]、[FromQuery]、[FromRoute] 和 [FromServices] 这些都是用于绑定控制器动作方法参数的特性,下面为你详细解释这些特性。 1. [FromBody] 作用:从 HTTP 请求…...
让数据应用更简单:Streamlit与Gradio的比较与联系
在数据科学与机器学习的快速发展中,如何快速构建可视化应用成为了许多工程师和数据科学家的一个重要需求。Streamlit和Gradio是两款备受欢迎的开源库,它们各自提供了便捷的方式来构建基于Web的应用。虽然二者在功能上有许多相似之处,但它们的…...
LlamaIndex 生成的本地索引文件和文件夹详解
LlamaIndex 生成的本地索引文件和文件夹详解 LlamaIndex 在生成本地索引时会创建一个 storage 文件夹,并在其中生成多个 JSON 文件。以下是每个文件的详细解释: 1. storage 文件夹结构 1.1 docstore.json 功能:存储文档内容及其相关信息。…...
AndroidRom定制删除Settings某些菜单选项
AndroidRom定制删除Settings某些菜单选项 1.前言. 最近在Rom开发中需要隐藏设置中的某些菜单,launcher3中的定制开发,这个属于很基本的定制需求,和隐藏google搜素栏一样简单,这里我就不展开了,直接上代码. 2.隐藏网络…...
Mysql相关知识2:Mysql隔离级别、MVCC、锁
文章目录 MySQL的隔离级别可重复读的实现原理Mysql锁按锁的粒度分类按锁的使用方式分类按锁的状态分类 MySQL的隔离级别 在 MySQL 中,隔离级别定义了事务之间相互隔离的程度,用于控制一个事务对数据的修改在何时以及如何被其他事务可见。MySQL 支持四种…...
Python爬虫实战:获取海口最近2周天气数据,为出行做参考
一、引言 天气状况对人们的出行计划影响重大。获取准确的天气信息并进行分析,能助力用户更好地规划出行。天气网虽提供丰富的天气数据,但因网站存在反爬机制,直接获取数据存在一定难度。本研究借助 Python 的 Scrapy 框架,结合多种技术手段,实现对海口最近两周天气数据的…...
并发设计模式之双缓冲系统
双缓冲的本质是 通过空间换时间,通过冗余的缓冲区解决生产者和消费者的速度差异问题,同时提升系统的并发性和稳定性。 双缓冲的核心优势 优势具体表现解耦生产与消费生产者和消费者可以独立工作,无需直接同步。提高并发性生产者和消…...
linux sysfs的使用
在Linux内核驱动开发中,device_create_file 和 device_remove_file 用于动态创建/删除设备的 sysfs 属性文件,常用于暴露设备信息或控制参数。以下是完整示例及详细说明: 1. 头文件引入 #include <linux/module.h> #include <linux/…...
【数据结构和算法】3. 排序算法
本文根据 数据结构和算法入门 视频记录 文章目录 1. 排序算法2. 插入排序 Insertion Sort2.1 概念2.2 具体步骤2.3 Java 实现2.4 复杂度分析 3. 快排 QuickSort3.1 概念3.2 具体步骤3.3 Java实现3.4 复杂度分析 4. 归并排序 MergeSort4.1 概念4.2 递归具体步骤4.3 Java实现4.4…...
LintCode第192题-通配符匹配
描述 给定一个字符串 s 和一个字符模式 p ,实现一个支持 ? 和 * 的通配符匹配。匹配规则如下: ? 可以匹配任何单个字符。* 可以匹配任意字符串(包括空字符串)。 两个串完全匹配才算匹配成功。 样例 样例1 输入: "aa&q…...
redis常用的五种数据类型
redis常用的五种数据类型 文档 redis单机安装redis数据类型-位图bitmap 说明 官网操作命令指南页面:https://redis.io/docs/latest/commands/?nameget&groupstring 常用命令 keys *:查看所有键exists k1 k2:键存在个数type k1&…...
Linux 进程与线程间通信方式及应用分析
Linux 进程与线程间通信方式及应用分析 文章目录 Linux 进程与线程间通信方式及应用分析 1. 管道(Pipe)1.1 匿名管道(Anonymous Pipe)示例代码:结果: 1.2 命名管道(FIFO)示例代码&am…...
AI日报 - 2024年04月22日
🌟 今日概览(60秒速览) ▎🤖 模型进展 | Google发布Gemini 2.5 Flash,强调低延迟与成本效益;Kling AI 2.0展示多轴运动视频生成;研究揭示SLM在知识图谱上优于LLM,RLHF在推理提升上存局限。 ▎💼…...
FreeRTos学习记录--2.内存管理
后续的章节涉及这些内核对象:task、queue、semaphores和event group等。为了让FreeRTOS更容易使用,这些内核对象一般都是动态分配:用到时分配,不使用时释放。使用内存的动态管理功能,简化了程序设计:不再需…...
HAL库(STM32CubeMX)——高级ADC学习、HRTIM(STM32G474RBT6)
系列文章目录 文章目录 系列文章目录前言存在的问题HRTIMcubemx配置前言 对cubemx的ADC的设置进行补充 ADCs_Common_Settings Mode:ADC 模式 Independent mod 独立 ADC 模式,当使用一个 ADC 时是独立模式,使用两个 ADC 时是双模式,在双模式下还有很多细分模式可选 ADC_Se…...
单例模式(线程安全)
1.什么是单例模式 单例模式(Singleton Pattern)是一种创建型设计模式,旨在确保一个类只有一个实例,并提供一个全局访问点来访问该实例。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单…...
FreeRTos学习记录--1.工程创建与源码概述
1.工程创建与源码概述 1.1 工程创建 使用STM32CubeMX,可以手工添加任务、队列、信号量、互斥锁、定时器等等。但是本课程不想严重依赖STM32CubeMX,所以不会使用STM32CubeMX来添加这些对象,而是手写代码来使用这些对象。 使用STM32CubeMX时&…...
基于大模型的血栓性外痔全流程风险预测与治疗管理研究报告
目录 一、引言 1.1 研究背景与目的 1.2 研究意义 二、血栓性外痔概述 2.1 定义与发病机制 2.2 临床表现与诊断方法 2.3 现有治疗手段综述 三、大模型在血栓性外痔预测中的应用原理 3.1 大模型技术简介 3.2 模型构建与训练数据来源 3.3 模型预测血栓性外痔的工作流程…...
进程控制(linux+C/C++)
目录 进程创建 写时拷贝 fork 进程终止 退出码 进程退出三种情况对应退出信号 :退出码: 进程退出方法 进程等待 两种方式 阻塞等待和非阻塞等待 小知识 进程创建 1.在未创建子进程时,父进程页表对于数据权限为读写,对于…...
C++如何处理多线程环境下的异常?如何确保资源在异常情况下也能正确释放
多线程编程的基本概念与挑战 多线程编程的核心思想是将程序的执行划分为多个并行运行的线程,每个线程可以独立处理任务,从而充分利用多核处理器的性能优势。在C中,开发者可以通过std::thread创建线程,并使用同步原语如std::mutex、…...
TensorBoard如何在同一图表中绘制多个线条
1. 使用不同的日志目录 TensorBoard 会根据日志文件所在的目录来区分不同的运行。可以为每次运行指定一个独立的日志目录,TensorBoard 会自动将这些目录中的数据加载并显示为不同的运行。 示例(TensorFlow): import tensorflow…...
微软Entra新安全功能引发大规模账户锁定事件
误报触发大规模锁定 多家机构的Windows管理员报告称,微软Entra ID新推出的"MACE"(泄露凭证检测应用)功能在部署过程中产生大量误报,导致用户账户被大规模锁定。这些警报和锁定始于昨夜,部分管理员认为属于误…...
基于FPGA的一维时间序列idct变换verilog实现,包含testbench和matlab辅助验证程序
目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 4.1 DCT离散余弦变换 4.2 IDCT逆离散余弦变换 4.3 树结构实现1024点IDCT的原理 5.算法完整程序工程 1.算法运行效果图预览 (完整程序运行后无水印) matlab仿真结果 FPGA仿真结果 由于FP…...
Linux进程5-进程通信常见的几种方式、信号概述及分类、kill函数及命令、语法介绍
目录 1.进程间通信概述 1.1进程通信的主要方式 1.2进程通信的核心对比 2.信号 2.1 信号的概述 2.1.1 信号的概念 2.2信号的核心特性 2.3信号的产生来源 2.4信号的处理流程 2.5关键系统调用与函数 2.6常见信号的分类及说明 2.6.1. 标准信号(Standard Sig…...
[架构之美]一键服务管理大师:Ubuntu智能服务停止与清理脚本深度解析
[架构之美]一键服务管理大师:Ubuntu智能服务停止与清理脚本深度解析 服务展示: 运行脚本: 剩余服务: 一、脚本设计背景与核心价值 在Linux服务器运维中,服务管理是日常操作的重要环节。本文介绍的智能服务管理脚本&a…...
C++算法(10):二叉树的高度与深度,(C++代码实战)
引言 在二叉树的相关算法中,高度(Height)和深度(Depth)是两个容易混淆的概念。本文通过示例和代码实现,帮助读者清晰区分二者的区别。 定义与区别 属性定义计算方式深度从根节点到该节点的边数根节点深度…...
k8s 基础入门篇之开启 firewalld
前面在部署k8s时,都是直接关闭的防火墙。由于生产环境需要开启防火墙,只能放行一些特定的端口, 简单记录一下过程。 1. firewall 与 iptables 的关系 1.1 防火墙(Firewall) 定义: 防火墙是网络安全系统&…...
