闭包!闭包!闭包!

1,275 阅读7分钟
闭包,是 Javascript 比较重要的一个概念,
对于初学者来讲,闭包是一个特别抽象的概念,
特别是 ECMAScript 规范给的定义,很晦涩

很多人分不清。外部函数,内部函数,外部变量。互相引用。

嘚吧嘚吧。最后看着专业的文章。来一句。写的什么鬼。。。。

这篇文章,从最基础的说起。解释一波。看完即使不会用,也会说

闭包的概念

接触任何新东西,概念总是晦涩的,看代码比自然语言更能理解一个事物的本质。其实,闭包无处不在,比如:jQuery就是一个大的闭包,下面先写一个最简单的闭包。一步步拆解讲一下。

function A(){
    function B(){
       console.log('jiguangxin');
    }
    return B;
}
var C = A();
C();// jiguangxin!

这是最简单的闭包。

有了初步认识后,我们简单分析一下它和普通函数有什么不同,上面代码翻译成自然语言如下:

1.定义普通函数 A

2.在 A 中定义普通函数 B

3.在 A 中返回 B

4.执行 A,并把 A 的返回结果赋值给变量 C  (这一步很重要。)

5.执行 C 

把这5步操作总结成一句话就是:

外部函数A 的 内部函数B 被 外部函数A 外的一个变量 c 引用。

把这句话再加工一下就变成了闭包的定义:

当一个内部函数被其外部函数之外的变量引用时,就形成了一个闭包。

bingo!

这就是闭包的概念。记住就好,这就是概念。

闭包的牛刀小试

在了解闭包的作用之前,我们先了解一下 Javascript 中的 垃圾回收 机制: 在 Javascript 中,如果一个对象不再被引用,那么这个对象就会被 回收,否则这个对象一直会保存在内存中。

在上述例子中,
函数B 定义在 函数A 中,
因此 函数B 依赖于 函数A ,
而外部变量 C 又引用了 函数B , 
所以函数A间接的被 C 引用。

也就是说,A 不会被回收,如果A中定义了变量,同样也不会被回收,会一直保存在内存中。为了证明我们的推理,上面的例子稍作改进:

function A() {
    var count = 0;
    function B() {
       count ++;
       console.log(count);
    }
    return B;
}
var C = A();
C();// 1
C();// 2
C();// 3

正常来说,。一个函数执行完。这个函数就应该被销毁。里面的变量也会消失。

count 是函数A 中的一个变量,它的值在函数B 中被改变,函数 B 每执行一次,count 的值就在原来的基础上累加 1 。因此,函数A中的 count 变量会一直保存在内存中。

当我们需要在模块中定义一些变量,并希望这些变量一直保存在内存中但又不会 “污染” 全局的变量时,就可以用闭包来定义这个模块。

闭包的应用实例

上面只是简单用一下闭包,看一下闭包到底是干什么的

实际开发中我们能用闭包干什么啊?

直接上代码

/*
 这个函数啊,如果使用普通的这个方法,
 返回的结果永远是首次传入,因为。函数里面的list变量。在每次执行完函数以后
 就销毁掉了,但是用闭包。可以延长变量的生存周期。
*/
function isFirstLoad(){
            var list=[];
            return function(option){
                if(list.indexOf(option)>=0){ //检测是否存在于现有数组中,有则说明已存在
                    console.log('已存在')
                }else{
                    list.push(option);
                    console.log('首次传入'); //没有则返回true,并把这次的数据录入进去
                }
            }
        }

var ifl = isFirstLoad();

ifl("zhangsan"); // 首次传入
ifl("lisi");  // 首次传入
ifl("zhangsan"); // 已存在
/*
 这个可以作为一个公共方法使用,
 别的函数可以使用这个函数来判断自己的字符串是否已经存在。
 如果没有里面的闭包。其他函数在调用isFirstLoad的时候
 每次调用完。list变量就会销毁。其他函数在调用。,
 永远都是首次传入,如果用闭包。list的变量被里面的匿名函数闭包保住了。延长了变量的生命周期
*/

