美文网首页
前端的那些事(三):你真的理解闭包吗

前端的那些事(三):你真的理解闭包吗

作者: 沐雨芝录 | 来源:发表于2019-04-26 18:11 被阅读0次

    前言

    看了很多书籍和文章,使用用闭包的原因说的真是管中窥豹,凭什么说函数作用域外部无法访问就用闭包,看下面的代码,照样也能访问内部变量,函数也可以获取内部变量。

    var count = 0;
    function foo() {
      count++;
      var a = 1;
      var b = 2;
      return {
        a,
        b
      };
    }
    console.log(foo().a, count); // 1  1
    console.log(foo().b, count); // 2  2
    

    此处count是记录函数调用了几次,在这里调用了两次。

    思考

    那我用普通函数就好啦,用闭包干嘛呀?

    解答

    • 肯定是想在外部获取函数内部数据。
    • 不知道你们注意到没有,我想获取a,b,是不是调用了两次foo函数count = 2; 这样就执行创建了两次var a = 1; var b = 1; 当这其中代码很多很复杂的时候,你就消耗了很多性能
    • 职责单一,不耦合代码是我们书写良好js的基本功,我们期望a,b职责不同,应该有自己的函数去做处理。
    • 其他的避免全局变量污染。
    • 传参减少作用域查询。
    我们将上述代码改变下
    var count = 0;
    function foo() {
      count++;
      var a = 1;
      var b = 2;
      return {
        fna: function() {
          return a;
        },
        fnb: function() {
          return b;
        }
      };
    }
    var f = foo();
    console.log(f.fna(), count); // 1  1
    console.log(f.fnb(), count); // 2   1
    

    很多人是不是想说,看起来好像更复杂了呀,确实复杂了。但是却有两个好处。1、职责单一;2、只调用了一次foo(),count一直是1;无论你想获取多少次函数数据,都只调用一次外层函数,性能好;

    如果你觉得闭包就这样,还用它干嘛,那就错了,它能做很多事情。从简到难:

    \color{#3f51b5}{1、函数外获取内部数据}
      函数内部其实都是私有属性,我们可以自由暴露其为共用属性。

    function foo() {
      var num = Math.random();
      return function fn() {
        return num;
      };
    }
    var f = foo();
    console.log(f());
    

    \color{#3f51b5}{2、完成读取一个数据和修改这个数据}

    function foo () {
        var num = Math.random();
        return {
            get_num : function () {
                return num;
            },
            set_num: function( value ) {
                return num = value;
            }
        }
    }
    

    \color{#3f51b5}{3、沙箱模式(自调用函数)}
      教大家写jquery实现原理。

    (function() {
        var jQuery = function() {};
        jQuery.custom = function() {
          return "jQuery的自定义方法";
        };
        window.jQuery = window.$ = jQuery;
    })();
    console.log($.custom());
    

    \color{#3f51b5}{4、立即执行函数(匿名闭包)}
    经典面试题:以下函数打印什么?

    for (var i = 0; i <= 5; i++) {
      setTimeout(function timer() {
        console.log(i); // 6个6
      }, 0);
    }
    

    如何打印0,1,2,3,4,5呢?

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

    \color{red}{解答}

    1、js循环会进入队列(先进先出的一种数据结构,数组、对象就是数据结构)。
    2、promise是微异步,setTimeout是宏异步,意思就是setTimeout会等队列执行完成以后,在执行。你想想循环执行完了这个i是多少,你在for循环外层打印,结果就是执行完的,就是6。
    3、那咋办,咱用缓存呀,在for循环队列的时候,我把i给缓存下来呀,匿名闭包就可以在内部访问外层的作用域,拿过来保存了。

    ps:var换成let块级作用域也可以,简单方便。

    也可以这么写代码实现:(更好理解是缓存的原因)

    for (var i = 0; i <= 5; i++) {
      function fn() {
        var i_ = i;
        setTimeout(function timer() {
          console.log(i_);
        }, 0);
      }
      fn();
    }
    

    \color{#3f51b5}{5、闭包缓存机制}
      以斐波那契数列为例,给大家普及下这是什么东西,就是兔子数列。

    在第一个月有一对刚出生的小兔子,在第二个月小兔子变成大兔子并开始怀孕,第三个月大兔子会生下一对小兔子,并且以后每个月都会生下一对小兔子。 如果每对兔子都经历这样的出生、成熟、生育的过程,并且兔子永远不死,那么兔子的总数是如何变化的?得到的就是斐波那契数列。

    月份:兔子数目。如下就是num对应foo(num)的值。

    var count = 0;
    function foo(num) {
        count++;
        if (num === 0) return 0;
        if (num === 1) return 1;
        return foo(num - 1) + foo(num - 2);
    }
    var f = foo(15);
    console.log(f); // 610
    console.log(count); // 1973
    

    我们的count就是用来计数的,记录到底执行了多少次这个函数,发现1973次,当num越大调用次数越大,建议不要num设置很大,要么页面卡死,要么爆栈了。

    这里抛出时间复杂度与空间复杂度,如何计算,后续会有专门文章去写。

    • 时间复杂度:O(2^N)
    • 空间复杂度:O(N)
      时间复杂度是指数阶,属于爆炸增量函数,在程序设计中我们应该避免这样的复杂度。
    改进版:
    var data = [1, 1];
    var count = 0;
    function foo(num) {
      count++;
      var v = data[num];
      if (v === undefined) {
        v = foo(num - 1) + foo(num - 2);
        data[num] = v;
      }
      return v;
    }
    foo(15);
    console.log(count); // 29
    

    大家可以看出这个递归,其实我们也是对递归进行缓存优化。

    缓存思路:
    • 普通递归,最郁闷的地方是,每次进入新的递归,之前算好的数据,它还会重新计算。所以性能极差。
    • 其实斐波那契数列本身就是一个数组,我们可以预定义一个数组data去缓存之前已经计算的数据,这样深层递归就不用重新计算了。
    • 当大家用递归的时候一定要使用缓存机制,提高的性能超一般的大。新的时间复杂度:O(N) ;空间复杂度:O(1)。

    \color{#3f51b5}{6、模块化的基础}
    模块化封装:

    var MyModules = (function Manager() {
      var modules = {};
      function define(name, deps, impl) {
        for (var i = 0; i < deps.length; i++) {
          deps[i] = modules[deps[i]];
        }
        modules[name] = impl.apply(impl, deps);
      }
      function get(name) {
        return modules[name];
      }
      return {
        define: define,
        get: get
      };
    })();
    

    模块化适用:

    
    MyModules.define('bar', [], function() {
      function hello(who) {
        return 'Let me introduce: ' + who;
      }
      return {
        hello: hello
      };
    });
    MyModules.define('foo', ['bar'], function(bar) {
      var hungry = 'hippo';
      function awesome() {
        console.log(bar.hello(hungry).toUpperCase());
      }
      return {
        awesome: awesome
      };
    });
    var bar = MyModules.get('bar');
    var foo = MyModules.get('foo');
    

    当然并非那么简单,你还需要做到:先加载所有定义好的模块,然后再使用,这里推荐AMD、CMD、es6 module。


    闭包存在缺点

    有两个缺点:

    • \color{#3f51b5}{引用占用内存}

    普及下垃圾回收机制两种方法:标记清除和引用计数,有兴趣的同学可以自行学习。

    解决办法:解除引用循环

    function foo() {
      var num = Math.random();
      return function fn() {
        return num;
      };
    }
    var f = foo();
    f = null; // 释放内存
    

    当我们使用闭包的时候,那么浏览器的GC(垃圾收集器)就无法自动释放内存,当你不使用的时候就设置f = null,下次GC运行时就会去释放缓存。

    • \color{#3f51b5}{内存泄漏}

    解决办法:不需要的变量设置null

    function assignHandler(){
        var el= document.getElementById("div");
        el.onclick = function(){
           
        };
        el = null;
    }
    

    更多内容可以看我的集录: 全面攻陷js:更新中...

    相关文章

      网友评论

          本文标题:前端的那些事(三):你真的理解闭包吗

          本文链接:https://www.haomeiwen.com/subject/dalzgqtx.html