前端学习<四>JavaScript基础——26-闭包

news2024/10/5 16:30:40

闭包的引入

我们知道,变量根据作用域的不同分为两种:全局变量和局部变量。

  • 函数内部可以访问全局变量和局部变量。

  • 函数外部只能访问全局变量,不能访问局部变量。

  • 当函数执行完毕,本作用域内的局部变量会销毁。

比如下面这样的代码:

 function foo() {
     let a = 1;
 }
 ​
 foo();
 console.log(a); // 打印报错:Uncaught ReferenceError: a is not defined

上方代码中,由于变量 a 是函数内的局部变量,所以外部无法访问。

但是,在有些场景下,我们就是想要在函数外部访问函数内部作用域的局部变量,那要怎么办呢?这就引入了闭包的概念。

什么是闭包

闭包(closure)的概念

闭包:如果外部作用域有权访问另外一个函数内部局部变量时,那就产生了闭包。这个内部函数称之为闭包函数。注意,这里强调的是访问局部变量

闭包代码举例:

 function fun1() {
   const a = 10;
   return function fun2() {
     console.log(a);
   };
 }
 fun1();
 // 调用外部函数,就能得到内部函数,并用 变量 result 接收
 const result = fun1();
 // 在 fun1函数的外部,执行了内部函数 fun2,并访问到了 fun2的内部变量a
 result(); // 10

打印结果:

 10

上方代码中,外部作用域(即全局作用域) 访问了函数 fun1 中的局部变量,那么,在 fun1 中就产生了闭包,函数 fun1是闭包函数。

全局作用域中,并没有定义变量a。正常情况下作为函数内的局部变量 a,无法被外部访问到。但是通过闭包,我们最后还是可以在全局作用域中拿到局部变量 a 的值。

注意,闭包函数是fun1,不是fun2。fun2在这里的作用是让全局作用域访问到变量a,fun2只是一个桥梁。

闭包的生命周期

  1. 产生:内部函数fn1被声明时(即被创建时,不是被调用时)就产生了。

  2. 死亡:嵌套的内部函数成为垃圾对象时。(比如fun1 = null,就可以让 fun1 成为垃圾对象)

在 chrome 浏览器控制台中,调试闭包

上面的代码中,要怎么验证,确实产生了闭包呢?我们可以在 chrome 浏览器的控制台中设置断点,当代码执行到 console.log(a)这一行的时候,如下图所示:

上图中, Local 指的是局部作用域,Global 指的是全局作用域;而 Closure 则是闭包,fn1 是一个闭包函数。

闭包的表现形式

形式1:将一个函数作为另一个函数的返回值

     function fn1() {
       var a = 2
 ​
       function fn2() {
         a++
         console.log(a)
       }
       return fn2
     }
 ​
     var f = fn1();   //执行外部函数fn1,返回的是内部函数fn2
     f() // 3       //执行fn2
     f() // 4       //再次执行fn2

当f()第二次执行的时候,a加1了,也就说明了:闭包里的数据没有消失,而是保存在了内存中。如果没有闭包,代码执行完倒数第三行后,变量a就消失了。

上面的代码中,虽然调用了内部函数两次,但是,闭包对象只创建了一个。

也就是说,要看闭包对象创建了几个,就看:外部函数执行了几次(与内部函数执行几次无关)。

形式2:将函数作为实参传递给另一个函数调用

在定时器、事件监听、Ajax 请求、Web Workers 或者任何异步中,只要使用了回调函数,实际上就是在使用闭包。

     function showDelay(msg, time) {
       setTimeout(function() {  //这个function是闭包,因为是嵌套的子函数,而且引用了外部函数的变量msg
         alert(msg)
       }, time)
     }
     showDelay('qianguyihao', 2000)

上面的代码中,闭包是里面的function,因为它是嵌套的子函数,而且引用了外部函数的变量msg。

闭包的作用

  • 作用1:延长局部变量的生命周期。

  • 作用2:让函数外部可以操作(读写)函数内部的数据(变量/函数)。

