当前位置: 首页 > news >正文

闭包、闭包应用场景

什么是闭包

要理解 JavaScript 中的闭包,需要先知道以下两个知识点:

  • JavaScript 中的作用域和作用域链
  • JavaScript 中的垃圾回收

回顾这两个知识点:

1. JavaScript 中的作用域和作用域链

  • 作用域就是一个独立的地盘,让变量不会外泄、暴露出去,不同作用域下同名变量不会有冲突。
  • 作用域在定义时就确定,并且不会改变。
  • 如果在当前作用域中没有查到值,就会向上级作用域去查,直到查到全局作用域,这么一个查找过程形成的链条就叫做作用域链。

2. JavaScript 中的垃圾回收

  • Javascript 执行环境会负责管理代码执行过程中使用的内存,其中就涉及到一个垃圾回收机制
  • 垃圾收集器会定期(周期性)找出那些不再继续使用的变量,只要该变量不再使用了,就会被垃圾收集器回收,然后释放其内存。如果该变量还在使用,那么就不会被回收。

有了这 2 个知识点的铺垫后,接下来再来看什么是闭包。

闭包不是一个具体的技术,而是一种现象,是指在定义函数时,周围环境中的信息可以在函数中使用。换句话说,执行函数时,只要在函数中使用了外部的数据,就创建了闭包。

而作用域链,正是实现闭包的手段。

什么?只要在函数中使用了外部的数据,就创建了闭包?

真的是这样么?可以证明一下:

在这里插入图片描述

在上面的代码中,我们在函数 a 中定义了一个变量 i,然后打印这个 i 变量。对于 a 这个函数来讲,自己的函数作用域中存在 i 这个变量,所以我们在调试时可以看到 Local 中存在变量 i

下面我们将上面的代码稍作修改,如下图:

在这里插入图片描述
在上面的代码中,我们将声明 i 这个变量的动作放到了 a 函数外面,也就是说 a 函数在自己的作用域已经找不到这个 i 变量了,它会怎么办?

学习了作用域链之后肯定知道,它会顺着作用域链一层一层往外找。然而上面在介绍闭包时说过,如果出现了这种情况,也就是函数使用了外部的数据的情况,就会创建闭包。

仔细观察调试区域,我们会发现此时的 i 就放在 Closure 里面的,从而证实了我们前面的说法。

当一个词特别难理解的时候,可以使用拆词法:“闭”可以理解为“封闭,闭环”,“包”可以理解为“一个类似于包裹的空间”,因此闭包实际上可以看作是一个封闭的空间,那么这个空间用来干啥呢?实际上就是用来存储变量的。

image-20211227163947135

那么是一个函数下所有的变量声明都会被放入到闭包这个封闭的空间里面么?

倒也不是,放不放入到闭包中,要看其他地方有没有对这个变量进行引用,例如:

在这里插入图片描述

在上面的代码中,函数 c 中一个变量都没有创建,却要打印 i、j、kx,这些变量分别存在于 a、b 函数以及全局作用域中,因此创建了 3 个闭包,全局闭包里面存储了 i 的值,闭包 a 中存储了变量 jk 的值,闭包 b 中存储了变量 x 的值。

但是仔细观察,就会发现函数 b 中的 y 变量并没有被放在闭包中,所以要不要放入闭包取决于该变量有没有被引用。

当然,此时可能会有这样的一个新问题,那么多闭包,岂不是占用内存空间么?

实际上,如果是自动形成的闭包,是会被销毁掉的。例如:

在这里插入图片描述

在上面的代码中,我们在第 16 行尝试打印输出变量 x,显然这个时候是会报错的,在第 16 行打一个断点调试就可以清楚的看到,此时已经没有任何闭包存在,垃圾回收器会自动回收没有引用的变量,不会有任何内存占用的情况。

当然,这里指的是自动产生闭包的情况,关于闭包,有时需要根据需求手动的来制造一个闭包。

来看下面的例子:

function eat(){
    var food = "鸡翅";
    console.log(food);
}
eat(); // 鸡翅
console.log(food); // 报错

在上面的例子中,声明了一个名为 eat 的函数,并对它进行调用。

JavaScript 引擎会创建一个 eat 函数的执行上下文,其中声明 food 变量并赋值。

当该方法执行完后,上下文被销毁,food 变量也会跟着消失。这是因为 food 变量属于 eat 函数的局部变量,它作用于 eat 函数中,会随着 eat 的执行上下文创建而创建,销毁而销毁。所以当我们再次打印 food 变量时,就会报错,告诉我们该变量不存在。

