大厂面试之闭包问题详解

大厂面试之闭包问题详解

1、闭包的表面现象
2、闭包的底层原理
3、闭包的形式
4、经典题

什么是闭包?

闭包的表面现象

产生闭包的条件:
1、有父子函数的关系
2、子函数使用了父函数的变量
3、子函数有调用

如下代码,子函数可以使用父函数身上的变量

   function father() {
       var n = 10;
       function son() {
           n++;
           console.log(n)
       }
      son()
   }
   father()

下面这个也可以形成闭包

	function father() {
      var n = 10;
       function son() {
           n++;
           console.log(n)
       }
       return son;
   }
   var result = father();
   result();

将son这个函数 return出来,让result接收,result接收的就是function(){…}

闭包的底层原理

  • 变量的生命周期
  • 垃圾回收机制
  • 执行上下文
  • 上下文执行栈

1、变量的生命周期--定义一个变量到不在使用这个变量,局部变量和全局变量

  • 局部变量的生命周期在函数执行完成以后就到头了

  • 全局变量的生命周期在页面关闭后就到头了

当fn()调用完成后,a的生命周期就到头了

function fn(){
    var a = 10;
}
fn();
console.log(a);

大家都知道console里面的a是访问不到的,这是为什么呢?因为js有垃圾回收机制

2、垃圾回收机制

  • 标记清除(大部分浏览器都使用的这种技术)–会给每一个被声明的变量打上标记(进入环境),如下,
    在var a的时候会标记‘进入环境’,在fn()调用的时候会标记‘离开环境’
    在‘离开环境’的时候垃圾回收机制就回收了这个变量
    function fn(){var a = 10} fn()

  • 引用计数–在var a 的时候标记一个1,当代码中出现a = null,标记‘0’,然后就被回收

    注意:如果这个数据有引用的关系,就不会被回收

3、执行上下文(当前代码的执行环境,简称EC)当js代码在执行过程中,遇到一下三种情况都会 产生一个EC,然后把EC放入ECS(上下文执行栈)中

  • 全局环境

  • 函数环境

  • eval环境–有安全性问题,不建议用

function father(){
    function son(){

     }
     son()
 }
 father()

1、代码走到script标签的时候,此时是一个全局环境,然后会创建一个执行上下文,名字叫做全局环境执行上下文(globalEC),把创建的globalEC放入ECS。
2、接着往下走,走到函数调用father()的时候,会创建一个函数环境执行上下文(fatherEC),放入ECS。
3、接着走函数内部,又创建一个函数环境执行上下文(sonEC),放入ECS,当son()调用完了后,把sonEC从栈里拿出来删掉,代码执行完后,globalEC出栈

执行栈如下图所示------== 图一进栈,图二出栈 ==
在这里插入图片描述
在这里插入图片描述

再来一段代码解释上下文执行栈

function father(){
   var n = 10;
    function son(){
        n++;
        console.log(n)
    }
    return son;
}
var result = father();
result();

1、代码走到script标签的时候,此时是一个全局环境,然后会创建一个执行上下文,名字叫做全局环境执行上下文(globalEC),把创建的globalEC放入ECS。
2、接着往下走,走到函数调用father()的时候,会创建一个函数环境执行上下文(fatherEC),放入ECS,由于函数内部没有调用son,所以函数走完了fatherEC就出栈了,
3、接着走到result(),又创建一个函数环境执行上下文(sonEC),放入ECS,当son()调用完了后,把sonEC从栈里拿出来删掉,代码执行完后,globalEC出栈

4、上下文执行栈(ECS),别名,函数调用栈(call stack)

  • 变量对象

创建完EC后需要走两个阶段
1、创建阶段
2、代码执行阶段

两个阶段的代码如下展示,展示的只是伪代码