代码演示:

 function fun1() {
   let a = 2
 ​
   function fun2() {
     a++
     console.log(a)
   }
   return fun2;
 }
 ​
 const foo = fun1();   //执行外部函数fn1,返回的是内部函数fn2
 foo() // 3       //执行fun2
 foo() // 4       //再次执行fun2

上方代码中,foo 代表的就是整个 fun2 函数。当执行了 foo() 语句之后,也就执行了fun2()函数,fun1() 函数内就产生了闭包。

作用1分析

一般来说,在 fn1() 函数执行完毕后,它里面的变量 a 会立即销毁。但此时由于产生了闭包,所以 fun1 函数中的变量 a 不会立即销毁,仍然保留在内存中,因为 fn2 函数还要继续调用变量 a。只有等所有函数把变量 a 调用完了,变量 a 才会销毁。

作用2分析:

在执行 foo()语句之后,竟然能够打印出 3,这就完美通过闭包实现了:全局作用域成功访问到了局部作用域中的变量 a。

达到的效果是:外界看不到变量a,但可以操作a。当然,如果你真想看到a,可以在fun2中将a返回即可。

闭包的应用场景

场景1:高阶函数

题目:不同的班级有不同成绩检测标准。比如:A班的合格线是60分,B 班的合格线是70分。已知某个人班级和分数,请用闭包函数判断他的成绩是否合格。

思路:创建成绩检测函数 checkStandard(n),检查成绩 n 是否合格,函数返回布尔值。

代码实现:

 // 高阶函数:判断学生的分数是否合格。形参 standardTemp 为标准线
 function createCheckTemp(standardTemp) {
   // 形参 n 表示具体学生的分数
   function checkTemp(n) {
     if (n >= standardTemp) {
       alert('成绩合格');
     } else {
       alert('成绩不合格');
     }
   }
   return checkTemp;
 }
 ​
 // 创建一个 checkStandard_A 函数,它以60分为合格线
 var checkStandard_A = createCheckTemp(60);
 // 再创建一个 checkStandard_B 函数,它以70分为合格线
 var checkStandard_B = createCheckTemp(70);
 ​
 // 调用函数
 checkStandard_A(65); // 成绩合格
 checkStandard_B(65); // 成绩不合格

对于A班来说,它的闭包函数是createCheckTemp(),闭包范围是 checkTemp()函数和参数standardTemp = 60。对于B班来说,它的闭包函数是全新的createCheckTemp(),闭包范围是全新的checkTemp()函数和全新的参数standardTemp = 70

因为有闭包存在,所以,并不会因为 createCheckTemp() 执行完毕后就销毁 standardTemp 的值;且A班和B班的standardTemp参数不会混淆。

备注:关于“高阶函数”的更多解释,我们在以后的内容中讲解。

场景2:封装JS模块

闭包的第二个使用场景是:定义具有特定功能的JS模块,将所有的数据和功能都封装在一个函数内部,只向外暴露指定的对象或方法。模块的调用者,只能调用模块暴露的对象或方法来实现对应的功能。

比如有这样一个需求:定义一个私有变量a,要求a只能被进行指定操作(加减),不能进行其他操作(乘除)。在 Java、C++ 等语言中,有私有属性的概念,但在JS中只能通过闭包模拟。

我们来看看下面的代码,如何通过闭包封装JS模块。

写法1:

(1)myModule.js:(定义一个模块,向外暴露多个方法,供外界调用)

 function myModule() {
     //私有数据
     var msg = 'Qinguyihao Haha'
 ​
     //操作私有数据的函数
     function doSomething() {
         console.log('doSomething() ' + msg.toUpperCase()); //字符串大写
     }
 ​
     function doOtherthing() {
         console.log('doOtherthing() ' + msg.toLowerCase()) //字符串小写
     }
 ​
     //通过【对象字面量】的形式进行包裹,向外暴露多个函数
     return {
         doSomething1: doSomething,
         doOtherthing2: doOtherthing
     }
 }

上方代码中,外界只能通过doSomething1和doOtherthing2来操作里面的数据,但不让外界看到里面的具体实现。