但是我们将此代码稍作修改:

function eat(){
    var food = '鸡翅';
    return function(){
        console.log(food);
    }
}
var look = eat();
look(); // 鸡翅
look(); // 鸡翅

在这个例子中,eat 函数返回一个函数,并在这个内部函数中访问 food 这个局部变量。调用 eat 函数并将结果赋给 look 变量,这个 look 指向了 eat 函数中的内部函数,然后调用它,最终输出 food 的值。

为什么能访问到 food,原因很简单,上面说过,垃圾回收器只会回收没有被引用到的变量,但是一旦一个变量还被引用着的,垃圾回收器就不会回收此变量。在上面的示例中,照理说 eat 调用完毕 food 就应该被销毁掉,但是我们向外部返回了 eat 内部的匿名函数,而这个匿名函数有引用了 food,所以垃圾回收器是不会对其进行回收的,这也是为什么在外面调用这个匿名函数时,仍然能够打印出 food 变量的值。

至此,闭包的一个优点或者特点也就体现出来了,那就是:

  • 通过闭包可以让外部环境访问到函数内部的局部变量。
  • 通过闭包可以让局部变量持续保存下来,不随着它的上下文环境一起销毁。

通过此特性,可以解决一个全局变量污染的问题。早期在 JavaScript 还无法进行模块化的时候,在多人协作时,如果定义过多的全局变量 有可能造成全局变量命名冲突,使用闭包来解决功能对变量的调用将变量写到一个独立的空间里面,从而能够一定程度上解决全局变量污染的问题。

例如:

var name = "GlobalName";
// 全局变量
var init = (function () {
    var name = "initName";
    function callName() {
        console.log(name);
        // 打印 name
    }
    return function () {
        callName();
        // 形成接口
    }
}());
init(); // initName
var initSuper = (function () {
    var name = "initSuperName";
    function callName() {
        console.log(name);
        // 打印 name
    }
    return function () {
        callName();
        // 形成接口
    }
}());
initSuper(); // initSuperName

最后,对闭包做一个总结:

  • 闭包是一个封闭的空间,里面存储了在其他地方会引用到的该作用域的值,在 JavaScript 中是通过作用域链来实现的闭包。

  • 只要在函数中使用了外部的数据,就创建了闭包,这种情况下所创建的闭包,我们在编码时是不需要去关心的。

  • 我们还可以通过一些手段手动创建闭包,从而让外部环境访问到函数内部的局部变量,让局部变量持续保存下来,不随着它的上下文环境一起销毁。

闭包经典问题

for (var i = 1; i <= 3; i++) {
    setTimeout(function () {
        console.log(i);
    }, 1000);
}

在上面的代码中,预期的结果是过 1 秒后分别输出 i 变量的值为 1,2,3。但是,执行的结果是:4,4,4

实际上,问题就出在闭包身上。循环中的 setTimeout 调用了一个匿名函数访问了它的外部变量 i,形成闭包。

i 变量只有 1 个,所以循环 3 次的 setTimeout 中都访问的是同一个变量。循环到第 4 次,i 变量增加到 4,不满足循环条件,循环结束,代码执行完后上下文结束。但是,那 3setTimeout1 秒钟后才执行,由于闭包的原因,所以它们仍然能访问到变量 i,不过此时 i 变量值已经是 4 了。

要解决这个问题,我们可以让 setTimeout 中的匿名函数不再访问外部变量,而是访问自己内部的变量,如下:

for (var i = 1; i <= 3; i++) {
    (function (index) {
        setTimeout(function () {
            console.log(index);
        }, 1000);
    })(i)
}

这样 setTimeout 中就可以不用访问 for 循环声明的变量 i 了。而是采用调用函数传参的方式把变量 i 的值传给了 setTimeout,这样它们就不再创建闭包,因为在我自己的作用域里面能够找到 i 这个变量。

当然,解决这个问题还有个更简单的方法,就是使用 ES6 中的 let 关键字。

它声明的变量有块作用域,如果将它放在循环中,那么每次循环都会有一个新的变量 i,这样即使有闭包也没问题,因为每个闭包保存的都是不同的 i 变量,那么刚才的问题也就迎刃而解。

for (let i = 1; i <= 3; i++) {
    setTimeout(function () {
        console.log(i);
    }, 1000);
}

