「JavaScript深入」彻底理解JS中的闭包

news2024/11/23 15:23:18

JavaScript深入 — 闭包

    • 一、概念
    • 二、示例
    • 三、实用的闭包
    • 四、用闭包模拟私有方法
    • 五、一个常见错误:在循环中创建闭包
      • 🌰 另一个经典例子-定时器与闭包
    • 六、优劣
      • 好处
      • 坏处
      • 解决
    • 七、图解闭包
    • 八、应用 💪
      • 封装私有变量
      • 函数工厂
      • 异步操作中的回调函数
      • 柯里化(封装函数)
    • 引申


一、概念

闭包简单来说就是引用了另一个函数作用域中变量的函数

闭包(closure)是一个函数以及其捆绑的周边环境状态(lexical environment,词法环境)的引用的组合。换而言之,闭包让开发者可以从内部函数访问外部函数的作用域。在JavaScript中,闭包会随着函数的创建而被同时创建


二、示例

//函数作为返回值
function test() {
  const a = 1;
  return function() {
    console.log('a: ',a);
  }
}

const fn = test();
const a = 2;
fn();// a: 1

通俗来说,a这个自由变量查找的规则,它会在函数定义的地方去向上一层查找它的值,而不会在函数执行的地方向上一层去查找

//函数作为参数
function test(fn) {
  const a = 1;
  fn();
}
const a = 2;
function fn() {
  console.log('a:',a);
}
test(fn);//a: 2

依然是上述通俗的定义,在函数定义处查找变量a的值


三、实用的闭包

🤔 假如,我们想在页面上添加一些可以调整字号的按钮。

function makeSizer(size){
  return function() {
    document.body.style.fontSize = size + 'px';
  }
}
var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);
document.getElementById("size-12").onclick = size12;
document.getElementById("size-14").onclick = size14;
document.getElementById("size-16").onclick = size16;

从本质上将,makeSizer 是一个函数工厂,他创建了将字体调整至指定大小的函数,上面的示例中,我们使用函数工厂创建了三个新函数,分别将字体大小调制12、14、16px
size12size14size16 都是闭包,它们共享相同的函数定义,但是保存了不同的词法环境,在size12中,size为12,其他同理。


四、用闭包模拟私有方法

Java支持将方法声明为私有,即它们只能被同一个类中的其他方法调用,而JavaScript没有这种原生支持,但我们可以使用闭包来模拟私有方法。

我们定义了一个匿名函数,用于创建一个计数器,我们立即执行了这个匿名函数,并将他的值赋给了变量Counter。我们可以把这个函数储存在另一个变量makerCounter中,并用他来创建多个计数器

var Counter = (function () {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function () {
      changeBy(1);
    },
    decrement: function () {
      changeBy(-1);
    },
    value: function () {
      return privateCounter;
    }
  }
})();

console.log(Counter.value()); // 0
Counter.increment();
Counter.increment();
console.log(Counter.value()); // 2
Counter.decrement();
console.log(Counter.value()); // 1

在之前的示例中,每个闭包都有它自己的词法环境,而这次我们只创建了一个词法环境,为三个函数所共享:Counter.incrementCounter.decrementCounter.value

上面使用了立即执行函数表达式(IIFE)的相关内容,使用立即执行函数表达式好处有下

  • 避免变量污染(命名冲突),例如不同的第三方库恰好使用了同一个变量名称
  • 隔离作用域
  • 提高性能(减少对作用域的查找)

(在ES6前,JS原生并没有块级作用域的概念,所以IIFE可以用函数作用域来模拟块级作用域)

每次调用其中一个计数器时,通过改变这个变量的值,会改变这个闭包的词法环境。然而在一个闭包内对变量的修改,不会影响到另外一个闭包中的变量


五、一个常见错误:在循环中创建闭包

在ECMAScript 2015引入 let 关键字之前,在循环中有一个常见的闭包创建问题

function showHelp(help) {
  document.getElementById("help").innerHTML = help;
}

function setupHelp() {
  var helpText = [
    { id: "email", help: "Your e-mail address" },
    { id: "name", help: "Your full name" },
    { id: "age", help: "Your age (you must be over 16)" },
  ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = function () {
      showHelp(item.help);
    };
  }

}

setupHelp();

