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

2019-04-26  本文已影响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。


闭包存在缺点

有两个缺点:

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

解决办法:解除引用循环

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

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

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

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

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

上一篇下一篇

猜你喜欢

热点阅读