• 细说JavaScript闭包


    JavaScript 闭包难点剖析

    一、作用域基本介绍

    ES6之前只有全局作用域与函数作用域两种,ES6出现之后,新增了块级作用域

    1.全局作用域

    在JavaScript中,全局变量是挂载在window对象下的变量,所以在网页中的任何位置你都可以使用并且访问到这个全局变量

    • 当我们定义很多全局变量的时候,会容易引起变量命名的冲突,所以在定义变量的时候应该注意作用域的问题
    var globalName = 'global'
    function getName() {
      console.log(globalName) // global
      var name = 'inner'
      console.log(name) // inner
    }
    getName()
    console.log(name) // 报错
    console.log(globalName) // global
    function setName() {
      vName = 'setName'
    }
    setName()
    console.log(vName) // setName
    console.log(windwo.vName) // setName
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    2.函数作用域

    在JavaScript中,函数定义的变量叫作函数变量,这个时候只能在函数内部才能访问到它,所以它的作用域也就是函数的内存,称为函数作用域

    • 当这个函数被执行完之后,这个局部变量也相应会被销毁。所以你会看到在getName函数外面的name是访问不到的
    function getName() {
      var name = 'inner'
      console.log(name) // inner
    }
    getName()
    console.log(name) // 报错
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    3.块级作用域

    ES6新增了块级作用域,最直接的表现就是新增的let关键词,使用let关键词定义的变量只能在块级作用域中被访问,有"暂时性死区"的特定,也就是说这个变量在定义之前是不能被使用的。

    • if语句及for语句后面的{…}这里面所包括的,就是块级作用域
    console.log(a) // a is not defined
    if (true) {
      let a = '123'
      console.log(a) // 123
    }
    console.log(a) // a is not defined
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    参考视频讲解:进入学习

    二、什么是闭包?

    红宝书:闭包是指有权访问另外一个函数作用域中的变量的函数
    MDN:一个函数和对其周围状态的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。

    1.闭包的基本概念

    • 闭包其实就是一个可以访问其他函数内部变量的函数。即一个定义在函数内部的函数,或者直接说闭包是个内嵌函数也可以。

    • 因为通常情况下,函数内部变量是无法在外部访问的(即全局变量和局部变量的区别),因此使用闭包的作用,就具备实现了能在外部访问某个函数内部变量的功能,让这些内部变量的值始终可以保存在内存中。

    function fun1() {
      var a = 1
      return function () {
        console.log(a)
      }
    }
    fun1()
    var result = fun1()
    result() // 1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    2.闭包产生的原因

    当访问一个变量时,代码解释器会首先在当前的作用域查找,如果没找到,就去父级作用域去查找,直到找到该变量或者不存在父级作用域中,这样的链路就是作用域链。

    var a = 1
    function fun1() {
      var a = 2
      function fun2() {
        var a = 3
        console.log(a) // 3
      }
    }
    // fun1 函数的作用域指向全局作用域(window)和它自己本身;fun2 函数的作用域指向全局作用域(window)、fun1 和它本身;而作用域是从最底层向上找,直到找到全局作用域 window 为止,如果全局还没有的话就会报错
    
    function fun1() {
      var a = 2
      function fun2() {
        console.log(a) // 2
      }
      return fun2
    }
    var result = fun1()
    result()
    // 那是不是只有返回函数才算是产生了闭包呢?其实也不是,回到闭包的本质,**我们只需要让父级作用域的引用存在即可**
    
    var fun3
    function fun1() {
      var a = 2
      fun3 = function () {
        console.log(a)
      }
    }
    fun1()
    fun3()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 闭包产生的本质:当前环境中存在指向父级作用域的引用

    3.闭包的表现形式

    1. 返回一个函数,上面将原因的时候已经说过,这里就不在赘述了

    2. 在定时器、事件监听、Ajax请求、Web Workers 或者任何异步中,只要使用了回调函数,实际上就是在使用闭包。

    // 2.1定时器
    setTimeout(function handler() {
      console.log('1')
    }, 1000)
    // 2.2事件监听
    $('app').click(function () {
      console.log('Event Listener')
    })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    1. 作为函数参数传递的形式,比如下面的例子
    // 3.作为函数参数传递的形式
    var a = 1
    function foo() {
      var a = 2
      function baz() {
        console.log(a)
      }
      bar(baz)
    }
    function bar(fn) {
      // 这就是闭包
      fn()
    }
    foo() // 输出2,而不是1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    1. IIFE(立即执行函数),创建了闭包,保存了全局作用域(window)和当前函数的作用域,因此可以输出全局的变量,如下所示。
    // 4.IIFE(立即执行函数)
    var a = 2
    (function IIFE() {
      console.log(a) // 输出2
    })()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • IIFE 这个函数会稍微有些特殊,算是一种自执行匿名函数,这个匿名函数拥有独立的作用域。这不仅可以避免了外界访问此 IIFE 中的变量,而且又不会污染全局作用域,我们经常能在高级的 JavaScript 编程中看见此类函数。

    三、如何解决循环输出问题?

    for (var i = 1; i <= 5; i++) {
      setTimeout(function () {
        console.log(i)
      }, 0)
    }
    // 依次输出 5个6
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    1. setTimeout 为宏任务,由于 JS 中单线程 eventLoop 机制,在主线程同步任务执行完后才去执行宏任务,因此循环结束后 setTimeout 中的回调才依次执行
    2. 因为 setTimeout 函数也是一种闭包,往上找它的父级作用域就是 window,变量 i 为 window 上的全局变量,开始执行 setTimeout 之前变量 i 已经是 6 了,因此最后输出的连续都是 6

    1.利用 IIFE

    利用 IIFE,当每次 for 循环时,把此时的变量 i 传递到定时器中,然后执行

    for (var i = 1; i <= 5; i++) {
      (function (j) {
        setTimeout(function timer() {
          console.log(j)
        }, 0)
      })(i)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    2.使用 ES6 中的 let

    let 让 JS 有了块级作用域,代码的作用域以块级为单位进行执行。

    for(let i = 1; i <= 5; i++) {
      setTimeout(function() {
        console.log()
      },0)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    3.定时器传入第三个参数

    setTimeout 作为经常使用的定时器,它是存在第三个参数的,日常工作中我们经常使用的一般是前两个,一个是回调函数,另外一个是时间,而第三个参数用得比较少。

    for(var i=1;i<=5;i++) {
      setTimeout(function(j) {
        console.log(j)
      },0,i)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 第三个参数的传递,改变了 setTimeout 的执行逻辑,从而实现我们想要的结果,这也是一种解决循环输出问题的途径
  • 相关阅读:
    表达式转换
    通过HbuilderX启动 微信开发者工具
    k8s篇之四、service
    初级网络工程师之从入门到入狱(二)
    win10端口转发
    【网络文明】关注网络安全
    ETL是什么
    Linux学习-数据类型学习
    【无标题】
    HT5010 音频转换器工作原理
  • 原文地址:https://blog.csdn.net/hellocoder2029/article/details/127734014