面试题-闭包

时间:2024-10-07 10:29:54

 

目录

什么是闭包?

 闭包的产生

 闭包中的变量

 自动形成的闭包与内存占用

手动形成闭包

 闭包的优点/特点

 对闭包的小结:

 闭包经典面试题一

闭包基础面试题二 

 闭包经典面试题三

闭包经典面试题四

真题解答


需要先掌握的知识

中的作用域与作用域链

  • 作用域就是一个独立的地盘,让变量不会外泄、暴露出去,不同作用域下同名变量不会有冲突
  • 作用域在定义时就确定,并且不会改变。
  • 如果在当前作用域中没有查到值,就会向上级作用域去查,直到查到全局作用域,这么一个查找过程形成的链条就叫做作用域链。

中的垃圾回收

  • Javascript 执行环境会负责管理代码执行过程中使用的内存,其中就涉及到一个垃圾回收机制
  • 垃圾收集器会定期(周期性)找出那些不再继续使用的变量,只要该变量不再使用了,就会被垃圾收集器回收,然后释放其内存。如果该变量还在使用,那么就不会被回收

什么是闭包?

闭包不是一个具体的技术,而是一种现象,是指在定义函数时,周围环境中的信息可以在函数中使用。换句话说,执行函数时,只要在函数中使用了外部的数据,就创建了闭包

而作用域链,正是实现闭包的手段。

 闭包的产生:执行函数时,使用了函数外部的数据,闭包就产生了。


  1. function a(){
  2. var i = 10;
  3. console.log(i);
  4. }
  5. a();
'
运行

 此时不会产生闭包,因为此时a函数没有使用外部数据,对于a函数来说,i在自己的函数作用域中。


 闭包的产生

  1. var i = 10;
  2. function a(){
  3. console.log(i);
  4. }
  5. a();
'
运行

此时,会产生闭包,因为a函数在自己的作用域中中找不到i 这个变量了,引用了外部数据,就会创建闭包。通过作用域链可知,它会顺着作用域链一层一层的往上找。

i就会被放到闭包中。


 闭包中的变量

  1. var i = 10;
  2. function A(){
  3. var j = 20;
  4. var k = 30;
  5. function b(){
  6. var x = 40;
  7. var y = 50;
  8. function c(){
  9. console.log(i, j, k, x);
  10. }
  11. c();
  12. }
  13. b();
  14. }
  15. A();
'
运行

被放入闭包中的变量有:i , j, k,x;y没有被放进去,因为y没有被引用。

这里一共创建了三个闭包:

全局闭包里面存储了i的值,

闭包a中存储了变量j, k,

闭包b中存储了变量x,

通过javascript的垃圾回收可知:只要该变量不再使用了,就会被垃圾收集器回收,然后释放其内存。如果该变量还在使用,那么就不会被回收

一个数据要不要放入闭包取决于该变量有没有被引用。


 自动形成的闭包与内存占用

只要该变量不再使用了,就会被垃圾收集器回收,然后释放其内存。如果该变量还在使用,那么就不会被回收

那么产生一个新的问题:那么多闭包,那岂不是占用内存空间么?

实际上,如果是自动形成的闭包,是会被销毁掉的。

  1. var i = 10;
  2. function A(){
  3. var j = 20;
  4. var k = 30;
  5. function b(){
  6. var x = 40;
  7. var y = 50;
  8. function c(){
  9. console.log(i, j, k, x);
  10. }
  11. c();
  12. }
  13. b();
  14. }
  15. A();
  16. console.log(k);

 尝试打印输出变量 k,显然这个时候是会报错的,此时已经没有任何闭包存在,垃圾回收器会自动回收没有引用的变量,不会有任何内存占用的情况。


手动形成闭包

  1. function eat(){
  2. var food = "鸡翅";
  3. console.log(food);
  4. }
  5. eat(); // 鸡翅
  6. console.log(food); // 报错

我们声明了一个eat函数,并对它进行调用。

JavaScript引擎会创建一个eat函数的执行上下文,并且给food进行赋值

当eat方法执行完后,上下文被销毁,food变量也会跟着消失,因为food是eat函数的局部变量,它作用于eat函数,会随着eat的创建而创建,销毁而销毁。所以当我们再次打印 food 变量时,就会报错,告诉我们该变量不存在。

手动形成闭包:

  1. function eat(){
  2. var food = '鸡翅';
  3. return function(){
  4. console.log(food);
  5. }
  6. }
  7. var look = eat();
  8. look(); // 鸡翅
  9. look(); // 鸡翅
'
运行

 eat返回一个函数,并在在这个函数内部访问food这个局部变量。

调用eat函数,将其结果赋值给look,look指向eat的内部函数,然后调用它,并最终输出food的值。

这里之所可以访问到food,是因为垃圾回收器只回收没有被引用的变量,但是一旦一个变量被引用着,垃圾回收器就不会回收此变量。

在上面示例中,eat函数执行完,照理说应该被销毁,但是向外部返回了eat内部的匿名函数,而这个匿名函数中又引用food,所以垃圾回收器不会对food变量进行回收


 闭包的优点/特点

  • 通过闭包可以让外部环境访问到函数内部的局部变量
  • 通过闭包可以让全局变量持续保存下来,不随着它的上下文一起销毁

