一篇文章带你理解闭包

于梦中2010

走在前端的大道上

本篇将自己读过的相关 javascript闭包 文章中,对自己有启发的章节片段总结在这(会对原文进行删改),会不断丰富提炼总结更新。

首先着重回顾一下 全局变量 和 局部变量

全局变量:可以在任意位置访问的量就叫 全局变量

 

var age = 20;
 function a(){
     console.log(age); //20
}
a();

局部变量:函数中用var定义的变量,只能在函数中访问这个变量,函数外部访问不了。

function a(){
    var age = 20;
}
a();
console.log(age); // Uncaught ReferenceError: age is not defined

重点:

1.在函数中如果不使用var定义变量那么js引擎会自动添加成全局变量。
2.全局变量从创建的那一刻起就会一直保存在内存中,除非你关闭这个页面,局部变量当函数运行完以后就会销毁这个变量,假如有多次调用这个函数它下一次调用的时候又会重新创建那个变量,既运行完就销毁,回到最初的状态,简单来说局部变量是一次性的,用完就扔,下次要我再重新创建。

函数的相关知识点:

1.一个函数内可以嵌套多个函数
2.函数里面的子函数可以访问它上级定义的变量,注意不只是一级,如果上级没有会继续往上级找,直到找到为止,如果找到全局变量到找不到就会报错。

function a(){
 var name = "追梦子";
 function b(){
     console.log(name); // "追梦子"
 }
 b();
}
a();

3.函数的另外一种调用形式,你可以把它叫做自调用,自己调用自己,达到自执行的效果。

var a = 0;
(function(){
   console.log(++a); // 1
})()

这种方式用()把内容包裹起来,后面的()表示执行这个函数,可能你会问为什么要把函数包起来,如果不包裹起来,js会把它当作函数声明来处理,如果包裹起来就是表达式。

正题

闭包说得通熟易懂一点,就是指 有权访问另一个函数作用域变量的函数。闭包可以解决函数外部无法访问函数内部变量的问题。创建闭包的常见方式,就是在一个函数内部创建另外一个函数,并返回。

下面是一段没有使用闭包的代码:

function fn(){
 var a = 10; //报错
}
alert(a);

因为a没有定义,虽然函数fn里面定义了a但是,但是它只能在函数fn中使用。也就是作用域的问题。

function fn(){
  //定义了一个变量name
 var name = '追梦子';
  //外部想访问这个变量name怎么办?return!把它返回出去,再用个变量接收一下不就可以了
  return name;
}
var name = fn();//接收fn返回的name值。
alert(name);//追梦子;

这里的闭包就是利用函数的return。除了通过return还可以通过其他的几种方法如下:
方法1:

function fn(){
  var a = 0;
  b = a;
}
alert(b)

 这里利用了js的一个特性,如果在函数中没有用var定义变量,那么这个变量属于全局的,但这种方法多少有些不好。

方法2:

var f = null;
function fn(){
  var a = 0;
  f = function(){
    a++;
    f.a = a;
  };
}
fn();
f();
alert(f.a);//1
f();
alert(f.a);//2

其实闭包还有一个很重要的特性,来看一个例子。

var lis= document.getElementsByTagName['li']; 
 //假如这段代码中的lis.length = 5;
for(var i=0;i<lis.length;i++){
 lis[i].onclick = function(){
  alert(i);
 };
}

最终结果是不管单击哪个li元素都是弹5。不信你试试。为什么呢。看下面分析。

for(var i=0;i<lis.length;i++){ }
      // i = 5对吧
  lis[0].onclick = function(){
    alert(i); 
  };
  lis[1].onclick = function(){
    alert(i); 
  };
  lis[2].onclick = function(){
    alert(i);
  };
  lis[3].onclick = function(){
    alert(i);
  };
  lis[4].onclick = function(){
    alert(i);
  };

为什么会这样呢,因为你for循环只是给li绑定事件,但是里面的函数代码并不会执行啊,这个执行是在你点击的时候才执行的好吧?但是此时的i已经是5了,所以所有的都打印出5来了。

for(var i=0;i<lis.length;i++){ 
   (function(i){
      lis[i].onclick = function(){
         console.log(i); //点击第几个返回第几个
      }
   })(i)
}
 

闭包的特点不只是让函数外部访问函数内部变量这么简单,还有一个大的特点就是 通过闭包可以让函数中的变量持久保持

