看啥推荐读物
专栏名称: Crystal_Wei
硕士研究生在读
目录
相关文章推荐
今天看啥  ›  专栏  ›  Crystal_Wei

前端学习之面试题系列:(二)对JS闭包的理解

Crystal_Wei  · 掘金  ·  · 2021-03-01 11:28
阅读 30

前端学习之面试题系列:(二)对JS闭包的理解

写在前面

闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。因此,闭包成为前端面试中经常被问到的一个知识点,比如面试官可能会问:“你了解什么是闭包吗?闭包的作用是什么?请谈谈你对闭包的理解”等等。本文是笔者在学习闭包的过程中总结的笔记,如有理解错误的地方,还请各位大佬多多指正~~~

闭包的理解

1、什么是闭包?

  • 概念

    百度百科原话是这么说的:闭包就是能够读取其他函数内部变量的函数。例如在javascript中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成定义在一个函数内部的函数。在本质上,闭包就是将函数内部和函数外部连接起来的桥梁。

  • 概念的拆分理解

    1. 闭包是什么?

      从概念中的“闭包就是能够读取其他函数内部变量的函数”这句话中抽取主谓宾可知,闭包是函数

    2. 闭包在哪里?

      从概念中的“闭包可以理解成定义在一个函数内部的函数”这句话可知,闭包是定义在函数内部的

    3. 闭包能干啥?

      从概念中的“闭包就是能够读取其他函数内部变量的函数”这句话可知,闭包能读取其他函数内部变量

  • 在代码中理解

    语言总是那么苍白无力,那么就让我们通过代码来感受感受到底什么是闭包?

    function addCounter(){    //addCounter()是一个函数,(外层函数)
        var count = 0         //count是一个被addCounter创建的内部变量(局部变量)
        function add(){       //add()是定义在函数addCounter()内部的函数(闭包)
            return count += 1
        }
        return add            
    }
    var closure = addCounter()  //定义一个全局变量closure来接收函数addCounter的返回值
    console.log(closure)        //closure的值就是add函数
    console.log(closure())      //closure()就是执行add函数,使count值增1,即count=1
    console.log(closure())      //注意这里的执行结果是2了
    复制代码

    执行结果:

    ƒ add(){ 
              return count += 1
            }
    1
    2
    复制代码

    从上面的代码可知,闭包(add函数)的存在,允许定义在addCounter函数外部的closure在可以读取到函数内部定义的局部变量count,从而实现count的增1运算。

  • 闭包的作用

    • 能够访问局部变量

      概念中有这么一句话 “闭包就是能够读取其他函数内部变量的函数”,这里的其他函数就是指闭包的外层函数,外层函数内部定义的变量就是局部变量。

      在上述代码中,add函数(闭包)能够访问它的外层函数addCounter内部定义的局部变量count

    • 保护这些局部变量

      保护这些局部变量是指,闭包可以保护这些局部变量在函数执行完后不被销毁,一直保存在内存中

      在上述代码中,局部变量count 就是这样受到闭包的保护,在函数addCounter执行完毕后,没有被销毁,一直保存在内存中,所以后面可以通过closure修改值。

2、闭包解决经典问题

面试时非常大的概率会遇到下面这道面试题,问你输出什么?

for(var i = 1; i <= 5; i++){
    setTimeout(function(){
        console.log(i)
    }, 1000)
}
复制代码
  • 输出结果分析

    第一次见到这种题时,很自然的觉得会每隔1s连续打印1 2 3 4 5。

    然而事与愿违,这个程序的执行结果却是1s后同时打印出5个6

    要想知道这个代码的输出结果,那么就需要了解这个程序的执行顺序。我们之所以会认为每隔1s连续打印1 2 3 4 5,是因为我们默认每循环一次,console.log(i)就会执行一次。事实上,setTimeout是一个异步函数,它的第一个参数是一个回调函数,这个程序的正确执行顺序是,先执行完循环体,(可以理解为此时已经复制好了5份setTimeout函数),此时的 i已经是6了;然后在1s后开始回调setTimeout中的函数function(){console.log(i)},因此会在1s后同时打印5个6。

  • 代码的修改

    当你回答这个程序的执行结果是1s后同时打印5个6后,面试官会问你如何修改代码,可以输出1 2 3 4 5?

    修改代码的方式有很多种,这里主要解释以下两种解决方案:

    • 方案1:直接将var改为let
      for(let i = 1; i <= 5; i++){
          setTimeout(function(){
              console.log(i)
          }, 1000)
      }
      复制代码

      为什么改为let就可以了呢?原因是 let有块级作用域,而var没有

      在这段程序中,如果改为let关键字,那么在每次循环时,let的值就会和循环体绑定在一起,这样在回调时,就不会输出同样的值了。

      关于letvar的区别,可以看笔者的往期文章 前端学习之面试题系列:(一)var、let、const的区别

      关于let的深入理解,可以看这篇文章 我用了两个月时间才理解let

    • 方案2:利用闭包
      for(var i = 1; i <= 5; i++){
          (function(j){
              setTimeout(function timer(){
                  console.log(j)
              },1000)
          })(i)
      }
      复制代码

      我们首先利用立即执行函数将i的值传给j,这样每次循环的i值都会被保存在函数内部,当执行闭包函数timer时,就可以访问外部函数中保存好的j值,而不是循环结束后i的值了。

    • 其余方案:
      • 利用setTimeout函数的第三个参数保存i

        for(let i = 1; i <= 5; i++){
            setTimeout(function() {
                console.log(i)
            },1000,i)
        }
        复制代码
      • setTimeout函数的第一个参数改为立即执行函数

        for(var i = 1; i <= 5; i++){  
            setTimeout(console.log(i),1000)   //第二个延迟时间参数不起作用
        }
        复制代码
        for(let i = 1; i <= 5; i++){
            setTimeout((function() {
                console.log(i)
            })(),1000)                        //第二个延迟时间参数不起作用
        }
        复制代码
  • 参考文章

    关于JS闭包

    闭包

    彻底理解setTimeOut




原文地址:访问原文地址
快照地址: 访问文章快照