这里赋值给 onfocus 的是闭包,在循环中创建了三个闭包,但它们共享了一个词法作用域,在这个作用域中存在一个变量 item。因为变量 item 使用var进行声明,由于变量提升,所以具有函数作用域。当 onfocus 的回调执行时,由于循环早已执行完毕,item 已经指向了 helpText

有了 let 就不会有这样的问题(块级作用域)

🌰 另一个经典例子-定时器与闭包

for(var i = 1; i <= 5; i++){
  setTimeout(function(){
    console.log(i + '');
  },100)
}

按照预期它应该依次输出 1 2 3 4 5,而结果它输出了五次5,同上理,在 setTimeout 的回调函数开始在 Callback Queue 中依次执行时,循环早执行解释,i的值为5,而这回调函数的五个闭包共享一个词法作用域

使用 let 关键字,按照预期依次输出 1 2 3 4 5

for(let i = 1; i <= 5; i++){
  setTimeout(function(){
    console.log(i + '');
  },100)
}

不用 let 关键字修改如下

for(var i = 1; i <= 5; i++){
  (function(number) {
    setTimeout(function(){
      console.log(number);
    },100)
  })(i)
}

六、优劣

好处

  • 保护函数内的变量安全,实现封装,防止变量流入其他环境发生命名冲突,避免全局变量污染
  • 在内存中维持一个变量,可以做缓存,延长变量的生命周期
  • 匿名自执行函数可以减少内存消耗

坏处

  • 内存泄漏:由于闭包中的函数引用了外部函数的变量,而外部函数的作用域在函数执行结束后并不会被销毁,这就导致了闭包函数中的变量也无法被销毁,从而占用了内存空间。如果闭包被滥用,可能会导致内存泄漏的问题。
  • 性能问题:闭包中的函数访问外部函数的变量需要通过作用域链来查找,而作用域链的长度决定了查找的速度。如果闭包层数较深,作用域链就会很长,从而影响了函数的执行效率。

解决

  • 及时释放闭包:如果不再需要使用闭包,可以手动将其赋值为 null,从而释放闭包中占用的内存空间。
  • 减少闭包层数:尽量减少闭包层数,避免作用域链过长,从而提高函数的执行效率。
  • 使用立即执行函数:可以使用立即执行函数来避免闭包的内存泄漏问题。由于立即执行函数在执行结束后会被立即销毁,因此其中的变量也会被释放。
  • 使用模块化编程:可以使用模块化编程来避免闭包的性能问题。在模块化编程中,每个模块都是一个独立的作用域,不会对全局作用域造成影响,从而避免了作用域链过长的问题。

七、图解闭包

在这里插入图片描述


八、应用 💪

封装私有变量

👇 闭包可以用于创建具有私有成员的对象。通过将变量放在闭包中,使之对外不可见。

function createCounter() {
  let count = 0;

  return {
    increment: function() {
      count++;
    },
    getValue: function() {
      return count;
    }
  };
}

const counter = createCounter();
counter.increment();
console.log(counter.getValue()); // 输出 1

函数工厂

👇 如下,makeSizer是一个函数工厂,他创建了将字体调整至指定大小的函数。

function makeSizer(size){
  return function() {
    document.body.style.fontSize = size + 'px';
  }
}
var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);
document.getElementById("size-12").onclick = size12;
document.getElementById("size-14").onclick = size14;
document.getElementById("size-16").onclick = size16;

异步操作中的回调函数

👇 在异步编程中,回调函数通常是闭包,因为它们可以访问其定义时的上下文,这对于保存状态和数据非常有用。

function fetchData(url, callback) {
  // 异步操作获取数据
  setTimeout(function() {
    const data = /* 获取的数据 */;
    callback(data);
  }, 1000);
}

fetchData('https://example.com/api', function(data) {
  console.log(data);
});

setTimeout 内部延迟执行的「匿名回调函数」可以访问外部函数fetchData的词法作用域,引用了其中变量callback,因此形成了闭包

柯里化(封装函数)

// 多参数柯里化
const curry = function(fn){
  return function curriedFn(...args){
    if(args.length<fn.length){
      return function(){
        return curriedFn(...args.concat([...arguments]));
      }
    }
    return fn(...args);
  }
}
const fn = (x,y,z,a)=>x+y+z+a;
const myfn = curry(fn);
console.log(myfn(1)(2)(3)(1));

