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

news2025/5/14 13:01:02

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


大家好,关于闭包问题其实实际上是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.coloradmin.cn/o/2340139.html

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

相关文章

让数据应用更简单:Streamlit与Gradio的比较与联系

在数据科学与机器学习的快速发展中&#xff0c;如何快速构建可视化应用成为了许多工程师和数据科学家的一个重要需求。Streamlit和Gradio是两款备受欢迎的开源库&#xff0c;它们各自提供了便捷的方式来构建基于Web的应用。虽然二者在功能上有许多相似之处&#xff0c;但它们的…

LlamaIndex 生成的本地索引文件和文件夹详解

LlamaIndex 生成的本地索引文件和文件夹详解 LlamaIndex 在生成本地索引时会创建一个 storage 文件夹&#xff0c;并在其中生成多个 JSON 文件。以下是每个文件的详细解释&#xff1a; 1. storage 文件夹结构 1.1 docstore.json 功能&#xff1a;存储文档内容及其相关信息。…

AndroidRom定制删除Settings某些菜单选项

AndroidRom定制删除Settings某些菜单选项 1.前言. 最近在Rom开发中需要隐藏设置中的某些菜单&#xff0c;launcher3中的定制开发&#xff0c;这个属于很基本的定制需求&#xff0c;和隐藏google搜素栏一样简单&#xff0c;这里我就不展开了&#xff0c;直接上代码. 2.隐藏网络…

【数据结构和算法】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…

FreeRTos学习记录--2.内存管理

后续的章节涉及这些内核对象&#xff1a;task、queue、semaphores和event group等。为了让FreeRTOS更容易使用&#xff0c;这些内核对象一般都是动态分配&#xff1a;用到时分配&#xff0c;不使用时释放。使用内存的动态管理功能&#xff0c;简化了程序设计&#xff1a;不再需…

HAL库(STM32CubeMX)——高级ADC学习、HRTIM(STM32G474RBT6)

系列文章目录 文章目录 系列文章目录前言存在的问题HRTIMcubemx配置前言 对cubemx的ADC的设置进行补充 ADCs_Common_Settings Mode:ADC 模式 Independent mod 独立 ADC 模式,当使用一个 ADC 时是独立模式,使用两个 ADC 时是双模式,在双模式下还有很多细分模式可选 ADC_Se…

单例模式(线程安全)

1.什么是单例模式 单例模式&#xff08;Singleton Pattern&#xff09;是一种创建型设计模式&#xff0c;旨在确保一个类只有一个实例&#xff0c;并提供一个全局访问点来访问该实例。这种模式涉及到一个单一的类&#xff0c;该类负责创建自己的对象&#xff0c;同时确保只有单…

FreeRTos学习记录--1.工程创建与源码概述

1.工程创建与源码概述 1.1 工程创建 使用STM32CubeMX&#xff0c;可以手工添加任务、队列、信号量、互斥锁、定时器等等。但是本课程不想严重依赖STM32CubeMX&#xff0c;所以不会使用STM32CubeMX来添加这些对象&#xff0c;而是手写代码来使用这些对象。 使用STM32CubeMX时&…

进程控制(linux+C/C++)

目录 进程创建 写时拷贝 fork 进程终止 退出码 进程退出三种情况对应退出信号 &#xff1a;退出码&#xff1a; 进程退出方法 进程等待 两种方式 阻塞等待和非阻塞等待 小知识 进程创建 1.在未创建子进程时&#xff0c;父进程页表对于数据权限为读写&#xff0c;对于…

TensorBoard如何在同一图表中绘制多个线条

1. 使用不同的日志目录 TensorBoard 会根据日志文件所在的目录来区分不同的运行。可以为每次运行指定一个独立的日志目录&#xff0c;TensorBoard 会自动将这些目录中的数据加载并显示为不同的运行。 示例&#xff08;TensorFlow&#xff09;&#xff1a; import tensorflow…

微软Entra新安全功能引发大规模账户锁定事件

误报触发大规模锁定 多家机构的Windows管理员报告称&#xff0c;微软Entra ID新推出的"MACE"&#xff08;泄露凭证检测应用&#xff09;功能在部署过程中产生大量误报&#xff0c;导致用户账户被大规模锁定。这些警报和锁定始于昨夜&#xff0c;部分管理员认为属于误…

基于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. 标准信号&#xff08;Standard Sig…

[架构之美]一键服务管理大师:Ubuntu智能服务停止与清理脚本深度解析

[架构之美]一键服务管理大师&#xff1a;Ubuntu智能服务停止与清理脚本深度解析 服务展示&#xff1a; 运行脚本&#xff1a; 剩余服务&#xff1a; 一、脚本设计背景与核心价值 在Linux服务器运维中&#xff0c;服务管理是日常操作的重要环节。本文介绍的智能服务管理脚本&a…

C++算法(10):二叉树的高度与深度,(C++代码实战)

引言 在二叉树的相关算法中&#xff0c;高度&#xff08;Height&#xff09;和深度&#xff08;Depth&#xff09;是两个容易混淆的概念。本文通过示例和代码实现&#xff0c;帮助读者清晰区分二者的区别。 定义与区别 属性定义计算方式深度从根节点到该节点的边数根节点深度…

Psychology 101 期末测验(附答案)

欢呼 啦啦啦~啦啦啦~♪(^∇^*) 终于考过啦~ 开心(*^▽^*) 撒花✿✿ヽ(▽)ノ✿ |必须晒下证书: 判卷 记录下判卷,还是错了几道,填空题2道压根填不上。惭愧~ 答案我隐藏了,实在想不出答案的朋友可以留言,不定时回复。 建议还是认认真真的学习~认认真真的考试~,知识就…

安全协议分析概述

一、概念 安全协议&#xff08;security protocol&#xff09;&#xff0c;又称密码协议。是以密码学为基础的消息交换协议&#xff0c;在网络中提供各种安全服务。&#xff08;为解决网络中的现实问题、满足安全需求&#xff09; 1.1 一些名词 那什么是协议呢&#xff1f; …

基础学习:(7)nanoGPT 剩下的细节

文章目录 前言3 继续巴拉结构3.1 encode 和 embedding3.2 全局layernorm3.3 lm_head(language modeling) 和 softmax3.4 softmax 和 linear 之间的 temperature和topk3.5 weight tying 前言 在 基础学习&#xff1a;&#xff08;6&#xff09;中, 在运行和训练代码基础上,向代…

Spark-SQL连接Hive总结及实验

一、核心模式与配置要点 1. 内嵌Hive 无需额外配置&#xff0c;直接使用&#xff0c;但生产环境中几乎不使用。 2. 外部Hive&#xff08;spark-shell连接&#xff09; 配置文件&#xff1a;将hive-site.xml&#xff08;修改数据库连接为node01&#xff09;、core-site.xml、…

Linux Wlan-四次握手(eapol)框架流程

协议基础 基于 IEEE 802.1X 标准实现的协议 抓包基础 使用上一章文章的TPLINK wn722n v1网卡在2.4G 频段抓包&#xff08;v2、v3是不支持混杂模式的&#xff09; eapol的四个交互流程 根据不同的认证模式不同&#xff0c;两者的Auth流程有所不同&#xff0c;但是握手流程基…