字节一面:闭包是什么?闭包的用途是什么?

前言

最近博主在字节面试中遇到这样一个面试题,这个问题也是前端面试的高频问题,因为在前端开发的日常开发中我们经常会用到闭包,我们会借助闭包来封装一些工具函数,所以更深的了解闭包是很有必要的,博主在这给大家细细道来。


🚀 作者简介:程序员小豪,全栈工程师,热爱编程,曾就职于蔚来、腾讯,现就职于某互联网大厂,技术栈:Vue、React、Python、Java
🎈 本文收录于小豪的前端系列专栏,后续还会更新前端入门以及前端面试的一些相关文章,手把手带你从零学习前端到面试找工作,并如果有想进入前端领域工作的同学,这个前端专栏会对你有所帮助,欢迎关注起来呀
🌼 本人也会持续的去关注AIGC以及人工智能领域的一些动向并总结到博客中,大家感兴趣的可以关注一下我的人工智能专栏
🌊 云原生的入门学习系列,大家有兴趣的可以看一看

本文目录

  • 什么是闭包
  • 应用场景
    • 实现块级作用域
    • 保存内部状态
    • 函数柯里化
    • 单例模式
    • 模拟私有属性
  • 闭包的缺点
    • 如何解决闭包导致的内存泄漏?
  • 总结
    • 个人结语

什么是闭包

闭包就是每次调用外层函数时,临时创建的函数作用域对象。
因为内层函数作用域链中包含外层函数的作用域对象,且内层函数被引用,导致内层函数不会被释放,同时它又保持着对父级作用域的引用,这个时候就形成了闭包。
所以闭包通常是在函数嵌套中形成的。
例如下面的代码,就形成了闭包:

javascript
复制代码function foo (){var name = 'snail'return function(){console.log('my name is '+name)}
}
var bar = foo();
bar();

闭包并不是一个需要学习新的语法或模式才能使用的工具或技巧,它是基于词法作用域编写代码时自然产生的结果。你甚至不需要为了闭包而创建闭包,了解了闭包,你会发现,代码中闭包随处可见。
了解闭包是为了可以根据自己的意愿来识别和影响闭包的使用。接下来我们看一下闭包常见的应用场景。

应用场景

实现块级作用域

首先我们来看这样一段代码:

function foo(){var result = [];for(var i = 0;i<10;i++){result[i] = function(){console.log(i)}}return result;
}
var result = foo();
result[0](); // 10
result[1](); // 10

可以看到,每个函数并不像我们期待的那样 result[0]() 打印 0result[1]() 打印 1,以此类推。
因为 var 声明的 i 不只是属于当前的每一次循环,甚至不只是属于当前的 for 循环,因为没有块级作用域,变量 i 被提升到了函数 foo 的作用域中。所以每个函数的作用域链中都保存着同一个变量 i,而当我们执行数组中的子函数时,此时 foo 内部的循环已经结束,此时 i = 10,所以每个函数调用都会打印 10

接下来我们对 for 循环内部添加一层即时函数(又叫立即执行函数 IIFE),形成一个新的闭包环境,这样即时函数内部就保存了本次循环的 i,所以再次执行数组中子函数时,结果就像我们期望的那样 result[0]() 打印 0result[1]() 打印 1

function foo(){var result = [];for(var i = 0;i<10;i++){(function(i){result[i] = function(){console.log(i)}})(i)}return result;
}
var result = foo();
result[0](); // 0
result[1](); // 1

保存内部状态

首先我们来看这样一段代码:

function cacheCalc(){var cache = new Map()return function (i){if(!cache.has(i)) cache.set(i,i*10)return cache.get(i)}
}var calc = cacheCalc()
console.log(calc(2)) // 20

可以看到,函数内部会使用 Map 保存已经计算过的结果(当然也可以是其他的数据结构),只有当输入数字没有被计算过时,才会计算,否则会返回之前的计算结果,这样就会避免重复计算。

而这样的技巧在 Vue3源码 中同样有使用到。代码地址
这里我在阅读源码的过程中加了一些注释,导致截图中代码行号和源文件中的不一致,但是代码并未进行任何修改。

在这里插入图片描述
这里的 compileToFunction 函数会将我们编写的模板进行编译生成 render 函数,而为了避免重复编译,这里在内部创建了一个 compileCache 对象保存编译过的数据。

函数柯里化