闭包是什么?闭包的应用场景有哪些?怎么销毁闭包?

闭包是一个封闭的空间,里面存储了在其他地方会引用到的该作用域的值,在 JavaScript 中是通过作用域链来实现的闭包。

只要在函数中使用了外部的数据,就创建了闭包,这种情况下所创建的闭包,我们在编码时是不需要去关心的。

我们还可以通过一些手段手动创建闭包,从而让外部环境访问到函数内部的局部变量,让局部变量持续保存下来,不随着它的上下文环境一起销毁。

使用闭包可以解决一个全局变量污染的问题。

如果是自动产生的闭包,我们无需操心闭包的销毁,而如果是手动创建的闭包,可以把被引用的变量设置为 null,即手动清除变量,这样下次 JavaScript 垃圾回收器在进行垃圾回收时,发现此变量已经没有任何引用了,就会把设为 null 的量给回收了。


以上笔记整理于渡一教育谢老师课堂

相关文章:

  • 瑞吉外卖 —— 2、后台登录和登出
  • 阿里云配置hadoop远程连接Web页面(如50070端口)
  • Linux 任务调度
  • Github每日精选(第32期):HTML智能仪表盘tabler
  • 对多个 CSV 文件做关联
  • 记录一次数据库CPU被打满的排查过程
  • 金九银十准备季:Java IO流面试题(含答案)
  • Kubeadm方式快速搭建K8S集群1.20版本
  • 手把手开发Admin 系列一(设计篇)
  • java计算机毕业设计西安财经大学校园一卡通管理系统源码+系统+数据库+lw文档+mybatis+运行部署
  • 丁鹿学堂前端培训:前端性能优化css篇(一)
  • MASA Stack 第四期社区例会
  • 钓鱼 - deb 包篡改
  • Jackson关于Western Blot、IHC 和 ELISA 的显色检测分析
  • ES7+知识点整理使用
  • 【翻译】babel对TC39装饰器草案的实现
  • 5分钟即可掌握的前端高效利器:JavaScript 策略模式
  • co.js - 让异步代码同步化
  • co模块的前端实现
  • iOS动画编程-View动画[ 1 ] 基础View动画
  • JavaScript HTML DOM
  • js写一个简单的选项卡
  • vuex 笔记整理
  • 多线程事务回滚
  • 前端知识点整理(待续)
  • 让你成为前端,后端或全栈开发程序员的进阶指南,一门学到老的技术
  • 写给高年级小学生看的《Bash 指南》
  • 要让cordova项目适配iphoneX + ios11.4,总共要几步?三步
  • 资深实践篇 | 基于Kubernetes 1.61的Kubernetes Scheduler 调度详解 ...
  • ​软考-高级-系统架构设计师教程(清华第2版)【第15章 面向服务架构设计理论与实践(P527~554)-思维导图】​
  • #include
  • $(document).ready(function(){}), $().ready(function(){})和$(function(){})三者区别
  • $redis-setphp_redis Set命令,php操作Redis Set函数介绍
  • (1/2) 为了理解 UWP 的启动流程,我从零开始创建了一个 UWP 程序
  • (Redis使用系列) Springboot 整合Redisson 实现分布式锁 七
  • (十八)SpringBoot之发送QQ邮件
  • (四)TensorRT | 基于 GPU 端的 Python 推理
  • (转)GCC在C语言中内嵌汇编 asm __volatile__
  • (转)http-server应用
  • ******之网络***——物理***
  • .bat批处理(十一):替换字符串中包含百分号%的子串
  • .Net Remoting常用部署结构
  • .NET 使用 JustAssembly 比较两个不同版本程序集的 API 变化
  • .net打印*三角形
  • /etc/sudoer文件配置简析
  • @基于大模型的旅游路线推荐方案
  • [ C++ ] STL priority_queue(优先级队列)使用及其底层模拟实现,容器适配器,deque(双端队列)原理了解
  • [Android] Upload package to device fails #2720
  • [Android] 修改设备访问权限
  • [Angular 基础] - 表单:响应式表单
  • [C#基础知识]专题十三:全面解析对象集合初始化器、匿名类型和隐式类型
  • [C/C++]数据结构 栈和队列()
  • [C++] cout、wcout无法正常输出中文字符问题的深入调查(1):各种编译器测试
  • [Deepin 15] 编译安装 MySQL-5.6.35
  • [EMWIN]FRAMEWIN 与 WINDOW 的使用注意