目录
什么是闭包?
闭包的产生
闭包中的变量
自动形成的闭包与内存占用
手动形成闭包
闭包的优点/特点
对闭包的小结:
闭包经典面试题一
闭包基础面试题二
闭包经典面试题三
闭包经典面试题四
真题解答
需要先掌握的知识
中的作用域与作用域链
- 作用域就是一个独立的地盘,让变量不会外泄、暴露出去,不同作用域下同名变量不会有冲突。
- 作用域在定义时就确定,并且不会改变。
- 如果在当前作用域中没有查到值,就会向上级作用域去查,直到查到全局作用域,这么一个查找过程形成的链条就叫做作用域链。
中的垃圾回收
- Javascript 执行环境会负责管理代码执行过程中使用的内存,其中就涉及到一个垃圾回收机制
- 垃圾收集器会定期(周期性)找出那些不再继续使用的变量,只要该变量不再使用了,就会被垃圾收集器回收,然后释放其内存。如果该变量还在使用,那么就不会被回收。
什么是闭包?
闭包不是一个具体的技术,而是一种现象,是指在定义函数时,周围环境中的信息可以在函数中使用。换句话说,执行函数时,只要在函数中使用了外部的数据,就创建了闭包。
而作用域链,正是实现闭包的手段。
闭包的产生:执行函数时,使用了函数外部的数据,闭包就产生了。
-
function a(){
-
var i = 10;
-
console.log(i);
-
}
-
a();
'
运行
此时不会产生闭包,因为此时a函数没有使用外部数据,对于a函数来说,i在自己的函数作用域中。
闭包的产生
-
var i = 10;
-
function a(){
-
console.log(i);
-
}
-
a();
'
运行
此时,会产生闭包,因为a函数在自己的作用域中中找不到i 这个变量了,引用了外部数据,就会创建闭包。通过作用域链可知,它会顺着作用域链一层一层的往上找。
i就会被放到闭包中。
闭包中的变量
-
var i = 10;
-
function A(){
-
var j = 20;
-
var k = 30;
-
function b(){
-
var x = 40;
-
var y = 50;
-
function c(){
-
console.log(i, j, k, x);
-
}
-
c();
-
}
-
b();
-
}
-
A();
'
运行
被放入闭包中的变量有:i , j, k,x;y没有被放进去,因为y没有被引用。
这里一共创建了三个闭包:
全局闭包里面存储了i的值,
闭包a中存储了变量j, k,
闭包b中存储了变量x,
通过javascript的垃圾回收可知:只要该变量不再使用了,就会被垃圾收集器回收,然后释放其内存。如果该变量还在使用,那么就不会被回收。
一个数据要不要放入闭包取决于该变量有没有被引用。
自动形成的闭包与内存占用
只要该变量不再使用了,就会被垃圾收集器回收,然后释放其内存。如果该变量还在使用,那么就不会被回收。
那么产生一个新的问题:那么多闭包,那岂不是占用内存空间么?
实际上,如果是自动形成的闭包,是会被销毁掉的。
-
var i = 10;
-
function A(){
-
var j = 20;
-
var k = 30;
-
function b(){
-
var x = 40;
-
var y = 50;
-
function c(){
-
console.log(i, j, k, x);
-
}
-
c();
-
}
-
b();
-
}
-
A();
-
console.log(k);
尝试打印输出变量 k,显然这个时候是会报错的,此时已经没有任何闭包存在,垃圾回收器会自动回收没有引用的变量,不会有任何内存占用的情况。
手动形成闭包
-
function eat(){
-
var food = "鸡翅";
-
console.log(food);
-
}
-
eat(); // 鸡翅
-
console.log(food); // 报错
我们声明了一个eat函数,并对它进行调用。
JavaScript引擎会创建一个eat函数的执行上下文,并且给food进行赋值
当eat方法执行完后,上下文被销毁,food变量也会跟着消失,因为food是eat函数的局部变量,它作用于eat函数,会随着eat的创建而创建,销毁而销毁。所以当我们再次打印 food 变量时,就会报错,告诉我们该变量不存在。
手动形成闭包:
-
function eat(){
-
var food = '鸡翅';
-
return function(){
-
console.log(food);
-
}
-
}
-
var look = eat();
-
look(); // 鸡翅
-
look(); // 鸡翅
'
运行
eat返回一个函数,并在在这个函数内部访问food这个局部变量。
调用eat函数,将其结果赋值给look,look指向eat的内部函数,然后调用它,并最终输出food的值。
这里之所可以访问到food,是因为垃圾回收器只回收没有被引用的变量,但是一旦一个变量被引用着,垃圾回收器就不会回收此变量。
在上面示例中,eat函数执行完,照理说应该被销毁,但是向外部返回了eat内部的匿名函数,而这个匿名函数中又引用food,所以垃圾回收器不会对food变量进行回收
闭包的优点/特点
- 通过闭包可以让外部环境访问到函数内部的局部变量
- 通过闭包可以让全局变量持续保存下来,不随着它的上下文一起销毁
通过此特性,我们可以解决一个全局变量污染的问题, 早期在 JavaScript 还无法进行模块化的时候,在多人协作时,如果定义过多的全局变量 有可能造成全局变量命名冲突,使用闭包来解决功能对变量的调用将变量写到一个独立的空间里面,从而能够一定程度上解决全局变量污染的问题。
-
var name = "GlobalName";
-
// 全局变量
-
var init = (function () {
-
var name = "initName";
-
function callName() {
-
console.log(name);
-
// 打印 name
-
}
-
return function () {
-
callName();
-
// 形成接口
-
}
-
}());
-
init(); // initName
-
var initSuper = (function () {
-
var name = "initSuperName";
-
function callName() {
-
console.log(name);
-
// 打印 name
-
}
-
return function () {
-
callName();
-
// 形成接口
-
}
-
}());
-
initSuper(); // initSuperName
'
运行
对闭包的小结:
- 闭包是一个封闭空间,里面存储了在其他会引用到该作用域的变量,在JavaScript通过作用域链来实现闭包
- 只要在函数中使用了外部数据,就创建了闭包,这种自动形成的闭包编码时,是不需要去关心的
- 我们可以通过手动来创建闭包,从而让外部环境可以使用到函数内部的局部变量,让局部变量持续存储下来,不随着它的上下文的销毁而销毁。
闭包经典面试题一
-
for (var i = 1; i <= 3; i++) {
-
setTimeout(function () {
-
console.log(i);
-
}, 1000);
-
}
-
//请写出打印的结果:三个4
'
运行
setTimeout()
的意思是设置一个定时器,该定时器在定时器到期后执行一个函数或指定的一段代码。
setTimeout
函数会优先执行之前的事件,最后再执行后续的事件。而之前的事件是i
循环,显然得等i
循环执行完毕再执行打印出i
的这个函数,当i
等于3时,因为小于等于3会再执行一次,直到i
等于4,由于闭包的原因,所以它们仍然能访问到变量 i,不过此时 i 变量值已经是 4 了。一共执行了3次,根据前面所诉会打出3个4。
要解决这个问题,我们可以让 setTimeout 中的匿名函数不再访问外部变量,而是访问自己内部的变量,如下:
-
for (var i = 1; i <= 3; i++) {
-
(function (index) {
-
setTimeout(function () {
-
console.log(index);
-
}, 1000);
-
})(i)
-
}
'
运行
当然,解决这个问题还有个更简单的方法,就是使用 ES6 中的 let 关键字。
它声明的变量有块作用域,如果将它放在循环中,那么每次循环都会有一个新的变量 i,这样即使有闭包也没问题,因为每个闭包保存的都是不同的 i 变量,那么刚才的问题也就迎刃而解。
-
for (let i = 1; i <= 3; i++) {
-
setTimeout(function () {
-
console.log(i);
-
}, 1000);
-
}
'
运行
闭包基础面试题二
-
function fun(){
-
var count = 1;
-
return function (){
-
console.log("jsfhjd")
-
console.log(count);
-
console.log(count++);
-
console.log(count);
-
}
-
}
-
fun();
-
var fun2 = fun();
-
fun2();
-
console.log("sjfhe");
-
fun2();
-
fun2();
'
运行
fun();这里什么都不会打印
var fun2 = fun(); fun2指向fun内部的匿名函数
执行fun2函数时,就会调用这个匿名函数,就会执行,会打印出jsfhjd,1,1,2
("sjfhe");打印出sjfhe
fun2();jsfhjd,2,2,3
fun2();jsfhjd,3,3,4
第二次执行闭包起作用,第一次为被销毁
我们可以看出只要count这个变量一直被使用(或者说后续会被使用到),所以不会被回收,我理解的是:这个变量不再使用了,就会被垃圾回收器回收,释放内存。
闭包经典面试题三
-
<body>
-
<ul id="list">
-
<li>公司简介</li>
-
<li>联系我们</li>
-
<li>营销网络</li>
-
</ul>
-
<script src="./"></script>
-
<script>
-
var list = document.getElementById("list");
-
var li = list.children;
-
for(var i = 0 ;i<li.length;i++){
-
console.log("kzh");
-
li[i].onclick=function(){
-
console.log(i)
-
console.log("jhg")
-
}
-
console.log(i)
-
}
-
-
</script>
-
</body>
打印的结果为:kzh, 0,kzh,1,kzh,2——>点击之后打印3,jhg,
因为i是贯穿整个作用域的,而不是给每一个li分配一个i,点击事件使异步,用户一定是在for运行完了以后,才点击,此时i已经变成3了。
那么怎么解决这个问题呢,可以用立即执行函数,给每个li创建一个独立的作用域,在立即执行函数执行的时候,i的值从0到2,对应三个立即执行函数,这3个立即执行函数里边的j分别是0,1,2所以就能正常输出了,看下边例子:
-
<body>
-
<ul id="list">
-
<li>公司简介</li>
-
<li>联系我们</li>
-
<li>营销网络</li>
-
</ul>
-
<script src="./"></script>
-
<script>
-
var list = document.getElementById("list");
-
var li = list.children;
-
for(var i = 0 ;i<li.length;i++){
-
console.log("kzh");
-
// 立即执行函数
-
(function(j){
-
console.log("jh");
-
li[j].onclick = function(){
-
console.log(j);
-
}
-
})(i)//把实参i赋值给形参j
-
console.log(i)
-
}
-
-
</script>
-
</body>
打印结果:kzh,jh,0,kzh,jh,1,kzh,jh,2——>点击事件:0,1,2
闭包经典面试题四
-
function fun(n,o){
-
console.log(o);
-
return{
-
fun:function(m){
-
return fun(m,n)
-
}
-
}
-
}
-
var a = fun(0)
-
a.fun(1)
-
a.fun(2)
-
a.fun(3)
-
var b = fun(0).fun(1).fun(2).fun(3)
-
var c = fun(0).fun(1)
-
c.fun(2)
-
c.fun(3)
'
运行
输出结果为:
undefined,0,0,0
undefined,0,1,2
undefined,0,1,1
解析:函数内部有函数就形成了闭包
fun(0);输出o为undefined:fun(0)只传一个参数赋值给n,o输出为undefined {n = 0}
a部分:
(1):代表内部函数的fun,执行内部函数,m = 1=>n,n=>o从内部向外部执行,n获取上次闭包值为0对应o,输出 o = 0 {m = 1}
a是返回对象 fun:function(m){ return fun(m,n) },闭包保存在a 中,闭包又是用来存储介质对的,认为key是n,存储的值是0,也就是说a 里面有一个闭包n值为0,所以每次传值不论传什么值进去,都是复制给m了,但通过闭包n值始终不变,n=0恒成立,所以输出为:undefined,0,0,0
b部分:
由上述推导知:fun(0) = undefined
fun(0).fun(1)相当与(1),结果为0,返回值是个对象,n = 0
fun(0).fun(1).fun(2),返回值是个新的对象,内部存储的闭包也是新的,和上面不同,n = 1
fun(0).fun(1).fun(2).fun(3),n = 2
总结:
当前传入的参数为多少并不重要,重要的是上一步给闭包中传入的key为多少
c部分:
输出依次:undefined/0/1/1
真题解答
- 闭包是什么?闭包的应用场景有哪些?怎么销毁闭包?
闭包是一个封闭的空间,里面存储了在其他地方会引用到的该作用域的值,在 JavaScript 中是通过作用域链来实现的闭包。
只要在函数中使用了外部的数据,就创建了闭包,这种情况下所创建的闭包,我们在编码时是不需要去关心的。
我们还可以通过一些手段手动创建闭包,从而让外部环境访问到函数内部的局部变量,让局部变量持续保存下来,不随着它的上下文环境一起销毁。
使用闭包可以解决一个全局变量污染的问题。
如果是自动产生的闭包,我们无需操心闭包的销毁,而如果是手动创建的闭包,可以把被引用的变量设置为 null,即手动清除变量,这样下次 JavaScript 垃圾回收器在进行垃圾回收时,发现此变量已经没有任何引用了,就会把设为 null 的量给回收了。