谈谈js的闭包
更新日期:
上一篇我们讲js的this,里面提到了执行环境,今天我们就结合执行环境一起讲一下闭包吧。
要理解闭包,这里我们补充一下js的作用域和作用域链吧,上一篇也只是稍微略过。
变量的作用域
全局作用域和局部作用域
在JS当中一个变量的作用域(scope)是程序中定义这个变量的区域。变量分为两类:全局(global)的和局部的。
- 全局变量的作用域是全局性的,即在JavaScript代码中,它处处都有定义
- 在函数之内声明的变量,就只在函数体内部有定义。它们是局部变量,作用域是局部性的。函数的参数也是局部变量,它们只在函数体内部有定义
1 | var a; //全局变量,作用域全局性 |
变量没有在函数内声明或者声明的时候没有带var就是全局变量,拥有全局作用域,window对象的所有属性拥有全局作用域,在代码任何地方都可以访问。函数内部声明并且以var修饰的变量就是局部变量,只能在函数体内使用。
作用域链
- 作用域链是什么
每个JavaScript执行环境都有一个和它关联在一起的作用域链。这个作用域链是一个对象列表或对象链。 - 作用域链的创建
当代码在一个环境中执行时,会创建变量对象的一个作用域链,来保证对执行环境有权访问的变量和函数的有序访问。作用域第一个对象始终是当前执行代码所在环境的变量对象。 - 搜索标识符
在函数运行过程中标识符的解析是沿着作用域链一级一级搜索的过程,从第一个对象开始,逐级向后回溯,直到找到同名标识符为止,找到后不再继续遍历,找不到就报错。1
2
3
4
5
6
7
8
9
10
11
12
13
14//作用域链[全局]
var a = 1;
function fun1(){
//作用域链[fun1,全局]
var b = 2;
function fun2(){
//作用域链[fun2, fun1,全局]
var c = 3;
alert(c); //查找fun2,有结果,返回3
alert(b); //查找fun2,无结果,查找fun1,有结果,返回2
alert(a); //查找fun2,无结果,查找fun1,无结果,查找全局,有结果,返回1
alert(abc); //查找fun2,无结果,查找fun1,无结果,查找全局,无结果,返回undefined,若此处为函数调用,则报错
}
}
with语句
with语句用于设置代码在特定对象中的作用域,主要用来临时扩展作用域链,将语句中的对象添加到作用域的头部。
下面引用w3c上的例子进行说明:1
2
3
4var sMessage = "hello";
with(sMessage) {
alert(toUpperCase()); //输出 "HELLO"
}
js闭包
铺垫了这么多,下面我们开始讲闭包。
官方的解释是:闭包是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。
这句话很难懂,我们还是从闭包的出现开始讲起吧。
闭包的出现
在Javascript语言中,只有函数内部的子函数才能读取局部变量。我们看下面的例子:1
2
3
4
5function B(){
var b = 2;
}
B();
alert(b); //undefined
在全局环境下无法访问函数B内的变量,这可以用前面的作用域和作用域链来解释,即全局函数的作用域链里不含有函数B内的作用域。现在如果我们想要访问内部函数的变量,可以这样做:1
2
3
4
5
6
7
8
9function B(){
var b = 2;
function C(){
alert(b); //2
}
return C;
}
var A = B();
A(); //2
也就是说,我们在函数B内定义了另外一个函数C,用于输出函数B内变量b的值。而当在函数B内把函数C返回,且被外部引用了的时候,我们就可以在函数B外面获得函数B里面的变量了,此时也创建了一个闭包。
在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。
再理解闭包
我们看下面代码:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17function B(){
var b = (b === undefined) ? 1 : b;
function C(){
b++;
alert(b);
};
return C;
}
var A = B();
A(); //2
A(); //3
var D = B();
D(); //2
D(); //3
D(); //4
A(); //4
A(); //5
当我们使用闭包把函数B里面的函数C返回并赋值给函数外的变量A,在每次调用A时函数B里面的变量b都会增加1,即该变量b并没有被销毁,此时我们可以继续使用或改变该变量b。
并且,当我们再次使用闭包把函数B里面的函数C返回并赋值给函数外的变量D时,里面的变量b会重新被赋值。觉得别扭吗?
其实想要更好地理解我们需要配合上节说过的执行环境来讲,这里我再贴一下执行环境有关的说明:
- 定义期
- 全局函数A创建了一个A的[[scope]]属性,包含全局[[scope]]
- 函数A里定义函数B,则B的[[scope]]包含全局[[scope]]和A的[[scope]]
- 执行期
当函数被执行的时候,就是进入这个函数的执行环境,首先会创一个它自己的活动对象,包含- this
- 参数(arguments,全局对象没有arguments)
- 局部变量(包括命名的参数)
- 一个变量对象的作用域链[[scope chain]]
我们把上面的代码执行过程再重新理一下:
1.变量声明和函数声明(声明的提升),此时进行函数B的定义期
2.变量A赋值第一步,此时进入函数B的执行期,进入B函数的执行环境,创建相关的this对象(window对象)、局部变量、作用域链等,我们称该变量b为b1,b1被赋值1
3.变量A赋值第二步,此时函数B执行完毕,返回函数B内的函数C,并引用变量b1,故变量b1并没有被回收
4.变量A调用,此时引用了变量b1并增加1,因为b1仍然在内存中,故可以实现b1的自增
5.进行变量D赋值,步骤和2一致,重新进入函数B的执行期,再次创建了B函数执行环境相关的this对象(window对象)、局部变量、作用域链等,这里我们可以看到,这个变量b是重新创建的,我们称之为b2,b2被赋值1
6.变量D调用,此时引用了变量b2并增加1,因为b2仍然在内存中,故可以实现b2的自增
闭包的用途
闭包可以用在许多地方,它的最大用处有两个:
- 用于读取其他函数内部变量的函数
- 让这些变量的值始终保持在内存中
接下来我们结合js的垃圾回收机制谈谈为什么闭包使变量保存在内存中吧。
js垃圾回收机制
js垃圾回收机制原理就是找出那些不在被使用的变量,然后释放其所占有的内存。回收器一般是按照固定的时间间隔或者预设的时间进行处理的。
对于其他语言来说,需要开发者手动的来跟踪内存,而JS的垃圾回收机制使得JS开发人员无需再关系内存的情况,所有的内存分配以及回收都会由垃圾回收器自动完成,执行环境会对执行过程中占有的内存负责。
- 垃圾回收机制的种类
- 标记清除
- 在和执行上下文类似的的环境中当变量名称进入环境的时候,那么变量会被打上YES。一般来说是绝对不会释放被打上YES标签的变量内存的,一旦变量在出了该环境时,变会被打上NO标签(和作用域貌似有点像),JS引擎会在一定时间间隔或者设置的时间来进行扫描,对NO标签的进行剔除以释放其内存。
- 引用计数
- 一般来说,引用计数的含义是跟踪记录每个值被引用的次数。当声明一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数便是1,如果同一个值又被赋给另一个变量,则该值的引用次数加1,相反,如果包含对这个值引用的变量又取得了另一个值,则这个值的引用次数减1。当这个值的引用次数为0时,说明没有办法访问到它了,因而可以将其占用的内存空间回收。
除了一些极老版本的IE,目前市面上的JS引擎基本采用标记清除来除了垃圾回收。
可见,当我们在函数外引用了函数里面的变量时,即使该函数已经执行完毕,但由于其内部变量仍被引用,便不会被内存空间回收。若带目的性地使用这个特点,则是闭包的一种用途。但是如果不是针对性地用于保存变量,则可能会导致内存泄露哦。
- 一般来说,引用计数的含义是跟踪记录每个值被引用的次数。当声明一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数便是1,如果同一个值又被赋给另一个变量,则该值的引用次数加1,相反,如果包含对这个值引用的变量又取得了另一个值,则这个值的引用次数减1。当这个值的引用次数为0时,说明没有办法访问到它了,因而可以将其占用的内存空间回收。
- 标记清除
参考
《学习Javascript闭包(Closure)》
《JS 垃圾回收机制简介~ 》
结束语
有人说闭包很方便,这话不假,我们可以利用闭包创建一些服务,用来保存需要的变量。但是闭包会使子函数保持其作用域链的所有变量及函数与内存中,内存消耗很大,所以不能滥用,并且在使用的时候尽量销毁父函数不再使用的变量哦。
码生艰难,写文不易,给我家猪囤点猫粮了喵~
查看Github有更多内容噢:https://github.com/godbasin
更欢迎来被删的前端游乐场边撸猫边学前端噢
如果你想要关注日常生活中的我,欢迎关注“牧羊的猪”公众号噢