function fn(){
   var num = 5;
   num+=1;
   alert(num);
}  
 fn(); //6
 fn(); //6

为什么都是 6 呢?因为 函数一旦调用里面的内容就会被销毁,下一次调用又是一个新的函数,和上一个调用的不相关了。

划重点:JavaScript中有回收机制,函数没有被引用 且执行完以后这个函数的作用域就会被销毁,如果一个函数被其他变量引用,这个函数的作用域将不会被销毁,(简单来说就是函数里面的变量会被保存下来,你可以理解成全局变量。)

再来

function fn(){
 var num = 0;
 return function(){
   num+=1;
     alert(num);   
   };  
}
var f = fn();
f(); //1
f(); //2

定义了一个fn函数,里面有个num默认为0,接着返回了一个匿名函数(也就是没有名字的函数)。我们在外部用 f 接收这个返回的函数。这个匿名函数干的事情就是把 num 加 1,还有我们用来调试的 alert 。

这里之所以执行完这个函数 num 没有被销毁,是因为那个匿名函数的问题,因为这个匿名函数用到了这个 num,所以没有被销毁,一直保持在内存中,因此我们 f() 时 num 可以一直加。

function a(){
    var aa = 0;
    function b(){
        aa ++;
        console.log(aa);
    }
    return b;
}
var ab = a();
ab(); //1
ab(); //2

里面的变量的值没有被销毁,因为函数a被外部的变量ab引用,所以变量aa没有被回收。

如果某个函数被它的父函数之外的一个变量引用,就形成了一个闭包

还有一种更为常用的闭包写法

var bi = (function(){
    var a = 0;
    function b(){
        a ++;
        console.log(a);
    }
    return b;
})();

bi(); //1
bi(); //2
bi(); //3

执行过程分析:

  首先把一个自执行函数赋值给了bi,这个自执行函数运行完成以后就bi的值就变成了

function b(){
    a ++;
    console.log(a);
}

  因为我们在上面的代码 return 回去了 b,然后因为这个自执行函数被 bi 引用所以里面的变量 a 并没有因为这个自执行函数执完而销毁,而是保存到了内存中,所以我们多次打印 bi()
就成了1、2、3

闭包是使用可以带来以下好处:

  1. 希望一个变量长期驻扎在内存中
  2. 避免全局变量的污染
  3. 私有成员的存在

闭包可以读取到函数内部的变量,这是由于闭包后函数的堆栈不会释放,也就是说这些值始终保持在内存中。这是一个优点,也是一个缺点。

通过例子再来回顾一下

闭包的理解:
  所谓的闭包就是可以创建一个独立的环境,每个闭包里面的环境都是独立的,互不干扰。
闭包的创建:
  一个函数中嵌套另外一个函数,并且将这个函数return出去,然后将这个return出来的函数保存到了一个变量中,那么就创建了一个闭包。
var arr = [];
for(var i=0;i<2;i++){
    arr[i] = function(){
        console.log(i);
    }
}
arr[0](); //2
arr[1](); //2

  实际情况我们是要打印0,1,2,3这样的数,但是每次都是打印2,什么情况呢?虽然我们在for中给arr的每个值添加了一个匿名函数,但是 在for循环中我们并没有执行这个函数,而是在for循环以后执行的这个函数,那么自然打印出来的就是for循环完以后i的值。

var arr = [];
// for(var i=0;i<2;i++){
    var i = 2;
    arr[0] = function(){
        console.log(i);
    }
    arr[1] = function(){
        console.log(i);
    }
// }
arr[0](); //2
arr[1](); //2

  相当于这样,虽然这个函数没有执行,但是arr的i已经给执行了,因为arr不是函数啊,肯定是执行的,而你函数没有调用自然就不会执行,当函数调用的时候i已经是2了。既然如此只要我们在for循环的时候直接执行这个函数就ok。

var arr = [];
for(var i=0;i<2;i++){
    arr[i] = function(){
        console.log(i);
    }
    arr[i]();
}
//0
//1

 这样,在每一次for循环的时候就直接执行了这个函数,打印正常,但是我们这样同样有一个问题,那就是在每一次for循环的时候这个函数就已经被执行了,我们要的是我们想什么时候调用就时候调用,而不是直接在for执行中直接执行,那么显然这样做是达不到我们的目的的。

  现在我们在想想闭包的概念,闭包可以创建独立的环境,并且每一个闭包的环境是独立的,也就是说,我们可以通过闭包来保存这些不同的变量。