引申

手写一个闭包?举个闭包的例子?

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

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

相关文章

css中背景色、背景图的使用

1、同时使用背景色、背景图片 参考链接&#xff1a;链接 以下样式&#xff0c;背景色在图片下方(缺点&#xff1a;图片不透明时&#xff0c;背景色会被完全遮挡。) .header {height: 100%;width: 100%;background-color: #000;background-image: url(/static/images/back.pn…

云原生之运维监控实践-使用Prometheus与Grafana实现对MySQL和Redis服务的监测

背景 如果你要为应用程序构建规范或用户故事&#xff0c;那么务必先把应用程序每个组件的监控指标考虑进来&#xff0c;千万不要等到项目结束或部署之前再做这件事情。——《Prometheus监控实战》 去年写了一篇在Docker环境下部署若依微服务ruoyi-cloud项目的文章&#xff0c;当…

数据结构之手搓顺序表(顺序表的增删查改)

目录 文章目录 前言 一、什么是顺序表&#xff1f; 二、动态顺序表的实现 1.头文件定义 2.实现顺序表的初始化 3.检查顺序表空间容量是否足够&#xff0c;不够就增容 4.顺序表的销毁 5.顺序表的打印 6.顺序表的尾插 7.顺序表的头插 8.顺序表的头删 9.顺序表的尾删 10.顺序…

LeetCode题练习与总结:二叉树的所有路径--257

一、题目描述 给你一个二叉树的根节点 root &#xff0c;按 任意顺序 &#xff0c;返回所有从根节点到叶子节点的路径。 叶子节点 是指没有子节点的节点。 示例 1&#xff1a; 输入&#xff1a;root [1,2,3,null,5] 输出&#xff1a;["1->2->5","1->…

RabbitMQ基本原理

一、基本结构 所有中间件技术都是基于 TCP/IP 协议基础之上进行构建新的协议规范&#xff0c;RabbitMQ遵循的是AMQP协议&#xff08;Advanced Message Queuing Protocol - 高级消息队列协议&#xff09;。 生产者发送消息流程&#xff1a; 1、生产者和Broker建立TCP连接&#…

国庆同欢,祖国昌盛!肌肉纤维启发,水凝胶如何重构聚合物

在这个国庆佳节&#xff0c;我们共同感受祖国的繁荣昌盛&#xff0c;同时也迎来了知识的探索之旅。今天来了解聚合物架构的重构的研究——《Hydrogel‐Reactive‐Microenvironment Powering Reconfiguration of Polymer Architectures》发表于《Advanced Science》。材料科学不…

【数据结构与算法】算法和算法分析

文章目录 一.算法1.定义2.描述 二.算法与程序三.算法特性四.算法效率的度量4.1算法时间事前分析法算法时间复杂度的渐进表示法分析算法时间复杂度的基本方法 4.2算法空间 数据的逻辑结构映像到内存就是数据的存储结构&#xff0c;针对数据的逻辑结构可以选择多种存储结构。数据…

Kotlin:2.0.0 的新特性

一、概述 kotlin 2.0.0版本英文官方文档 The Kotlin 2.0.0 release is out and the new Kotlin K2 compiler is Stable! Additionally, here are some other highlights: Kotlin 2.0.0发布了&#xff0c;新的Kotlin K2编译器已经稳定了。此外&#xff0c;以下是其他一些亮点: …

Linux操作系统中dubbo

1、简介 dubbo框架是做微服务通信的&#xff0c;是由阿里巴巴开发&#xff0c;后捐赠给阿帕奇基金会。 2、与OpenFeign的区别 dubbo是采用RPC协议实现微服务通信&#xff0c;OpenFeign是采用Http请求的方式实现的。 OpenFeign 最简单的&#xff0c;就是Spring公司开发的&am…

TinyAP:使用TinyML对抗Wi-Fi攻击的智能接入点

论文标题&#xff1a; 英文&#xff1a;TinyAP: An intelligent Access Point to combat Wi-Fi attacks using TinyML中文&#xff1a;TinyAP&#xff1a;使用TinyML对抗Wi-Fi攻击的智能接入点 作者信息&#xff1a; Anand Agrawal 和 Rajib Ranjan Maiti&#xff0c;来自印…

