闭包
前言
闭包对每个前端来说都是一个绕不开的话题。学习之初也因为搞清闭包的概念耗费了不少精力,今天写一篇博客来记录本人对闭包的理解,笔者水平有限,若有疏漏及错误,愿不吝赐教。
什么是闭包?
你可以在一个函数里面嵌套另外一个函数。嵌套(内部)函数对其容器(外部)函数是私有的。它自身也形成了一个闭包。一个闭包是一个可以自己拥有独立的环境与变量的的表达式(通常是函数,因为ES6有了块级作用域的概念)。
可以先点击上面链接查看MDN对闭包的讲述。
闭包在JavaScript高级程序设计(第3版)中是这样描述:闭包是指有权访问另一个函数作用域中的变量的函数。
这样闭包的作用就很明显了
- 可以在函数的外部访问到函数内部的局部变量。
- 让这些变量始终保存在内存中,不会随着函数的结束而自动销毁。
我们来写一个简单的例子:
console.log('closure');functionadd(){var count =0;returnfunction(){
count +=1;return count;};}var a =add();functionfun(){alert(a());}fun();fun();
我们来解释一下这段代码:
定义了add() 函数,函数内定义了count,返回值也是一个函数。子匿名函数引用外部变量count,因此形成了闭包。
当下面我们定义并赋值
a = add()
时,
add()
函数执行且仅执行一次。执行后a被赋值(匿名函数)了
上面代码中
function() { count += 1; return count; };
部分就形成了一个闭包。引用了函数外部变量
count
。因此count在函数执行结束也不会销毁,一直存在于内存中。然后每次调用a()函数时,返回的都是内层匿名函数(闭包)的执行结果,因为内层匿名函数在外层函数的内部,所以即使外层函数执行完了,第二个函数依然能访问count(JS设计的作用域链,当前作用域能访问上级的作用域)。
闭包经典案例
该案例是牛客网题库的一道题:
<ul><li>i</li><li>i</li><li>i</li><li>i</li></ul>
js实现:要求点击每个li标签,弹出对应的索引。(0,1,2,3)
实现1:
var elements = document.getElementsByTagName('li');for(var i =0; i < elements.length; i++){
elements[i].onclick=function(){
console.log(i);alert(i);};}
获取dom元素,进行遍历不就可以了吗?但是当我们自信的点击标签时候发现弹出都是4……。整个人都觉得不好了!
ps:有人疑惑不应该都是3吗?为什么是4? 这是因为for循环的执行顺序都是以i++结束的,最后一次不进入循环。但是var i 是在全局中的,因此i会被赋值为4.具体for循环知识可以自行搜索这里不做赘述。
下面是这个题目的解答:
每个li标签的onclick事件执行时,本身onclick绑定的function的作用域中没有变量i,i为undefined,则解析引擎会寻找父级作用域,发现父级作用域中有i,且for循环绑定事件结束后,i已经赋值为4,所以每个li标签的onclick事件执行时,alert的都是父作用域中的i,也就是4。这是作用域的问题。这里onclick事件绑定的函数形成闭包,但引用的变量i是全局的。
闭包真正的含义是,如果一个函数访问了此函数的父级及父级以上的作用域变量,就可以称这个函数是一个闭包。
这里其实是一个作用域问题,闭包中引用的变量不会消失,所以我们把每次的i都保存到变量中,就可以实现想要的结果。
for(let i =0; i < elements.length; i++){
elements[i].onclick=function(){return(function(n){
console.log(n);alert(n);})(i);};}
这样我们就使用闭包将i作为参数传递到匿名函数(闭包)中,每个点击事件都有了一个局部的num变量,保存的即相应的i值。
拓展:
我们可以直接将for循环的var改为let就可以实现想要的效果。let的到来,让在循环体内创建闭包具有了更好的可读性。可搜索学习let和const的相关知识。或亲自动手,将let通过babel编译查看相关结果进行研读。
有关let的知识点会在以后的博文中进行整理。
闭包造成的内存泄漏问题
闭包怎么引起内存泄漏?谁会存在内存泄漏?
我们借助垃圾回收机制的标记清除法来看一下:
示例:
functionfn(){let count =1functionfun(){
count ++
console.log(`函数内部调用${count}次`)}return fun
}const result =fn();result()//2result()//3
我们来分析一下:
result
是一个全局变量,代码执行完毕不会立即销毁result
可以找到fn()
函数,fn()
函数return fun()
,因此 fun()也不会销毁。fun()
引用到了父级作用域的count
,因此,在引用的count
便不会被垃圾回收机制回收。此时,闭包就引起了内存泄漏问题 正常情况下,一般的局部变量函数执行完就会销毁,但是我们这里的result
没有销毁,所以它引用到的count
也会留存在内存当中。
闭包的应用
闭包的实际应用主要是函数工厂,以及模拟私有方法。
闭包实现函数工厂
举例:
functionmakeAdder(x){returnfunction(y){return x + y;};}var add5 =makeAdder(5);var add10 =makeAdder(10);
console.log(add5(2));// 7
console.log(add10(2));// 12
这是MDN官网的一个
makeAdder(x)
函数,接受一个参数
x
,并返回新的函数,返回的函数接受一个参数
y
,并返回
x+y
的值。
从本质上讲,
makeAdder
是一个函数工厂 — 他创建了将指定的值和它的参数相加求和的函数。在上面的示例中,我们使用函数工厂创建了两个新函数 — 一个将其参数和 5 求和,另一个和 10 求和。
add5 和 add10 都是闭包。它们共享相同的函数定义,但是保存了不同的词法环境。在 add5 的环境中,x 为 5。而在 add10 中,x 则为 10。
应用案例:假如我们想在页面添加几个按钮用来调整页面字号。我可以给body元素设置font-size,然后通过em单位设置页面中其他元素的字号:
body{font-family: Helvetica, Arial, sans-serif;font-size: 12px;}h1{font-size: 1.5em;}h2{font-size: 1.2em;}
然后可以对body的font-size进行调整:
functionmakeSizer(size){returnfunction(){
document.body.style.fontSize = size +'px';};}var size12 =makeSizer(12);var size14 =makeSizer(14);var size16 =makeSizer(16);
这样,size12,size14,size16就可以分别将body字体调整为12,14,16px,再将它们添加到点击事件上。
document.getElementById('size-12').onclick = size12;
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;
这样就实现了想要的效果。这就是一个简单的闭包实现函数工厂的案例。
闭包模拟私有方法
var Counter =(function(){var privteCounter =0;functionchangeBy(val){
privteCounter += val;}return{increment:function(){changeBy(1)},decrement:function(){changeBy(-1)},value:function(){return privteCounter
}}})()
console.log(Counter.value());/* logs 0 */
Counter.increment();
Counter.increment();
console.log(Counter.value());/* logs 2 */
Counter.decrement();
console.log(Counter.value());/* logs 1 */
在之前的示例中,每个闭包都有它自己的词法环境;而这次我们只创建了一个词法环境,为三个函数所共享:
Counter.increment
,
Counter.decrement
和
Counter.value
。
该共享环境创建于一个立即执行的匿名函数体内。这个环境中包含两个私有项:名为
privateCounter
的变量和名为 changeBy 的函数。这两项都无法在这个匿名函数外部直接访问。必须通过匿名函数返回的三个公共函数访问。
这三个公共函数是共享同一个环境的闭包。多亏 JavaScript 的词法作用域,它们都可以访问
privateCounter
变量和
changeBy
函数。
备注: 你应该注意到我们定义了一个匿名函数,用于创建一个计数器。我们立即执行了这个匿名函数,并将他的值赋给了变量Counter。我们可以把这个函数储存在另外一个变量makeCounter中,并用他来创建多个计数器。
varmakeCounter=function(){var privteCounter =0;functionchangeBy(val){
privteCounter += val;}return{increment:function(){changeBy(1);},decrement:function(){changeBy(-1);},value:function(){return privteCounter;},};};var Counter1 =makeCounter();var Counter2 =makeCounter();
console.log(Counter1.value());/* logs 0 */
Counter1.increment();
Counter1.increment();
console.log(Counter1.value());/* logs 2 */
Counter1.decrement();
console.log(Counter1.value());/* logs 1 */
console.log(Counter2.value());/* logs 0 */
请注意两个计数器 Counter1 和 Counter2 是如何维护它们各自的独立性的。每个闭包都是引用自己词法作用域内的变量 privateCounter 。
每次调用其中一个计数器时,通过改变这个变量的值,会改变这个闭包的词法环境。然而在一个闭包内对变量的修改,不会影响到另外一个闭包中的变量。
备注: 以这种方式使用闭包,提供了许多与面向对象编程相关的好处 —— 特别是数据隐藏和封装。
闭包应用部分是引用MDN的案例,大家可以移步阅读(
一定要读
)。
今天的总结分享就到这里,继续努力搬砖!只作个人总结所用,有不足与疏漏请评论区补充。
版权归原作者 寒烟说 所有, 如有侵权,请联系我们删除。