首先说一下什么是函数柯里化?
柯里化是把接收多个参数的函数变成接收单一参数(最初函数的第一个参数)的函数,并且返回接收余下的参数且返回结果的新函数。
翻译成人话就是可以将一个接受多个参数的函数分解成多个接收单个参数的函数的技术,直到接收的参数满足了原来所需的数量后,才执行原函数的逻辑。
例如一个非常经典的面试题 => 实现 add(x)(y)(z) = x+y+z 中就用到了函数柯里化。代码如下:

function add(x){return function(y){return function(z){return x+y+z}}
}
console.log(add(1)(2)(3)) // 6

再比如我们有一个函数 foo,可以将输入的数字保留两位小数。此时我们需要一个函数,可以把输入数字保留两位小数并每隔三位添加一个逗号,这个时候就可以把函数 foo 引入进来,并在之前结果的基础上添加每隔三位添加逗号的功能。

单例模式

单例模式是一种常见的涉及模式,它保证了一个类只有一个实例。实现方法一般是先判断实例是否存在,如果存在就直接返回,否则就创建了再返回。单例模式的好处就是避免了重复实例化带来的内存开销:

// 单例模式
function Singleton(){this.data = 'singleton';
}Singleton.getInstance = (function () {var instance;return function(){if (instance) {return instance;} else {instance = new Singleton();return instance;}}
})();var sa = Singleton.getInstance();
var sb = Singleton.getInstance();
console.log(sa === sb); // true
console.log(sa.data); // 'singleton'

模拟私有属性

javascript 没有 java 中那种 public private 的访问权限控制,对象中的所用方法和属性均可以访问,这就造成了安全隐患,内部的属性任何开发者都可以随意修改。虽然语言层面不支持私有属性的创建,但是我们可以用闭包的手段来模拟出私有属性:

// 模拟私有属性
function getGeneratorFunc () {var _name = 'John';var _age = 22;return function () {return {getName: function () {return _name;},getAge: function() {return _age;}};};
}var obj = getGeneratorFunc()();
obj.getName(); // John
obj.getAge(); // 22
obj._age; // undefined

闭包的缺点

从上面的介绍中我们可以得知,闭包的使用场景非常广泛,那我们是不是可以大量使用闭包呢?不可以,因为闭包过度使用会导致性能问题,还是看之前演示的一段代码:

function foo() {var a = 2;function bar() {console.log( a );}return bar;
}var baz = foo();baz(); // 这就形成了一个闭包

乍一看,好像没什么问题,然而,它却有可能导致 内存泄露

我们知道,javascript 内部的垃圾回收机制用的是引用计数收集:即当内存中的一个变量被引用一次,计数就加一。垃圾回收机制会以固定的时间轮询这些变量,将计数为 0 的变量标记为失效变量并将之清除从而释放内存。

上述代码中,理论上来说, foo 函数作用域隔绝了外部环境,所有变量引用都在函数内部完成,foo 运行完成以后,内部的变量就应该被销毁,内存被回收。然而闭包导致了全局作用域始终存在一个 baz 的变量在引用着 foo 内部的 bar 函数,这就意味着 foo 内部定义的 bar 函数引用数始终为 1,垃圾运行机制就无法把它销毁。更糟糕的是,bar 有可能还要使用到父作用域 foo 中的变量信息,那它们自然也不能被销毁… JS 引擎无法判断你什么时候还会调用闭包函数,只能一直让这些数据占用着内存。

这种由于闭包使用过度而导致的内存占用无法释放的情况,我们称之为:内存泄露。

如何解决闭包导致的内存泄漏?

返回的函数调用后,把外部的引用关系置空

function fn2(){let test = new Array(1000).fill('isboyjc')return function(){console.log(test)return test}
}
let fn2Child = fn2()
fn2Child()
fn2Child = null

总结

本期博客详细介绍了闭包是什么,闭包的应用场景,闭包的缺点以及如何解决闭包导致的内存泄漏问题,跟着这篇博文认真的学习下来相信下次面试官再问你闭包的问题你不会在惧怕,甚至能够回答的面面俱到让面试官眼前一亮。

后续我们这个前端专栏还会讲述ES6垃圾回收js算法技巧Vue入门实战React入门实战前端面试题等等文章,如果您感兴趣的话,欢迎点赞三连并关注我以及我的前端专栏,我们下期文章再见。

个人结语

各位看官老爷们好,小豪已经建立了技术交流群,如果你很感兴趣,可以私信我加入我的社群。

📝社群中不定时会有很多活动,例如学习资料分享、大厂面经分享、技术讨论、行业大佬创业杂谈等等。