我们回顾一下闭包的创建方法:一个函数中嵌套另外一个函数,并且将这个函数return出去,然后将这个return出来的函数保存到了一个变量中,那么就创建了一个闭包

var arr = [];
for(var i=0;i<2;i++){
    arr[i] = a(i);
}

function a(i){
    return function(){
        console.log(i);
    }
}

arr[0](); //0
arr[1](); //1

   此时就是一个闭包,这样写有些麻烦,我们对此改进一下。

var arr = [];
for(var i=0;i<3;i++){
    arr[i] = (function(i){
        return function(){
            console.log(i);
        }
    })(i)
}

arr[0](); //0
arr[1](); //1
arr[2](); //2

还可以这样

var arr = [];
for(var i=0;i<3;i++){
    (function(i){
        arr[i] = function(){
            console.log(i);
        }
    })(i)
}

arr[0](); //0
arr[1](); //1
arr[2](); //2

 此时 arr 里面的 i 用的是闭包里面的 i,而不是 for 中的 i,因为我们说过每个闭包的环境都是独立的。

js中的回收机制

function a(){
  var num = 10;
  return function(){
    num ++;
    console.log(num);
  }
}
a()(); //11
a()(); //11

按理说第二次执行函数a的时候应该打印出12才对,但是打印的却是11。

首先来看看我们的理解

1.我们在函数a中返回了一个匿名函数,在这个匿名函数中我们num++了一下,然后我们在函数外面执行了这个匿名函数函数,(第一括号执行函数a第二个括号执行这个rutrun回去的函数)
2.现在num是11,然后我们又执行了一次这个函数,你们应该是12吧,为什么不是呢?

实际js的执行

  但是js的设计者为了让没有必要的变量保存在内存中,(我们写的任何变量都是需要内存空间的),什么叫没有必要的变量?也就是说你不在需要这个变量的时候它就会被销毁?那么你肯定会问js怎么知道那些变量是我们不需要的哪些是我们需要的。所以js为了知道哪些变量需要保存下来,哪些不需要保存下来,会进行一些判断。接下来我们就一起看看js是怎么判断的。

1.在js中定义的全局变量是不会被销毁的,因为我们随时都可能会用到这个变量,所以不能被销毁。

2.但是在函数中定义的变量就不一定了,而且由于在函数的定义的变量的生命周期在执行完这个函数就销毁的原因自然就保存不了上一次的值。

3.但是并不是说函数就真的保存不了上一次的值,因为有的时候我们确实需要上一次的值,所以js判断是否需要保存上一次变量的值的时候就会遵守这样一个规则:

如果这个函数有被外部的变量引用就不会销毁(这句话说的不够准确,下面代码会一步一步解释),否则销毁。怎么理解这句话呢?

function a(){
    var b = 0;
    return function(){
        b ++;
        console.log(b);
    }
}

var d = a();
d();//1
d();//2

  函数a被变量d引用,更准确的说是函数a里面的那个匿名函数被变量d所引用,因为变量d等于的是函数a执行完成后的值,而函数a执行完以后又因为函数a返回了那个匿名函数,所以准确的说是变量d等于匿名函数。而这个匿名函数因为使用了函数a中的变量b并且还被变量d所引用,所以就形成了一个闭包,只要这个变量d不等于null的话,那么那个变量b会一直保存到变量d中不会被销毁。

总结:

1、如果一个对象不被引用,那么这个对象就会被GC回收;
2、如果两个对象互相引用,但是没有被第3个对象所引用,那么这两个互相引用的对象也会被回收。

参考文章:
1.初识js中的闭包
2.从闭包案例中学习闭包的作用,会不会由你
3.那些年我们一起过的JS闭包,作用域,this,让我们一起划上完美的句号
4.再次讲解js中的回收机制是怎么一回事。

阅读 2.3k

于梦中的前端成长日记
于梦中的前端成长日记——记录点滴

前端菜鸟儿,请多关照!

2.1k 声望
180 粉丝
0 条评论

前端菜鸟儿,请多关照!

2.1k 声望
180 粉丝
文章目录
宣传栏