(2)index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>闭包的应用-自定义JS模块</title>
</head>
<body>
<!--
闭包应用举例: 定义JS模块
  * 具有特定功能的js文件
  * 将所有的数据和功能都封装在一个函数内部(私有的)
  * 【重要】只向外暴露一个包含n个方法的对象或方法
  * 模块的使用者, 只需要调用模块暴露的对象或者方法来实现对应的功能
-->
<script type="text/javascript" src="myModule.js"></script>
<script type="text/javascript">
    var module = myModule();
    module.doSomething1();
    module.doOtherthing2();
</script>
</body>
</html>

写法2:

同样是实现上面的功能,我们还采取另外一种写法,写起来更方便。如下:

(1)myModule2.js:(是一个立即执行的匿名函数)

(function () {
    //私有数据
    var msg = 'Qinguyihao Haha'

    //操作私有数据的函数
    function doSomething() {
        console.log('doSomething() ' + msg.toUpperCase())
    }

    function doOtherthing() {
        console.log('doOtherthing() ' + msg.toLowerCase())
    }

    //外部函数是即使运行的匿名函数,我们可以把两个方法直接传给window对象
    window.myModule = {
        doSomething1: doSomething,
        doOtherthing2: doOtherthing
    }
})()

(2)index.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>闭包的应用-自定义JS模块</title>
</head>
<body>
<!--
闭包的应用2 : 定义JS模块
  * 具有特定功能的js文件
  * 将所有的数据和功能都封装在一个函数内部(私有的)
  * 只向外暴露一个包信n个方法的对象或函数
  * 模块的使用者, 只需要通过模块暴露的对象调用方法来实现对应的功能
-->

<!--引入myModule文件-->
<script type="text/javascript" src="myModule2.js"></script>
<script type="text/javascript">
    myModule.doSomething1()
    myModule.doOtherthing2()
</script>
</body>
</html>

上方两个文件中,我们在myModule2.js里直接把两个方法直接传递给window对象了。于是,在index.html中引入这个js文件后,会立即执行里面的匿名函数。在index.html中把myModule直接拿来用即可。

小结:

写法1和写法2都采用了闭包。

内存溢出和内存泄露

我们再讲两个概念,这两个概念和闭包有些联系。

内存泄漏

内存泄漏占用的内存没有及时释放。

内存泄露的次数积累多了,就容易导致内存溢出。

常见的内存泄露

1、意外的全局变量

2、没有及时清理的计时器或回调函数

3、闭包

情况1举例:

// 意外的全局变量
function fn() {
  a = new Array(10000000);
  console.log(a);
}

fn();

情况2举例:

// 没有及时清理的计时器或回调函数
var intervalId = setInterval(function () { //启动循环定时器后不清理
  console.log('----')
}, 1000)

// clearInterval(intervalId);  //清理定时器

情况3举例:

function fn1() {
  var a = 4;
  function fn2() {
    console.log(++a)
  }
  return fn2
}
var f = fn1()
f()

// f = null //让内部函数成为垃圾对象-->回收闭包

内存溢出

内存溢出:程序运行时出现的错误。当程序运行需要的内存超过剩余的内存时,就抛出内存溢出的错误。

代码举例:

    var obj = {};
    for (var i = 0; i < 10000; i++) {
    obj[i] = new Array(10000000);  //把所有的数组内容都放到obj里保存,导致obj占用了很大的内存空间
    console.log("-----");
    }

闭包是否会造成内存泄漏

一般来说,答案是否定的。因为内存泄漏是非预期情况,本来想回收,但实际没回收;而闭包是预期情况,一般不会造成内存泄漏。

但如果因代码质量不高,滥用闭包,也会造成内存泄漏。

闭包面试题

代码举例:

function addCount() {
  let count = 0;
  return function () {
    count = count + 1;
    console.log(count);
  };
}

const fun1 = addCount();
const fun2 = addCount();
fun1();
fun2();

fun1();
fun2();

打印结果:

1
1
2
2

代码解释:

(1)fun1 和 fun2 这两个闭包函数是互不影响的,因此第一次调用时,count变量都是0,最终各自都输出1。

(2)第二次调用时,由于闭包有记忆性,所以各自会在上一次的结果上再加1,因此输出2。