通过此特性,我们可以解决一个全局变量污染的问题, 早期在 JavaScript 还无法进行模块化的时候,在多人协作时,如果定义过多的全局变量 有可能造成全局变量命名冲突,使用闭包来解决功能对变量的调用将变量写到一个独立的空间里面,从而能够一定程度上解决全局变量污染的问题

  1. var name = "GlobalName";
  2. // 全局变量
  3. var init = (function () {
  4. var name = "initName";
  5. function callName() {
  6. console.log(name);
  7. // 打印 name
  8. }
  9. return function () {
  10. callName();
  11. // 形成接口
  12. }
  13. }());
  14. init(); // initName
  15. var initSuper = (function () {
  16. var name = "initSuperName";
  17. function callName() {
  18. console.log(name);
  19. // 打印 name
  20. }
  21. return function () {
  22. callName();
  23. // 形成接口
  24. }
  25. }());
  26. initSuper(); // initSuperName
'
运行

 对闭包的小结:

  • 闭包是一个封闭空间,里面存储了在其他会引用到该作用域的变量,在JavaScript通过作用域链来实现闭包
  • 只要在函数中使用了外部数据,就创建了闭包,这种自动形成的闭包编码时,是不需要去关心的
  • 我们可以通过手动来创建闭包,从而让外部环境可以使用到函数内部的局部变量,让局部变量持续存储下来,不随着它的上下文的销毁而销毁。

 闭包经典面试题一

  1. for (var i = 1; i <= 3; i++) {
  2. setTimeout(function () {
  3. console.log(i);
  4. }, 1000);
  5. }
  6. //请写出打印的结果:三个4
'
运行

setTimeout()的意思是设置一个定时器,该定时器在定时器到期后执行一个函数或指定的一段代码。

setTimeout函数会优先执行之前的事件,最后再执行后续的事件。而之前的事件是i循环,显然得等i循环执行完毕再执行打印出i的这个函数,当i等于3时,因为小于等于3会再执行一次,直到i等于4,由于闭包的原因,所以它们仍然能访问到变量 i,不过此时 i 变量值已经是 4 了。一共执行了3次,根据前面所诉会打出3个4。

要解决这个问题,我们可以让 setTimeout 中的匿名函数不再访问外部变量,而是访问自己内部的变量,如下:

  1. for (var i = 1; i <= 3; i++) {
  2. (function (index) {
  3. setTimeout(function () {
  4. console.log(index);
  5. }, 1000);
  6. })(i)
  7. }
'
运行

 

当然,解决这个问题还有个更简单的方法,就是使用 ES6 中的 let 关键字。

它声明的变量有块作用域,如果将它放在循环中,那么每次循环都会有一个新的变量 i,这样即使有闭包也没问题,因为每个闭包保存的都是不同的 i 变量,那么刚才的问题也就迎刃而解。

  1. for (let i = 1; i <= 3; i++) {
  2. setTimeout(function () {
  3. console.log(i);
  4. }, 1000);
  5. }
'
运行

闭包基础面试题二 

  1. function fun(){
  2. var count = 1;
  3. return function (){
  4. console.log("jsfhjd")
  5. console.log(count);
  6. console.log(count++);
  7. console.log(count);
  8. }
  9. }
  10. fun();
  11. var fun2 = fun();
  12. fun2();
  13. console.log("sjfhe");
  14. fun2();
  15. 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这个变量一直被使用(或者说后续会被使用到),所以不会被回收,我理解的是:这个变量不再使用了,就会被垃圾回收器回收,释放内存。


 闭包经典面试题三

  1. <body>
  2. <ul id="list">
  3. <li>公司简介</li>
  4. <li>联系我们</li>
  5. <li>营销网络</li>
  6. </ul>
  7. <script src="./"></script>
  8. <script>
  9. var list = document.getElementById("list");
  10. var li = list.children;
  11. for(var i = 0 ;i<li.length;i++){
  12. console.log("kzh");
  13. li[i].onclick=function(){
  14. console.log(i)
  15. console.log("jhg")
  16. }
  17. console.log(i)
  18. }
  19. </script>
  20. </body>

打印的结果为:kzh, 0,kzh,1,kzh,2——>点击之后打印3,jhg,

因为i是贯穿整个作用域的,而不是给每一个li分配一个i,点击事件使异步,用户一定是在for运行完了以后,才点击,此时i已经变成3了。

那么怎么解决这个问题呢,可以用立即执行函数,给每个li创建一个独立的作用域,在立即执行函数执行的时候,i的值从0到2,对应三个立即执行函数,这3个立即执行函数里边的j分别是0,1,2所以就能正常输出了,看下边例子:

 

  1. <body>
  2. <ul id="list">
  3. <li>公司简介</li>
  4. <li>联系我们</li>
  5. <li>营销网络</li>
  6. </ul>
  7. <script src="./"></script>
  8. <script>
  9. var list = document.getElementById("list");
  10. var li = list.children;
  11. for(var i = 0 ;i<li.length;i++){
  12. console.log("kzh");
  13. // 立即执行函数
  14. (function(j){
  15. console.log("jh");
  16. li[j].onclick = function(){
  17. console.log(j);
  18. }
  19. })(i)//把实参i赋值给形参j
  20. console.log(i)
  21. }
  22. </script>
  23. </body>

打印结果:kzh,jh,0,kzh,jh,1,kzh,jh,2——>点击事件:0,1,2


闭包经典面试题四

  1. function fun(n,o){
  2. console.log(o);
  3. return{
  4. fun:function(m){
  5. return fun(m,n)
  6. }
  7. }
  8. }
  9. var a = fun(0)
  10. a.fun(1)
  11. a.fun(2)
  12. a.fun(3)
  13. var b = fun(0).fun(1).fun(2).fun(3)
  14. var c = fun(0).fun(1)
  15. c.fun(2)
  16. 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 的量给回收了。