📝本人目前是在互联网大厂正式工作,也有过多个大厂的工作经历,加入社群也会有简历修改辅导,模拟面试,手把手项目实战教学,大厂工作内推机会以及大厂面试题解析分享等福利。

📝社群方向很多,相关领域有Web全栈(前后端)、人工智能、AIGC、自媒体变现、前沿科技文章分享、论文精读等等。

📝不管你是多新手的小白,都欢迎你加入社群中讨论、聊天、分享,加速助力你成为下一个技术大佬!也随时欢迎您跟我沟通,一起交流,一起成长。变现、进步、技术、资料、项目、你想要的这里都会有

📝网络的风口只会越来越大,风浪越大,鱼越贵!欢迎您加入社群~一个人可以或许可以走的很快,但一群人将走的更远!

📝想都是问题,做都是答案!行动起来吧!欢迎评论区or后台与我沟通交流,也欢迎您扫描下方二维码直接加入到我的交流社群!

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

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

相关文章

自动驾驶感知传感器标定安装说明

1. 概述 本标定程序为整合现开发的高速车所有标定模块,可实现相机内参标定和激光、相机、前向毫米波 至车辆后轴中心标定,标定参数串联传递并提供可视化工具验证各个模块标定精度。整体标定流程如下,标定顺序为下图前标0-->1-->2-->3,相同编号标定顺序没有强制要求…

Python学习笔记:正则表达式、逻辑运算符、lamda、二叉树遍历规则、类的判断

1.正则表达式如何写&#xff1f; 序号实例说明1.匹配任何字符(除换行符以外)2\d等效于[0-9]&#xff0c;匹配数字3\D等效于[^0-9]&#xff0c;匹配非数字4\s等效于[\t\r\n\f]&#xff0c;匹配空格字符5\S等效于[^\t\r\n\f]&#xff0c;匹配非空格字符6\w等效于[A-Za-z0-9]&…

<JDBC>

文章目录 1.JDBC核心技术1.数据的持久化2.JAVA中的数据存储技术3.JDBC介绍4.JDBC体系结构5.JDBC程序编写步骤 2.获取数据库连接1.Driver接口实现类2.注册与加载JDBC驱动3.URL4.用户和密码 3. PreparedStatement 和 Statement1.PreparedStatement介绍2. PreparedStatement vs St…

CMake3.27+OpenCV4.8+VS2019+CUDA配置

1、准备工作 CMake3.27OpenCV4.8opencv_contrib-4.8.0CUDACUDNNTensorRT下载好并安装cuda 2、正式开始安装 启动CMake开始配置 打开刚解压的cmake文件夹中找到bin目录下的cmake-gui.exe 点击cmake中左下角的 Configure进行第一次配置&#xff0c;会弹出选择环境对话框 再点击Fi…

HodlSoftware-免费在线PDF工具箱 加解密PDF 集成隐私保护功能

HodlSoftware是什么 HodlSoftware是一款免费在线PDF工具箱&#xff0c;集合编辑 PDF 的简单功能&#xff0c;可以对PDF进行加解密、优化压缩PDF、PDF 合并、PDF旋转、PDF页面移除和分割PDF等操作&#xff0c;而且工具集成隐私保护功能&#xff0c;文件只在浏览器本地完成&…

windows系统依赖环境一键安装

window系统程序依赖库&#xff0c;可以联系我获取15958139685 脚本代码如下&#xff0c;写到1. bat文件中&#xff0c;双击直接运行&#xff0c;等待安装完成即可 Scku.exe -AVC.exe /SILENT /COMPONENTS"icons,ext\reg\shellhere,assoc,assoc_sh" /dir%1\VC

Wireshark流量分析

目录 1.基本介绍 2.基本使用 1&#xff09;数据包筛选: 2&#xff09;筛选ip&#xff1a; 3&#xff09;数据包还原 4&#xff09;数据提取 3.wireshark实例 1.基本介绍 在CTF比赛中&#xff0c;对于流量包的分析取证是一种十分重要的题型。通常这类题目都是会提供一个包含…

PHP之 导入excel表格时,获取日期时间变成浮点数