写在最后:希望大家可以点个关注点个赞,这对up真的很重要!谢谢!

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

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

相关文章

Ubuntu20从0开始选择合适版本手动安装cuda,torch-geometric,jax

一个全新的ubuntu20台式机&#xff0c;在Additional Drivers安装nvidia-470-server&#xff08;一开始安装450&#xff0c;cunda版本只能到11.0&#xff0c;torch有些库用不了&#xff0c;可以直接切换点击Apply Changes重启就行&#xff09; nvidia-smi查看CUDA Version可到…

Redis 配置与使用 (Linux 虚拟机Windows客户端)

Centos7 安装Redis详细教程 - JcongJason - 博客园 (cnblogs.com) 安装 下载redis安装包并解压 # 下载&#xff0c;我是在root下执行的下载&#xff0c;所以我的下载目录为&#xff1a;/root/redis-5.0.5&#xff0c;这里按照自己的实际情况调整 wget https://download.redi…

JVM、maven、Nexus

一、jvm简介 1.应用程序申请内存时出现的三种情况&#xff1a; ①OOM:内存溢出&#xff0c;是指应用系统中存在无法回收的内存或使用的内存过多&#xff0c;最终使得程序运行要用到的内存大于能提供的最大内存。此时程序就运行不了&#xff0c;系统会提示内存溢出&#xff0c…

css3实现微信扫码登陆动画

在做微信扫码登陆时&#xff0c;出现一个背景光图上下扫码动画&#xff0c;用css3图片实现。 实现原理&#xff1a; 1.准备一个渐变的背景.png图 2.css动画帧实现动画 看效果&#xff1a; css代码&#xff1a; #wx-scan{position: absolute;top:0px;left: 50%;z-index: 3;ma…

错误分析 (Machine Learning研习十九)

错误分析 您将探索数据准备选项&#xff0c;尝试多个模型&#xff0c;筛选出最佳模型&#xff0c;使用 Grid SearchCV微调其超参数&#xff0c;并尽可能实现自动化。在此&#xff0c;我们假设您已经找到了一个有前途的模型&#xff0c;并希望找到改进它的方法。其中一种方法就…

数据密集型应用系统设计 PDF 电子书(Martin Kleppmann 著)

简介 《数据密集型应用系统设计》全书分为三大部分&#xff1a; 第一部分&#xff0c;主要讨论有关增强数据密集型应用系统所需的若干基本原则。首先开篇第 1 章即瞄准目标&#xff1a;可靠性、可扩展性与可维护性&#xff0c;如何认识这些问题以及如何达成目标。第 2 章我们比…

JQuery(四)---【使用JQuery实现动画效果】

目录 前言 一.隐藏和显示 1.1使用方法 1.2案例演示(1) 1.3隐藏/显示效果一键切换 二.淡入淡出效果 2.1使用方法 2.2案例演示(fadeIn) 2.3案例演示(fadeOut) 2.4案例演示(fadeToggle) 2.5案例演示(fadeTo) 三.滑动 3.1使用方法 3.2案例演示(slideDown) 3.3案例演示…

三道模拟题

P1003 [NOIP2011 提高组] 铺地毯 题目描述 原题点这里-->P1003 [NOIP2011 提高组] 铺地毯 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 为了准备一个独特的颁奖典礼&#xff0c;组织者在会场的一片矩形区域&#xff08;可看做是平面直角坐标系的第一象限&#xff09;铺…

黑马头条项目结构

微服务架构具有许多优点&#xff0c;其中一些主要优点包括&#xff1a; 松耦合性&#xff1a;每个微服务都是独立的&#xff0c;可以独立部署、独立扩展和独立更新&#xff0c;这种松耦合性使得系统更加灵活&#xff0c;易于维护和演化。 技术多样性&#xff1a;由于每个微服务…

鸿蒙开发 @ohos/hypium找不到问题

用的是最新的 开发工具 DevEco Studio 3.1.1 新建的空项目 报错 ohpm ERROR: Install failed ENOENT: no such file or directory, stat ‘E:\win\Project\MyApplication1\oh_modulesohos\hypium’ 解决方式 当前项目中 \oh_modules.ohpmohoshypium1.0.6\oh_modules 这里面有o…

