• 从一道经典前端面试题再来看闭包


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

    上面这个内容会打印什么?

    看过这题的都会知道答案,每隔一秒打印一个5,打印5次。如果我想将每一轮循环的i打印出来呢,很简单,将var替换成let;

    这道题真的是考察闭包吗?

    为什么要有闭包?

    因为在JavaScript中,没有办法在函数外部访问到函数内部的变量对象。那么反之,有了闭包,我们可以在函数以外的任何地方访问到函数内部的变量对象。

    (注意,我这里用的是变量对象,而不是某个变量,因为它是一个合集,准确的说,是包含了整个函数作用域。)

    如何写闭包?

    常见的闭包方式是:

    function fn1() {
      var a = 1,
            b = 2;
      return function() {
           return a
      }    
    }
    
    var fn2 = fn1();
    fn2();    // 1

    这里fn1执行完成后,按理说,内部的a、b所在的作用域应该会销毁,但是因为闭包的存在,返回的匿名函数保留了对当前作用域的引用,因此我们可以在fn1执行完成之后,依然可以访问到fn1内部的变量a,这就是闭包的使用。

    (注意,这里虽然只是return了a,但是变量b也在内存中,也没有销毁,因为闭包保存的不是某个变量,而是整个变量对象)

    再来看一些其它闭包例子

    function fn1() {
      var a = 1;  
      setTimeout(function() {
          console.log(a)  
      }, 1000 )  
    }
    
    fn1();
    
    // 1

    当fn1执行完成后,内部作用域并没有销毁,而是被setTimeout保留下来了,因此这也是闭包!

    var a = 1, b = 2;
    
    function () {}
    
    .....
    
    var btn = document.getElementById('btn');
    
    btn.addEventListener('click', function() {}, false);

    没错,这也是闭包!我用DOM2级方式给btn这个dom节点添加事件,尽管里面什么变量都没有引入,但依然保留着外界的变量对象,这也是闭包!

    除了上面这些,还有吗?当然有了,比如每一个带callback回调函数的,都是用了闭包,再比如每一个模块导出的时候,一定会有闭包来访问一些内部的函数或者变量,这也是闭包!

    好了,现在我懂了

    那我们再来回看最初提的那个问题,思考一下

    为什么原题中的代码没有达到我们期待的效果?

    我们所期待的是,每一次for循环,我们都能保存一个i的副本,将它保留下来并传给setTimeout,我们每次循环都会重新定义这个函数,也就是说第一次循环和第二次循环中的setTimeout是不一样的(也就是说循环结束的时候,是有5个函数)。题中的代码也就等同于下面的代码:

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

    setTimeout本身就是一个闭包,而且大括号提供了一个块级作用域,所以我们理想情况下很容易做到,但是却失败了,原因是什么?并不是闭包的问题,而是我们保存的这个i的副本,出了问题。它们都被封闭在一个共享的全局作用域中,实际上只有一个i,看似有了块级作用域,但是没起作用,因为是var声明的变量不存在块级作用域,因此循环结束的时候,“所有”的i,其实也就是一个i,就是5。

    这道题的解题思路是什么?

    其实就是让var声明的变量i保留在块级作用域内。

    那么我们再来看,为什么用let能解决这个问题,很简单,let声明的变量有块级作用域,因此i有了5个副本,并且毫不相关,再配合setTimeout的闭包,我们成功了!

    上面那个方法也等于下面这个

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

    还有没有别的方法了,如果不改变var,如何制造块级作用域?es5里虽然没有块级作用域,但是我们有模拟块级作用域的方法:函数作用域!

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

    这里为了避免变量a污染全局,最后将a赋值为null,当然了,也可以let a ;

    但是这样写又有些繁琐,因为还要创建一个函数a,然后再销毁,那能否不这样呢?

    IIFE!也就是立即执行函数。

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

    综合来看,这道题与其说是考闭包,不如说是考块级作用域的概念,如果硬要考闭包,不如不给代码,把需求告诉他,让他手写一个,这样才行吧。

    对了,这里再补充一点之前提过的,当我用let替换var的时候,既然每次循环都是一个块级作用域,互相不干扰,那为什么i会一直自动加1呢,它是怎么记得上次循环是多少呢?

    因为JavaScript引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。

    end

  • 相关阅读:
    PHP危险函数总结学习
    2019网络与信息安全专项赛题解
    BUUCTF平台-web-边刷边记录-2
    SpringCloud-Config 分布式配置中心
    SpringCloud-Gateway 网关路由、断言、过滤
    SpringCloud-Ribbon负载均衡机制、手写轮询算法
    服务注册与发现-Eureka、Consul、Zookeeper的区别
    Docker 私有仓库搭建
    微服务熔断限流Hystrix之流聚合
    微服务熔断限流Hystrix之Dashboard
  • 原文地址:https://www.cnblogs.com/yanchenyu/p/10038058.html
Copyright © 2020-2023  润新知