这是我参与更文挑战的第10天,活动详情查看: 更文挑战
前言
在上一篇文章 《 深入谈谈JavaScript的作用域及作用域链》中,我们详细的了解了JavaScript中的作用域和作用域链。有了作用域的知识作基础,再来看闭包相关的知识就很简单了,简直是易如反掌,手到擒来,那么我们今天就来谈谈JavaScript中的闭包。如果看完学会了请点个赞,最好关注一下(嘿嘿嘿...),然后评论“学会了”,如果学到了请评论“学到了”,千万不要出现“学会了,但没有完全学会”、“学到了,但没有完全学到”的情况~
好了,让我们一起开始愉快的学习吧!
闭包是个什么东东
闭包是 函数 和声明该函数的 词法环境 的组合。新手看了这句话可能会很懵,下面我来解释一下这句话:
函数:内部函数 声明该函数:外部函数 词法环境: 作用域(作用域链,详情见上篇文章) 组合:以上形成的综合体 理解了上面这句话的含义之后,我们就能知道 闭包的形成条件 :
两个为嵌套关系的函数,内部函数访问了外部函数的声明的变量。
由此我们可以看出,闭包的基本模型如下:
function outer(){
var num = 100
function inner(){
console.log(num)
}
return inner
}
var ret = outer()
ret()
闭包的作用
一,私有变量,保护数据的安全
很多时候,我们并不希望数据会被外部更改,我们希望数据的更改被限制在某个范围内,让数据的更改变的安全可维护。
下面我们来看一个利用闭包制作的计数器函数,在这个计数器函数中,我们要将count变量给保护起来,除了内部返回的fn函数可以修改count的值,其他地方都不可以可以修改count的值。如下代码:
// 功能:统计fn函数调用的次数
function outer(){
var count = 0
function fn(){
count++
console.log("函数fn 被调用了" + count + "次")
}
return fn
}
var ret = outer()
ret()
二,持久化数据
在上面的计数器函数中,我们会想到一个问题:为什么调用ret的时候,count的值是在++,而不是每次都从0开始?
这是因为函数在调用的时候,会在内存里面去开辟一块空间来执行函数内部的代码,当函数调用结束的时候,函数开辟的空间会被销毁掉。而在上面计数器函数中,count的值存在于outer函数中,outer函数作为一个整体不会被销毁,count的值自然也不会被销毁掉。
我们可以利用闭包这一特性做些什么呢?比如计算斐波那契数列的时候。
斐波那契数列(Fibonacci sequence),又称黄金分割数列,因数学家莱昂纳多·斐波那契(Leonardoda Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:0、1、1、2、3、5、8、13、21、34、……在数学上,斐波那契数列以如下被以递推的方法定义:F(0)=0,F(1)=1, F(n)=F(n - 1)+F(n - 2)(n ≥ 2,n ∈ N*)
现在我们要生成一个斐波那契数列,会有大量重复的计算,如果我们要获取斐波那契第1000个数值,用时多久呢?如下代码:
const fb = function() {
function fn(n) {
if (n === 1 || n === 2) {
return 1
}
var num = fn(n - 1) + fn(n - 2)
return num
}
return fn
}()
console.log(fb(100))
你会发现你的浏览器卡死了,因为大量重复的计算占了太多的内存空间。但如果使用闭包持久化数据(缓存)呢?如下代码:
const fb = function() {
var cache = [0,1] // 建立缓存区
function fn(n) {
var num = cache[n]
if(typeof num !== 'number'){ // 如果缓存区没有当前位数对应的斐波那契值
cache[n] = fn(n-1) + fn(n-2) // 当前位数对应的斐波那契值加入缓存
num = cache[n]
}
return num // 如果缓存区有当前位数对应的斐波那契值,直接返回
}
return fn
}()
console.log(fb(1000))
我们可以看出,即使计算的数值是1000位的数字,也是秒出结果。在斐波那契数列计算越复杂的情况,使用闭包的持久化数据越快。
闭包内存泄露
什么是内存泄露呢?通俗的讲,内存泄露是指声明的变量,没有被使用,在js执行结束之后,没有被销毁。也就是说,一块内存空间被占用了,一直得不到回收,别的对象不能再使用这块内存空间。
很多教程资料都说使用闭包一定会内存泄露,其实这种说法是不严谨的。
我们要明确一点:JavaScript中内存释放的机制不需要人为操控,是js引擎自动释放的。
说起来内存泄露,其实在 IE9 之前才会有闭包变量引起内存泄漏的问题。这是因为 IE9 之前采用的垃圾回收算法不是现在使用的标记-清除算法,而是引用计数算法。引用计数算法在处理 COM 对象(组件对象模型)会有循环引用的问题,而循环引用才是导致内存泄漏的元凶。在早期的 V8 中,由于闭包引用的变量被挂载了全局的大对象 windows 中,所以这一变量由老生代区采用标记-清除算法进行回收。频繁的垃圾回收会生成大量的内存碎片,所以也会导致内存泄漏问题。为了解决这一问题,以及频繁的垃圾回收导致的全停顿问题(垃圾回收在主线程执行),后来 v8 又采用了标记-清除整理算法,以及增量回收、并行回收、并发回收等垃圾回收技术。
一,引用计数算法
js会自动去分配内存,存储对象,定期检查对象被引用次数,以引用次数来判断是否回收。如下代码:
// 把创建对象的地址赋值给变量obj,这个对象被引用了,这个对象引用计数为1(此时这个对象不会被垃圾回收掉)
var obj = {
name: "大冰块"
}
// 把obj的地址赋值给变量one,one也指向了对象,这个对象被引用了2次(不会被回收)
var one = obj
// 此时修改obj = 1,obj不指向对象了,但是这个对象被引用了1次不会被回收)
obj = 1
// 此时修改one = null,让one也不指向对象了,对象被引用了0次,0次引用的对象,被标记为垃圾内存,被垃圾回收机制给回收掉。
one = null
引用计数存在的缺陷:循环引用的错误。如下代码:
function fn(){
var obj1 = {} // {}这个对象被引用了1次
var obj2 = {} // {}这个对象被引用了2次
obj1.a = obj2 // {}这个对象被引用了3次
obj2.b = obj1 // {}这个对象被引用了4次
}
// 调用fn结束,理应要销毁obj1、obj2这两个对象,但是由于这两个对象都不是0次引用对象,不能够被回收。
fn()
fn()
fn()
fn()
fn()
// 如果此时多次调用这个fn函数,就会造成内存泄漏的问题。
二,标记-清除算法
直接从window上开始找,从window这个根对象往下查找,如果找不到引用了这个对象的变量,该对象就作为垃圾内存回收掉。所以标记-清除算法解决了循环引用的问题。
var obj = {
name: "大冰块"
}
obj = null // 没有再引用 `{name: "大冰块"}` 这个对象,该对象被回收掉。
从2012年起,所有现代浏览器都使用了标记-清除垃圾回收算法,所以在新一代浏览器中,使用闭包几乎不会出现内存泄漏问题。
当然,关于遇到网页占用内存过大的问题,如果是因为闭包内存泄露,我们还有终极大招,那就是手动释放闭包占用的内存:
function outer(){
...
}
var ret = outer()
ret = null
后记
相信通过本文的学习,你一定对闭包有了更深的理解与认识,再也不怕在面试中遇到了它了。最后我再举个例子,就好像这篇文章你看完了,点了个赞,加入了收藏夹,关注了作者,这些都是在你的闭包领域做的,别人并不能去把你的赞取消,收藏取消,关注也取消。懂了么?
PS: 今天是我参加掘金更文挑战的第10天,没有存稿的我,日日努力淦文章,已经坚持10天啦!有志者,事竟成。大家一起加油哦~
更文挑战的文章目录如下:
- 2021.06.01 《多图预警!详细谈谈Flex布局的容器元素和项目元素的属性~》
- 2021.06.02 《如何把css渐变背景玩出花样来》
- 2021.06.03 《如何使用SVG制作沿任意路径排布的文字效果》
- 2021.06.04 《3大类15小类前端代码规范,让团队代码统一规范起来!》
- 2021.06.05 《团队管理之git提交规范:大家竟然都不会写commit记录?| 周末学习》
- 2021.06.06 《如何控制css鼠标样式以及扩大鼠标点击区域 | 周末学习》
- 2021.06.07 《 纯css实现:仿掘金账户密码登录时,小熊猫捂眼动作切换的小彩蛋》
- 2021.06.08 《 从11个方面详细谈谈原型和原型链》
- 2021.06.09 《 深入谈谈JavaScript的作用域及作用域链》