读取到的时间 float(0.20833333333333) 原格式 15:00:00 代码 if (Request::isPost()) {$file_url input(upfile); // 本地上传文件地址// 读取文件内容$local_file_url __dir__./../../../public.$file_url;// $spreadsheet new Spreadsheet();// $sheet $spreadsheet-…

SQL中ON筛选和Where筛选的区别

转载&#xff1a;sql连接查询中on筛选与where筛选的区别https://zhuanlan.zhihu.com/p/26420938 结论:on后面接上连接条件&#xff0c;where后面接上过滤条件

设计模式--建造者模式(Builder Pattern)

一、什么是建造者模式 建造者模式&#xff08;Builder Pattern&#xff09;是一种创建型设计模式&#xff0c;它关注如何按照一定的步骤和规则创建复杂对象。建造者模式的主要目的是将一个复杂对象的构建过程与其表示分离&#xff0c;从而使同样的构建过程可以创建不同的表示。…

配置zookeeper

配置zookeeper_3.5.7 1.配置zookeeper2.zookeeper案例 1.配置zookeeper tar -zxvf apache-zookeeper-3.5.7-bin.tar.gz -C /opt/module/ mv apache-zookeeper-3.5.7-bin/ zookeeper-3.5.7# 在/opt/module/zookeeper-3.5.7/这个目录下创建 zkData mkdir zkData #在/opt/module/…

【Redis】Redis是什么、能干什么、主要功能和工作原理的详细讲解

&#x1f680;欢迎来到本文&#x1f680; &#x1f349;个人简介&#xff1a;陈童学哦&#xff0c;目前学习C/C、算法、Python、Java等方向&#xff0c;一个正在慢慢前行的普通人。 &#x1f3c0;系列专栏&#xff1a;陈童学的日记 &#x1f4a1;其他专栏&#xff1a;CSTL&…

Kubernetes对象深入学习之五:TypeMeta无效之谜

欢迎访问我的GitHub 这里分类和汇总了欣宸的全部原创(含配套源码)&#xff1a;https://github.com/zq2599/blog_demos 本篇概览 本文是《Kubernetes对象深入学习之五》系列的第五篇&#xff0c;从前文的分析也能看出&#xff0c;代表对象类型的schema.ObjectKind&#xff0c;于…

Git企业开发控制理论和实操-从入门到深入(三)|分支管理

前言 那么这里博主先安利一些干货满满的专栏了&#xff01; 首先是博主的高质量博客的汇总&#xff0c;这个专栏里面的博客&#xff0c;都是博主最最用心写的一部分&#xff0c;干货满满&#xff0c;希望对大家有帮助。 高质量博客汇总 然后就是博主最近最花时间的一个专栏…

Vue2学习笔记のVue中的ajax

目录 Vue中的ajaxvue脚手架配置代理方法一方法二 插槽 hello, 这篇文章是Vue2学习笔记的第四篇&#xff0c;也是第四章&#xff1a;Vue中的ajax。 Vue中的ajax vue脚手架配置代理 方法一 在vue.config.js中添加如下配置&#xff1a; devServer:{proxy:"http://localho…

go-kafka

go kafka包 本文使用的是kafka-go 6.5k 这个包 其他包参考&#xff1a; 我们在细分市场中非常依赖GO和Kafka。不幸的是&#xff0c;在撰写本文时&#xff0c;Kafka的GO客户库的状态并不理想。可用选项是&#xff1a; 萨拉玛&#xff08;Sarama&#xff09; 10k&#xff0c;这…

10.Oracle中decode函数

【函数格式】&#xff1a; decode ( expression, condition_01, result_01, condition_02, result_02, ......, condition_n, result_n, result_default) 【函数说明】&#xff1a; 若表达式expression值与condition_01值匹配&#xff0c;则返回result_01&#xff0c;…

CPU深度解析

操作系统课程 计算机组成 ALU:计算单元(运算器)PC:pc寄存器存执行指令Registers:寄存器存数据MMU:控制器程序的构成:指令+数据 总线:一个程序读入内存,全是由0和1构成,从内存读取到cpu计算,需要通过总线。一段01数据段是指令还是数据是通过来源总线区分的。总线分…

flutter和原生利用pigeon建立通道

首先导入依赖&#xff1a; dependencies: pigeon: ^10.0.0定义一个文件&#xff1a; /// 用于定于flutter和平台的桥接方法 /// HostApi() 标记的&#xff0c;是用于 Flutter 调用原生的方法&#xff1b; /// FlutterApi() 标记的&#xff0c;是用于原生调用 Flutter 的方法&…

Netty入门学习和技术实践

Netty入门学习和技术实践 Netty1.Netty简介2.IO模型3.Netty框架介绍4. Netty实战项目学习5. Netty实际应用场景6.扩展 Netty 1.Netty简介 Netty是由JBOSS提供的一个java开源框架&#xff0c;现为 Github上的独立项目。Netty提供异步的、事件驱动的网络应用程序框架和工具&…