function father(){
     var n = 10;
     function son(){
         n++;
         console.log(n)
     }
     return son;
 }
 var result = father();
 result();
 result();
   //fatherEC的内部走的路子
	// 1、创建阶段
      fatherEC = {
          VO:{ //变量对象
              // 找变量、找函数、找参数、找arguments对象
              n:undefined,  //找变量声明
              son:'son在内存里的引用地址'
          },
          scope:[  //存储作用域链
              Global.AO,
          ],
          this:'window'
      }

    // 2、执行阶段--伪代码
    fatherEC = {
        AO:{ //变量对象
            n:10,  //变量赋值
            son:'...'  //函数赋函数的引用
        },
        scope:[  //存储作用域链
            fatherEC.AO,Global.AO,
        ],
        this:'window'
    }



    //sonEC的内部走的路子
    // 1、创建阶段----伪代码
    sonEC = {
        VO:{ //变量对象
        },
        scope:[  //存储作用域链
            fatherEC.AO,Global.AO,
        ],
        // this:'window'
    }

    // 2、执行阶段--伪代码
    sonEC = {
        AO:{ //变量对象
        },
        scope:[  //存储作用域链
            sonEC.AO,fatherEC.AO,Global.AO,
        ],
        // this:'window'
    }
    //因为son里面有n,所以n就会在sonEC中的执行阶段中的scope中从
    //左往右的找n,sonEC.AO里面的VO没有,就找fatherEC.AO,直到找到n为止。

闭包的形式

下面的代码不是一个闭包,仔细分析会发现innerFn的参数

 var a = 20;
 function wrapFn(){
    var b = 10;
    function innerFn(b){
        console.log(b);
    }
     return innerFn;
 }
 var fn = wrapFn();
 fn(a);

下面的代码是一个闭包 --闭包的形式

 var a = 20;
  function wrapFn(){  // 这个函数就是一个闭包---根据谷歌浏览器为准
      var b = 10;
      function innerFn(){
          console.log(b);
      }
      return innerFn;
  }
  var fn = wrapFn();
  fn(a);

在这里插入图片描述

 function wrapFn(){  //1
            var a = 10;
            return function(){  //2
                var b = 20;
                return function(){  //3
                    console.log(b) //此时的闭包函数是2
                    console.log(a) //此时的闭包函数是1 2
                    debugger;
                }
            }
        }
        var son = wrapFn()()();

有一个特殊情况的闭包—只要子函数与父函数能够凑到一起满足闭包的条件,那就形成了闭包
此时的innerFn1使用了父函数的变量,但是没有调用innerFn1,调用的是innerFn2

 function wrapFn(){ //这是一个闭包函数
    var a = 10;
    function innerFn1(){
        return a;   //innerFn1是一个子函数,它的里面用到了a,这个a是父级的,但是这个函数并没有被调用
    }
    function innerFn2(){    //innerFn2是一个子函数,它里面没有用到a,但是它调用了,所以就让父函数形成了一个闭包环境
        debugger;
    }
    innerFn2()
}
wrapFn();

经典题

<body>
    <ul>
        <li>red</li>
        <li>pink</li>
        <li>green</li>
        <li>blue</li>
        <li>yellow</li>
    </ul>

    <script>
        var lis = document.querySelectorAll('li');
        for(var i =0;i<lis.length;i++){
            lis[i].onclick = function(){
                console.log(i)
            }
        }//i每走一次,就会垃圾回收机制回收,改善后的代码为如下


        var lis = document.querySelectorAll('li');
        for(var i =0;i<lis.length;i++){
            (function(){
                lis[i].onclick = function(){
                    console.log(i)
                }
            }(i));//for循环走一次,声明的i在function里面有引用的关系,
            // 有引用的关系就不会被回收,然后5个i就会被存在内存中
        }
    </script>
</body>

注意:
1、普通函数,定义函数的时候是嵌套的,调用的时候也是嵌套的
2、闭包函数,定义函数的时候是嵌套的,调用的时候是独立的

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值