• module1-05-JS 闭包难点剖析


    JS 闭包难点剖析

    • javaScript中闭包是相当重要的概念,并且与作用域相关知识的指向密切相关。

    • 思考题:

      • ① JS中的作用域是什么意思

      • ② 闭包会在那些场景中使用

      • ③ 通过定时器循环输出自增的数字通过JS的代码如何实现?

    一、作用域、闭包介绍

    1.1 作用域

    • 在ES6出现之前只存在全局作用域函数作用域

    • 在ES6出现之后出现了块级作用域

    console.log(a) //a is not defined
    if (true) {
     let a = '123'
     console.log(a)// 123
    }
    console.log(a) //a is not defined
    • 可以看出在if的{}里面使用 let 声明的a在 {} 之外就无法访问到

    1.2 闭包

    (1)红宝书和MDN上给出闭包的概念

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

    (2)闭包的基本概念

    • 闭包其实就是一个可以访问到其他函数内部变量的函数,即一个定义在函数内部的函数

    • 因为通常的情况下,函数内部变量是无法在外部访问的

      • 比如返回的函数能访问到fun1里面的a

    function fun1() {
    var a = 1;
    return function(){
    console.log(a);
    };
    }
    fun1();
    var result = fun1();
    result();  // 1

    (3)闭包产生的原因

    • 这里面需要了解到作用域链的概念,而什么是作用域链呢?

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

        • 比如这里再fun2寻找a的时候,现在自身的作用域查找,没有的话区fun1,再去全局

    var a = 1;
    function fun1() {
     var a = 2
     function fun2() {
       var a = 3;
       console.log(a);//3
    }
    }
    • 所以说明了 当前函数一般都会存在上层函数的作用域的引用,那么就形成了一条作用域链

    • 所以产生闭包的实质就是,当前环境中存在指向父级作用域的引用

    (4)闭包的不同出现情况

    • 从上面的例子可以知道,闭包都是在一个函数中返回另外一个函数,那么如果没有返回的函数的

      • 就比如换一种闭包的情况

    var fun3;
    function fun1() {
     var a = 2
     fun3 = function() {
       console.log(a);
    }
    }
    fun1();
    fun3();
    • 通过闭包的实质我们可以知道,只需要存在指向父级的引用即可,所以代码里面的fun3是一个存在与全局的变量,而再fun1里面,用了赋予一个匿名函数的创建方法,也可以获得指向父级的a

      • 所以不管有没有放回函数,只需要满足条件即可

    二、闭包的表现形式

    • 在明白了闭包的本质之后,可以看看闭包的表现形式以及应用场景到底有哪些

    (1)返回一个函数,上面已经讲过了

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

    // 定时器
    setTimeout(function handler(){
     console.log('1');
    },1000);
    // 事件监听
    $('#app').click(function(){
     console.log('Event Listener');
    });

    (3)作为函数参数传递的形式

    var a = 1;
    function foo(){
     var a = 2;
     function baz(){
       console.log(a);
    }
     bar(baz);
    }
    function bar(fn){
     // 这就是闭包
     fn();
    }
    foo();  // 输出2,而不是1

    (4)IIFE

    • 创建了闭包,保存了全局作用域(window)和当前函数䣌作用域,因此可以输出全局的变量

      • 这个函数会稍微有些特殊,算是一种自执行的匿名函数,这个匿名函数有独特的作用域,可以避免外界访问此IIFE中的变量,而且不会污染全局作用域

    var a = 2;
    (function IIFE(){
     console.log(a);  // 输出2
    })();

    三、使用闭包解决实际问题

    (1)循环输出问题

    for(var i = 1; i <= 5; i ++){
     setTimeout(function() {
       console.log(i)
    }, 0)
    }
    • 这是一个非常经典的题目,相信我不用多说了

      • 这段代码的输出结果是 5 个 6

      • 提出疑问

        • ① 为什么是5个6

        • ② 怎么输出1、2、3、4、5

    分析出现问题的原因

    • ① setTimeout 为宏任务,由于 JS 中单线程 eventLoop 机制,在主线程同步任务执行完后才去执行宏任务,因此循环结束后 setTimeout 中的回调才依次执行

    • ② 因为 setTimeout 函数也是一种闭包,往上找它的父级作用域链就是 window,变量 i 为 window 上的全局变量,开始执行 setTimeout 之前变量 i 已经就是 6 了,因此最后输出的连续就都是 6

    解决问题

    • 利用IIFE

    for(var i = 1;i <= 5;i++){
      (function(j){
        setTimeout(function timer(){
          console.log(j)
        }, 0)
      })(i)
    }
    
    • 利用ES6中的let

      • 最优解决

    for(let i = 1; i <= 5; i++){
      setTimeout(function() {
        console.log(i);
      },0)
    }
    
    • 定时器传入第三个参数

      • 很多人不知道其实setTimeout存在第三个参数的,第三个参数。即引用i此时的状态的值

    for(var i=1;i<=5;i++){
      setTimeout(function(j) {
        console.log(j)
      }, 0, i)
    }
    

    四、总结

    • 其实闭包的使用在日常的 JavaScript 编程中经常出现,使用的场景特别多而且复杂。由于闭包会使一些变量一直保存在内存中不会自动释放,所以如果大量使用的话就会消耗大量内存,从而影响网页性能。因此,你更应该深入理解闭包的原理,从而保证交付的代码性能更好。

  • 相关阅读:
    MyBatis中#{}和${}的区别
    springBoot 配置详解
    模板方法模式
    记录一次linux挂载数据盘
    缓存击穿,缓存穿透,缓存雪崩
    Echart折线值相加问题
    mybatis plus主键生成策略
    BigDecimal精度损失
    cent0s6安装nginx小程序https
    Centos6 java运行环境部署
  • 原文地址:https://www.cnblogs.com/lezaizhu/p/14455789.html
Copyright © 2020-2023  润新知