C语言常用标准库 -- 5.<time.h>

目录 引言 5. C标准库--time.h 5.1 简介 5.2 常量与宏 5.3 库变量 5.4 库宏 5.5 库函数 5.6 注意事项 &#x1f308;你好呀&#xff01;我是 程序猿 &#x1f30c; 2024感谢你的陪伴与支持 ~ &#x1f680; 欢迎一起踏上探险之旅&#xff0c;挖掘无限可能&#xff0c;…

线程池面试集

目录 线程池中提交一个任务的流程是怎样的? 线程池有五种状态 如何优雅的停止一个线程? 线程池的核心线程数、最大线程数该如何设置? 如何理解Java并发中的可见性、原子性、有序性? Java死锁如何避免? 线程池中提交一个任务的流程是怎样的? 线程池有五种状态 如何优…

【docker学习】Linux系统离线方式安装docker环境方法

centos7-linux安装docker(离线方式) 下载docker的安装文件 https://download.docker.com/linux/static/stable/x86_64/ 下载的是&#xff1a;docker-18.06.3-ce.tgz 这个压缩文件 将docker-18.06.3-ce.tgz文件上传到centos7-linux系统上&#xff0c;用ftp工具上传即可 解压…

《RabbitMQ篇》基本概念介绍

MQ功能 解耦 MQ允许不同系统或组件之间松散耦合。发送者和接收者不需要直接连接&#xff0c;从而提高了系统的灵活性和可维护性。异步处理 使用MQ可以实现异步消息传递&#xff0c;发送者可以将消息放入队列后立即返回&#xff0c;不必等待接收者处理。这提高了系统的响应速度…

鸿蒙NEXT开发-ArkTS(基于最新api12稳定版)

注意&#xff1a;博主有个鸿蒙专栏&#xff0c;里面从上到下有关于鸿蒙next的教学文档&#xff0c;大家感兴趣可以学习下 如果大家觉得博主文章写的好的话&#xff0c;可以点下关注&#xff0c;博主会一直更新鸿蒙next相关知识 专栏地址: https://blog.csdn.net/qq_56760790/…

Spring IoC笔记

目录 1.什么是 IoC&#xff1f; 2.IoC类注解&#xff08;五大注解&#xff09; 2.1那为什么要这么多类注解&#xff1f; 2.2五大注解是不是可以混用&#xff1f; 2.3程序被spring管理的条件是&#xff1f; 3.bean对象 3.1Bean 命名约定 3.2获取bean对象 4.⽅法注解 B…

《应急通信产业发展研究报告》蓝皮书解读

近日&#xff0c;中国信通院发布了《应急通信产业发展研究报告》蓝皮书&#xff0c;该报告是对中国应急通信产业现状、发展趋势及其政策环境的综合分析&#xff0c;旨在为行业发展提供参考与指导。以下是小编对该蓝皮书的一些内容解读&#xff1a; 1.应急通信的重要性 应急通信…

2.点位管理|前后端如何交互——帝可得后台管理系统

目录 前言点位管理菜单模块1.需求说明2.库表设计3.生成基础代码0 .使用若依代码生成器最终目标1.创建点位管理2.添加数据字典3.配置代码生成信息4.下载代码并导入项目 4.优化菜单——点位管理1.优化区域管理2.增加点位数3. 合作商4.区域管理中添加查看详情功能5.合作商添加点位…

【拥抱AIGC】通义灵码扩展管理

通义灵码提供了扩展管理&#xff0c;支持自定义指令&#xff0c;满足企业编码场景的扩展诉求。 适用版本 企业标准版、企业专属版 通义灵码管理员、组织内全局管理员&#xff08;专属版&#xff09;在通义灵码控制台-扩展管理中&#xff0c;进行自定义指令的管理、查看自定义…

解锁数据宝藏:AI驱动搜索工具,让非结构化数据“说话

哈哈,说起这个 AI 搜索演示啊,那可真是个有意思的话题!非结构化数据,这家伙虽然难搞,但价值却是杠杠的。今天呢,咱就好好聊聊怎么借助 Fivetran 和 Milvus,快速搭建一个 AI 驱动的搜索工具,让企业能从那些乱七八糟的数据里淘到金子! 一、非结构化数据的挑战与机遇 首…