前言
最近博主在字节面试中遇到这样一个面试题,这个问题也是前端面试的高频问题,因为在前端开发的日常开发中我们经常会用到闭包,我们会借助闭包来封装一些工具函数,所以更深的了解闭包是很有必要的,博主在这给大家细细道来。
🚀 作者简介:程序员小豪,全栈工程师,热爱编程,曾就职于蔚来、腾讯,现就职于某互联网大厂,技术栈: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]()
打印 0
,result[1]()
打印 1
,以此类推。
因为 var
声明的 i
不只是属于当前的每一次循环,甚至不只是属于当前的 for
循环,因为没有块级作用域,变量 i
被提升到了函数 foo
的作用域中。所以每个函数的作用域链中都保存着同一个变量 i
,而当我们执行数组中的子函数时,此时 foo
内部的循环已经结束,此时 i = 10
,所以每个函数调用都会打印 10
。
接下来我们对 for
循环内部添加一层即时函数(又叫立即执行函数 IIFE
),形成一个新的闭包环境,这样即时函数内部就保存了本次循环的 i
,所以再次执行数组中子函数时,结果就像我们期望的那样 result[0]()
打印 0
,result[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后台与我沟通交流,也欢迎您扫描下方二维码直接加入到我的交流社群!