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

 “爱自有天意,天有道自不会让有情人分离”


大家好,关于闭包问题其实实际上是js作用域的问题,那么js有几种作用域呢?

作用域类型关键字/场景作用域范围示例
全局作用域var(无声明)整个程序var x = 10;
函数作用域var 在函数内函数内部function foo() { var x; }
块级作用域letconst{} 代码块内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("新的内部参数");

输出结果:

----- 分割线 -----外部函数可以访问自己的变量: 我是外部变量
外部函数可以访问自己的参数: 外部参数
内部函数访问自己的变量: 我是内部变量
内部函数访问自己的参数: 内部参数
内部函数访问外部变量: 我是外部变量
内部函数访问外部常量: 我是外部常量
内部函数访问外部参数: 外部参数
只能通过内部函数的返回值来获取内部变量: 我是内部变量----- 分割线 -----内部函数访问自己的变量: 我是内部变量
内部函数访问自己的参数: 新的内部参数
内部函数访问外部变量: 我是外部变量
内部函数访问外部常量: 我是外部常量
内部函数访问外部参数: 外部参数

 这个代码展示的是:

  1. 内部函数可以访问:

    • 自己的变量(innerVar)和参数(innerParam)
    • 外部函数的变量(outerVar)、常量(outerConst)和参数(outerParam)
  2. 外部函数只能访问:

    • 自己的变量(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() 操作才能操作和访问。

回调函数

回调函数想必就不用介绍了,在任何语言中都有出现和应用。

闭包可以让回调函数记住并访问其定义时的作用域变量,即使回调在异步操作(如 setTimeoutfetch、事件监听)中被调用。

介绍一个例子:

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"};
    }

 

闭包是一把双刃剑,它既可以:创建私有变量,避免全局变量污染 也会:闭包会导致内存泄漏,如果不销毁闭包,他引用的外部变量就会一直保存在内存当中,无法被释放,从而导致内存泄漏 。

就像她对你一样,既能在恋爱中让你开心幸福,也会在吵架时让你痛苦不堪

但是,只要我们珍惜这些幸福,勇敢面对好好处理这些痛苦就能让我们的感情历久弥新。闭包也是一样啊,只要我们利用好它的优点,规避全局变量污染就能让我们变成大佬。

所以,面对再多的困难,再多的误会也要拉紧她的手,会幸福的! 

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

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

相关文章

为什么Makefile中的clean需要.PHONY

原因一&#xff1a;避免Makefile检查时间戳 前置知识&#xff1a;makefile在依赖文件没有改变时不会执行编译命令 #第一次执行&#xff0c;OK [rootVM-16-14-centos ~]# make g -E main.cc -o main.i g -S main.i -o main.s g -c main.s -o main.o g main.o -o main#第二…

垂直行业突围:工业软件在汽车、航空领域的 “破壁” 实践

在当今科技高速发展的时代&#xff0c;工业软件已悄然完成从通用工具到垂直行业 “战略武器” 的蜕变。特别是在汽车与航空这两大高端制造领域&#xff0c;工业软件的价值早已超越单纯的效率提升&#xff0c;成为关乎核心技术自主可控的关键要素&#xff0c;一场围绕工业软件的…

07.Python代码NumPy-排序sort,argsort,lexsort

07.Python代码NumPy-排序sort&#xff0c;argsort&#xff0c;lexsort 提示&#xff1a;帮帮志会陆续更新非常多的IT技术知识&#xff0c;希望分享的内容对您有用。本章分享的是NumPy的使用语法。前后每一小节的内容是存在的有&#xff1a;学习and理解的关联性&#xff0c;希望…

LVDS系列8:Xilinx 7系可编程输入延迟(一)

在解析LVDS信号时&#xff0c;十分重要的一环就是LVDS输入信号线在经过PCB输入到FPGA中后&#xff0c;本来该严格对齐的信号线会出现时延&#xff0c;所以需要在FPGA内部对其进行延时对齐后再进行解析。 Xilinx 7系器件中用于输入信号延时的组件为IDELAYE2可编程原语&#xff0…

AI驱动研发效率在中后台的实践

本文探讨了AI驱动的中后台前端研发实践&#xff0c; 涵盖设计出码、接口定义转换、代码拟合、自动化测试等多个环节&#xff0c;通过具体案例展示了AI技术如何优化研发流程并提升效率。特别是在UI代码编写和接口联调阶段&#xff0c;并提出了设计出码&#xff08;Design to Cod…

【Rust 精进之路之第6篇-流程之舞】控制流:`if/else`, `loop`, `while`, `for` 与模式匹配初窥

系列: Rust 精进之路:构建可靠、高效软件的底层逻辑 作者: 码觉客 发布日期: 2025-04-20 引言:让代码“活”起来——指令的流动 在前面的文章中,我们已经掌握了 Rust 的基础数据类型(标量和复合类型)以及如何通过变量绑定来存储和命名它们。这相当于我们准备好了程序…

C++ 表达式求值的基础(四十九)

1. 运算符的分类 1.1 按操作数个数 一元运算符&#xff08;Unary&#xff09; 作用于单个操作数&#xff1a; 取地址 &obj解引用 *ptr逻辑非 !b一元加减 x, -x递增递减 i, i-- 二元运算符&#xff08;Binary&#xff09; 作用于两个操作数&#xff1a; 算术运算 a b, a …