算法练习第19天|222.完全二叉树的节点个数

222.完全二叉树的节点个数 222. 完全二叉树的节点个数 - 力扣&#xff08;LeetCode&#xff09;https://leetcode.cn/problems/count-complete-tree-nodes/description/ 题目描述&#xff1a; 给你一棵 完全二叉树 的根节点 root &#xff0c;求出该树的节点个数。题目数据保…

SpringBoot版本配置问题与端口占用

前言 ​ 今天在配置springboot项目时遇到了一些问题&#xff0c;jdk版本与springboot版本不一致&#xff0c;在使用idea的脚手架创建项目时&#xff0c;idea的下载地址是spring的官方网站&#xff0c;这导致所下载的版本都是比较高的&#xff0c;而我们使用最多的jdk版本是jdk…

淘宝API接口开发系列:采集商品视频,属性,sku价格,详情图等

淘宝API接口开发是一个复杂的过程&#xff0c;涉及到与淘宝开放平台的对接&#xff0c;以及理解和使用其提供的API。如果你想采集商品视频、属性、SKU价格、详情图等信息&#xff0c;你需要遵循淘宝开放平台的开发者文档&#xff0c;并确保你的应用已经获得了适当的权限。 1.请…

关于C#程序(Windows窗体应用)的退出询问

在一般的软件中我们常常会发现当我们退出系统的时候&#xff0c;总会有提示 那我们来看看这个是怎么实现的&#xff1a; 首先单击退出按钮&#xff0c;进入到我们的退出按钮属性&#xff0c;点击闪电标志&#xff1a; 找到FormClosing&#xff0c;双击进入 进行代码写入&…

Learn SRP 02

3.Editor Rendering 3.1Drawing Legacy Shaders 因为我们的管线只支持无光照的着色过程&#xff0c;使用其他不同的着色过程的对象是不能被渲染的&#xff0c;他们被标记为不可见。尽管这是正确的&#xff0c;但是它还是隐藏了场景中一些使用错误着色器的对象。所以让我们来渲…

【Golang】并发编程之三大问题:原子性、有序性、可见性

目录 一、前言二、概念理解2.1 有序性2.2 原子性后果1&#xff1a;其它线程会读到中间态结果&#xff1a;后果2&#xff1a;修改结果被覆盖 2.3 可见性1&#xff09;store buffer(FIFO)引起的类似store-load乱序现象2&#xff09;store buffer(非FIFO)引起的类似store-store乱序…

代理模式(结构型模式)

目录 1、概述 2、结构 2.1、角色分类 2.2、类图 3、静态代理 3.1、案例类图 3.2、案例代码 4、JDK 动态代理 4.1、案例代码 4.2、底层原理 4.3、执行流程说明 5、CGLib 动态代理 5.1、案例代码 6、三种代理的对比 6.1、JDK代理和CGLib代理 6.2、动态代理和静态…

【Latex排版小记录】latex设置两端对齐

Latex排版的时候遇到了公式/英文过长超出来的情况 解决办法&#xff1a;在\begin{document}里面增加\begin{sloppypar} \begin{document} \begin{sloppypar}\end{sloppypar} \end{document}

Spring Boot - 利用MDC(Mapped Diagnostic Context)实现轻量级同步/异步日志追踪

文章目录 Pre什么是MDC&#xff08;Mapped Diagnostic Context&#xff09;Slf4j 和 MDC基础工程工程结构POMlogback-spring.xmlapplication.yml同步方式方式一&#xff1a; 拦截器自定义日志拦截器添加拦截器 方式二&#xff1a; 自定义注解 AOP自定义注解 TraceLog切面 测试…

Java实现优先级队列(堆)

前言 在学习完二叉树的相关知识后&#xff0c;我们对数据结构有了更多的认识&#xff0c;本文将介绍到优先级队列(堆&#xff09; 1.优先级队列 1.1概念 前面介绍过队列&#xff0c;队列是一种先进先出(FIFO)的数据结构&#xff0c;但有些情况下&#xff0c;操作的数据可能…