上面的代码就是一个可以服用的公共方法。判断一个字符串是否已经在一个数组里面。而且方法可以一直使用。如果不使用闭包,就不是一个服用的方法了,因为list变量,在函数isFirstLoad被调用完就会消失。。。。。。使用闭包,变量就会一直存在。继续使用

匿名函数和闭包之间的关系

  1. 接触闭包过程中,有具名函数闭包的。有匿名函数闭包的。很混乱。到底匿名函数和闭包有什么关系呢(实际上没有任何鸟关系)。

  2. 再说匿名函数,一般用到匿名函数的时候都是立即执行的。通常叫做自执行匿名函数或者自调用匿名函数。常用来构建沙箱模式,作用是开辟封闭的变量作用域环境jquery的源码就是在一个大匿名函数里面执行

  3. 一般情况下,匿名函数写法这两种

// 括号的位置是区别
(function(){ 
  console.log("我是匿名方式1");
})();//我是匿名方式1

(function(){ 
  console.log("我是匿名方式2");
}());//我是匿名方式2
  1. 示范一下具名的闭包函数怎么转换成匿名函数
// 这是具名的函数,
function box(){
  var a = 10;
  return function inner(){
    console.log(a) ; 
  }
}
var outer = box();
outer()


//第一步直把内部inner这个具名函数改为匿名函数并直接return, 结果同样是10,里面改成匿名的函数

function box(){
  var a = 10;
  return function(){
    console.log(a) ; 
  }
}
var outer = box();
outer();//10

//第二步把外部var outer = box()改成立即执行的匿名函数,两个都是匿名的

var outer = (function(){
  var a=10;
  return function(){
    console.log(a);
  }
})();
/*
outer 作为立即执行匿名函数执行结果的一个接收,这个执行结果是闭包,outer等于这个闭包。
执行outer就相当于执行了匿名函数内部return返回的闭包函数
这个闭包函数可以访问到匿名函数内部的私有变量a,所以打印出10
*/
outer();//10
  1. 由此可见。其实匿名,具名 函数和形成闭包没有任何关系,只是不同的展示方式而已

for循环经典闭包案子的分析

这个问题面试题啊。考点啊,都很多的,很多人研究不清楚里面的到底是什么原因,现在就解释一波,不对的地方,也请指正。

  1. 直接上代码
// 老生长谈的问题之一啊
for(var i = 0;i<5;i++){
  setTimeout(function(){
    console.log(i);
  },100*i);
}
/*
我们希望打印出来0,1,2,3,4,然而打印出来的是5个5,很尴尬。什么原因引起的这问题呢?
这是因为setTimeout的回调函数并不是立即执行的而是要等到循环结束才开始计时和执行(在此对运行机制不伸展),
要说明的一点是js中函数在执行前都只对变量保持引用,并不会真正获取和保存变量的值。所以等循环结束后i的值是已经是5了,
因此执行定时器的回调函数会打印出5个5。
*/
  1. 解决办法
// 给异步函数套一个立即执行的匿名函数。里面的函数使用了外部匿名函数的变量i,形成闭包,

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

// 这个匿名的其实等同于这个具名的函数

for(var i = 0;i<5;i++){
  function hasNameFn(i){
    setTimeout(function(){
      console.log(i);
    },100*i);
  };
  hasNameFn(i);
}
/*而自执行的匿名函数的作用也很简单:就是每一次循环创建一个私有词法环境,
执行时把当前的循环的i传入,保存在这个词法环境中
这样执行for循环的时候。生成了10个函数(两两嵌套),每个函数里面都有自己的词法作用域。变量生命周期
被延长。settimeout寻找i的时候就在自己的作用域里面找。如同下面这个情况
*/

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