Three.js + React 实战系列 : 从零搭建 3D 个人主页

可能你对tailiwindcss毫不了解&#xff0c;别紧张&#xff0c;记住我们只是在学习&#xff0c;学习的是作者的思想和技巧&#xff0c;并不是某一行代码。 在之前的几篇文章中&#xff0c;我们已经熟悉了 Three.js 的基本用法&#xff0c;并通过 react-three-fiber 快速构建了一…

Kotlin实现Android应用保活方案

Kotlin实现Android应用保活优化方案 以下的Android应用保活实现方案&#xff0c;更加符合现代Android开发规范&#xff0c;同时平衡系统限制和用户体验。 1. 前台服务方案 class OptimizedForegroundService : Service() {private val notificationId 1private val channel…

windows拷贝文件脚本

1、新建脚本文件xxx.bat&#xff0c;名字任意&#xff0c;后缀未.bat即可&#xff0c;将以下内容拷贝进去&#xff0c;修改src和des为自己文件的目录即可。 echo off :: 设置字符集为UTF-8&#xff0c;命令窗口能正确显示中文字符。 chcp 65001 rem 读取当前目录并进入当前目…

Qt 核心库总结

Qt 核心库&#xff08;QtCore&#xff09; QtCore 是 Qt 框架的基础模块&#xff0c;提供非图形界面的核心功能&#xff0c;是所有 Qt 应用程序的基石。它包含事件循环、信号与槽、线程管理、文件操作、字符串处理等功能&#xff0c;适用于 GUI 和非 GUI 应用程序。本文将从入…

大模型相关面试问题原理及举例

大模型相关面试问题原理及举例 目录 大模型相关面试问题原理及举例Transformer相关面试问题原理及举例大模型模型结构相关面试问题原理及举例注意力机制相关面试问题原理及举例大模型与传统模型区别 原理:大模型靠海量参数和复杂结构,能学习更复杂模式。传统模型参数少、结构…

【AI+HR实战应用】用DeepSeek提升HR工作效能

用DeepSeek提升HR工作效能 一、AI 与 AIGC 简介二、DeepSeek 介绍三、使用 DeepSeek 的渠道及硬件要求四、使用 DeepSeek 的核心技巧五、AI 在人力资源的应用场景六、AI 绘画与多模态应用七、个人使用 AI 的能力层级八、企业拥抱 AI 的策略九、提示词管理的重要性 一、AI 与 AI…

Postgresql几个常用的json操作

将行记录转为jsonb row_to_json(表名或别名)将行记录集转为json数组 &#xff08;jsonb) select json_agg(row_to_json(t) order by t.task_name) into v_next_taskfrom dyna_flow_task t where t.zidv_template_id and t.levelv_next_level ;访问json字段&#xff0c;用->…

ESP32学习与快速总结——5.系统存储

1.ESP32分区表 为什么ESP32要分区 00&#xff1a;34-- 简述&#xff1a;其他单片机生成文件少&#xff0c;功能少&#xff0c;而ESP32功能多&#xff0c;文件多 分区表各个文件简介 --7&#xff1a;31vscode查看分区表 --9&#xff1a;33ota通过idf.py menuconfi…

Linux 进程控制(自用)

非阻塞调用waitpid 这样父进程就不会阻塞&#xff0c;此时循环使用我们可以让父进程执行其他任务而不是阻塞等待 进程程序替换 进程PCB加载到内存中的代码和数据 替换就是完全替换当前进程的代码段、数据段、堆和栈&#xff0c;保存当前的PCB 代码指的是二进制代码不是源码&a…

Spring 微服务解决了单体架构的哪些痛点?

1. 部署困难 (Deployment Difficulty & Risk) 单体痛点: 整体部署: 对单体应用的任何微小修改&#xff08;哪怕只是一行代码&#xff09;&#xff0c;都需要重新构建、测试和部署整个庞大的应用程序。部署频率低: 由于部署过程复杂且风险高&#xff0c;发布周期通常很长&a…

面试题之高频面试题

最近开始面试了&#xff0c;410面试了一家公司 针对自己薄弱的面试题库&#xff0c;深入了解下&#xff0c;也应付下面试。在这里先祝愿大家在现有公司好好沉淀&#xff0c;定位好自己的目标&#xff0c;在自己的领域上发光发热&#xff0c;在自己想要的领域上&#xff08;技术…

【MySQL】Read view存储的机制,记录可见分析

read view核心组成 1.1 事务id相关 creator_trx_id: 创建该read view的事务id 每开启一个事务都会生成一个 ReadView&#xff0c;而 creator_trx_id 就是这个开启的事务的 id。 m_ids: 创建read view时系统的活跃事务&#xff08;未提交的事务&#xff09;id集合 当前有哪些事…

【刷题Day20】TCP和UDP(浅)

TCP 和 UDP 有什么区别&#xff1f; TCP提供了可靠、面向连接的传输&#xff0c;适用于需要数据完整性和顺序的场景。 UDP提供了更轻量、面向报文的传输&#xff0c;适用于实时性要求高的场景。 特性TCPUDP连接方式面向连接无连接可靠性提供可靠性&#xff0c;保证数据按顺序…