文章目录
- 一、闭包是什么? 🤦♂️
- 二、闭包 😎
- 三、使用场景 😁
- 四、使用场景(2) 😁
- 五、闭包的原理
- 六、思考
- 总结
- 一、 更深层次了解闭包,分析以下代码执行过程
- 二、闭包
- 三、闭包定义
- 四、闭包导致的内存泄漏
- 面试题
一、闭包是什么? 🤦♂️
当一个函数的返回值又是一个函数,作为返回值的那个函数叫做闭包
它利用了 作用域的嵌套 ,将原本的局部变量进化成私有变量的环境,叫闭包
- 闭包由两部分组成 :函数+可以访问的自由变量
一个普通的函数function,如果它可以访问外层作用于的自由变量,那么这个函数就是一个闭包
二、闭包 😎
const f = (function() {
let a = 0;
return function() {
a++;
console.log(a);
};
})();
f();
f();
f();
好兄弟,看!这是一个闭包函数,我们先来分析下这个函数
()() 他叫做立即运行的匿名函数,当一个匿名函数被括起来,然后再在后面加一个括号,这个匿名函数就能立即运行起来
假设我们删去 这个匿名函数,f 是这个最外层的Function,当f()执行时,返回里层的function,
f()()才能调用里层的function
假设去掉,咱们接下往下走
f 其实就是这个函数的返回值 return 出来的函数
第一段代码执行 f 之后,发现a++了,因为里层的函数执行了,那为什么我再最下面console.log(a) 报错呢??
......
console.log(a);
我们都知道let 声明的变量有作用域,不会变量提升,而对于内部的function来说,他是可以拿到a的,因为他们同一作用域(外层),a是个局部变量,所以在外层拿不到;
这也是闭包的 特点 :将原本局部作用域中的局部变量,临时保存起来,不受外部的作用域的影响,可以被外部,反复使用
,刚好说明了我们第二条定义,利用了 “ 作用域的嵌套 ”, 将原本的局部变量进化成私有变量的环境,叫闭包
三、使用场景 😁
举两个小例子,让我们看看闭包的使用场景,看完以下两个例子,好兄弟你能不能想起来对闭包的定义
,以及对闭包特点作用
?
var allLi = document.querySelectorAll("li");
for (var i = 0; i < allLi.length; i++) {
console.log(i); //这里我每次都能拿到i
allLi[i].onclick = (function(i) {
return function() {
console.log(i);
};
})(i);
}
// 当我点击的时候,真正触发的时retrun出来的function
// 借助匿名函数帮助我们创建了一个作用域
// 不管里层函数什么时候执行,都能拿到匿名函数里的变量
for (let i = 0; i < allLi.length; i++) {
console.log(i); //这里我每次都能拿到i
allLi[i].onclick = function(i) {
console.log(i)
}
}
// 这个其实也是一个闭包,因为let 有作用域
函数的返回值又是一个函数
利用作用域的嵌套
,将原本的局部变量
进化成私有变量
的环境 ,叫闭包
四、使用场景(2) 😁
给不能传参
的 内置函数
的回调参数
传参
let a = "hello";
setTimeout(function(a) {
console.log(a);
}, 1000);
三个问题
- setTimeout 是什么类型函数?
- 内置函数可以传参吗?
- 如果可以,怎么传?就像是上面那个案例吗?
直接说明
:上个案例是错误写法,首先setTimeout是window内置的定时器函数,其次他不能传参,因为是内置!!!那有没有解决方案呢?yes!yes!yes! ✌
看答案之前先思考两个问题
- setTimeout 接收两个参数,第一参数是什么?
- 为什么上个案例返回undifine?
setTimeout(fn("hello"), 5000);
function fn(a) {
return function() {
console.log(a);
};
}
说明
:setTimeout是第一个参数是一个函数,因为是内置函数没有接收值,所以不能传参。之所以上个案例打印undifined,是因为上一个函数没有返回值,返回值的标志就是retrun。 既然这样,那我们把这个函数拿出来,而且retrun出去,哎嘿,巧了不是,意外写了个闭包,呐呐,这就是闭包使用场景二:给不能传参
的 内置函数
的回调参数
传参 ✌ ,这一刻你有没有爱上闭包??
五、闭包的原理
大篇说了很多闭包,我想好兄弟应该对闭包有很深的印象了吧?如果 `没有`, 那好的,是你出去还是我出去?👉,开个玩笑啦,让好兄弟没能理解是我的 “锅” ,烦请好兄弟评论留言,小妹我加以改正,一起进步,好兄弟如果有收获,还请多多点赞关注,你的大拇指就是小妹更新的动力,爱你 biu biu biu ~ 💕 🥰,言归正传,我们来看看闭包的原理
- 从计算机的角度分析:利用了计算机的
垃圾回收机制
。比如我们电脑桌上的回收站,他是先保存到一个临时空间,如果再使用就可以重复使用,确定不用最终删除 - js特性分析:利用了
函数的定义作用域
,和函数的执行上下文 (作用域链)
,不管函数在哪里执行,函数都可以操作自身定义域中存在的变量
六、思考
我们在使用闭包时,可以在作用外部操作作用域内部的数据,有些我们也不知道什么时候调用了影响了结果,使用闭包可以不用定义很多全局变量,但是根据计算机的垃圾回收机制,有些不确定删除的变量会占用内存,所以也比较消耗内存,对于低版本的ie浏览器(ie8),可能会造成内存泄漏就会产生蓝屏卡死等啥的,好在现在浏览器会有内存保护机制,如果满了就会报错,我们修改后刷新下即可
总结
通篇说了很多次闭包的概念,这里我还是想再强调一下,闭包不是一种固定的写法,它是一种环境,利用`作用域的嵌套`, 得到一种效果:原本的`局部变量`进化成`私有变量`,重复使用, 闭包能少用就少用,像递归,能不用就不用
你学废了吗?
😂 谢谢阅读,谢谢点赞并关注(没错我就要道德绑架你!)
一、 更深层次了解闭包,分析以下代码执行过程
1. function foo(){
2. function bar(){
3. console.log('吃饭饭')
4. }
5. return bar
6. }
7. var fn=foo()
8. fn()
原理:
- 调用栈是代码运行环境,在编译前会有个GO对象存放当前环境内置方法类等
- 在编译时,会生成一个全局执行上下文 GEC,会被关联到一个变量环境VO,全局执行上下文的VO 对应的是全局变量GO,在编译时,代码中的变量和函数都会被作为属性添加到VO中
- 不同的是,编译时变量为undi ,函数对象对应一个地址,同时也会为函数对象生成一个内存空间
- 当代码运行时,再执行函数对象时,会创建对应的函数执行上下文 FEC,同样也会被关联到一个变量环境VO,函数执行上下文的VO对应的是AO,当函数执行完毕,这个函数执行上下文也会弹出调用栈
代码:
- 开始分析执行过程,代码至上而下执行,先编译后执行
- 第一行 发现是个函数,GO 存放一个
foo:地址
;第七行,发现是个变量, GO 存放一个fn:undi
- 编译完成,开始执行代码
- 代码应该是从第7行开始执行的。 foo() ,发现foo对应的是个内存地址,找到内存地址,并创建一个函数执行上下文,因为foo里面包括了定义了一个函数bar,这是这个AO会定义一个bar:内存地址,且开辟一个bar的内存空间,。
- 并把函数执行上下文放到调用栈,将bar的内存地址返回给fn
- 这是fn 赋值为一个函数,当fn执行时,同样的道理,找到对应bar 的内存地址,并创建 bar 这个函数的函数执行上下文,因为函数里只有console操作,所以对应的AO 为空
- 当函数执行完毕,对应的函数执行上下文弹出调用框,
二、闭包
1. function foo(){
2. var name='haha'
3 function bar(){
4 console.log('吃饭饭' , name)
5 }
6 return bar
7 }
8 var fn=foo()
9 fn()
- 依旧开始分析执行过程,代码至上而下执行
- 第一行,发现是个函数,好的,GO添加一个属性 foo:内存地址
- 1-7 函数体就会跳过
- 第八行,发现声明了一个fn ,好的,GO添加一个属性 fn:unde
- 编译完成,开始执行代码
- 他会直接从第八行开始解析,foo()执行
- 在GO里找到foo,发现是个地址,找到对应的内存地址,foo() 所以创建了一个函数执行上下文,在foo里发现定义了一个name,这是这个函数执行上下文对应的AO里就会创建一个name:unde,又发现创建了一个bar,是个函数,所以对应创建了内存空间和函数执行上下文
- 编译好了以后开始执行,首先name赋值为haha,这时foo对应AO里的namew为haha ,并且把bar retrun出去
- 这时 GO里的fn 赋值为一个内存地址
- foo执行完毕,被销毁,弹出调用栈。
- fn执行,同样的道理,先找到对应的内存地址,创建函数执行上下文,编译bar,发现没有什么变量定义
- console.log(name),在bar这个AO里是没有任何值的,因为每个函数内存地址里存放当前作用域和父级作用域,所以根据父级作用域进行查找,此时的name值为Foo里的name
- 哎?? foo不是被销毁了吗??
- 这就是闭包 闭包由两部分组成 :函数+可以访问的自由变量
三、闭包定义
- 闭包由两部分组成 :函数+可以访问的自由变量
一个普通的函数function,如果它可以访问外层作用于的自由变量,那么这个函数就是一个闭包
- 从广义的角度来说 : JavaScript中的函数都是闭包
- 从狭义的角度来说 : JavaScript中一个函数,如果访问了外层作用于的变量,那么它是一个闭包
- 闭包在实现上是一个结构体(对象),它存储了一个函数和一个关联的环境 ( 相当于一个符号查找表 );
- 闭包跟函数最大的区别在于,当捕捉闭包的时候,它的
自由变量
会在捕捉时被确定,这样即使脱离了捕捉时的上下文,它也能照常运行
var name = '夏夏'
function my(){
console.log(name)
}
根据1234定义来说,当一个函数内部访问到外部的变量就成为闭包,其实上面例子就是一个闭包,我们无意间写了很多闭包,哈哈哈哈
四、闭包导致的内存泄漏
我们经常会说闭包是造成内存泄露的,为什么呢?
- 比如在上面的案例中,如果后续我们不再使用 foo 函数了,那么该函数对象应该要被销毁掉,并且其引用着的父
作用域AO也应该被销毁掉;但是目前因为在全局作用域下 fn 变量对 0xb00 的函数对象有引用,而 0xb00 的作用域中 AO 有用,所以最终会造成这些内存都是无法被释放的; - 所以我们经常说的闭包会造成内存泄露,其实就是刚才的引用链中的所有对象都是无法释放,那么,怎么解决这个问题呢?
- 当将 foo 设置为 null 时,就不再对函数对象 0xb00 有引用,那么对应的 AO 对象也就不被销毁了,在 GO 的下一次检测中,它们就会被销毁掉;直接把家连根拔起
foo = null
面试题
为什么会有内存泄漏