深入理解JavaScript闭包

    在js中,闭包主要涉及js中其它几个特性:作用域链,垃圾(内存)回收机制,函数嵌套等;

一、作用域链(Scope Chain)

    在理解闭包前,最好要先理解作用域链。简单来说,作用域链就是函数在定义时创建的,用于寻找使用到的变量值的一个索引。而他内部的规则是将把函数自身的本地变量放在最前面,把自身的父级函数放在其次,再把高一级的函数放在后面,以此类推把全局变量放在最后面。

    当函数寻找一个变量时,js解释器会先去作用域链中去查找,一层一层查找,直至到全局变量,直到找到这个变量为止,返回这个变量,否则返回undefined。js函数内的变量值不是在编译的时候就确定的,而是等在运行时期再去寻找的。

执行环境(execution context)

    每个函数运行时都会产生一个执行环境,而这个执行环境怎么表示呢?js为每一个执行环境关联了一个变量对象。环境中定义的所有变量和函数都保存在这个对象中。全局执行环境是最外围的执行环境,全局执行环境被认为是window对象,因此所有的全局变量和函数都作为window对象的属性和方法创建的。 

二、内存回收机制

    一般来说,函数在执行开始的时候,会把其中定义的变量保存到内存中,以便后面的语句调用。等到函数执行完了,这些变量就被认为是无用的,就会将其对应的内存空间回收。等到下次再执行此函数的时候,所有变量又回到最初的状态,重新赋值使用。

但是如果这个函数内部又嵌套了另一个函数,而这个函数是有可能在外部被调用到的,并且这个函数又调用了外部的变量的话,这种内存回收机制就会出现问题

  function outer(){
     var scope = "outer";
     function inner(){
        return scope;
     }
     return inner;
  }
  var fn = outer();
  fn();

outer()内部返回了一个inner函数,当调用outer时,inner函数的作用域链就已经被初始化了(复制父函数的作用域链,再在前端插入自己的活动对象)

    在外部函数返回后,又调用了内部函数,那么内部函数就无法读取到他所需要的外部函数变量的值了,所以js解释器在遇到函数定义的时候,会自动把函数和他可能使用的变量(本地变量和父级函数和祖先级函数的变量)一起保存起来,也就是构建一个闭包。

三、闭包

    闭包简单来说就是能读懂其他函数内部变量的函数。

闭包有两个作用: 
第一个就是可以读取自身函数外部的变量(沿着作用域链寻找) 

第二个就是让这些外部变量始终保存在内存中 

关于第二个:

     function outer(){
         var result = new Array();
         for(var i = 0; i < 2; i++){//注:i是outer()的局部变量
            result[i] = function(){
               return i;
            }
         }
         return result;//返回一个函数对象数组
         //这个时候会初始化result.length个关于内部函数的作用域链
      }
      var fn = outer();
      console.log(fn[0]());//result:2
      console.log(fn[1]());//result:2

由于result函数并未定义i变量,遂沿着作用域链查找,而这个变量i是父函数执行结束后将最终值保存在内存里的结果。 所以结果为2。由此也可以得出,js函数内的变量值不是在编译的时候就确定的,而是等在运行时期再去寻找的。解决办法使用自执行函数

      function outer(){
         var result = new Array();
         for(var i = 0; i < 2; i++){
            result[i] = function(){
               return i;
            }();//预先执行函数写法
         }
         return result;
      }

还可以使用ES6 let使函数变成块级作用域

关于闭包还有this指向问题,this对象是在运行时基于函数的执行环境绑定的。

在全局函数中,this指向window,当函数被当做某个对象调用时,this等于这个对象。而匿名函数具有全局性,因此this对象通常指向window

-----------------------------------------------------------------------------------

在网上看到了一个比较有意思的闭包题和大家分享一下:

function fun(n,o) {
  console.log(o)
  return {
    fun:function(m){
      return fun(m,n);
    }
  };
}
var a = fun(0);  a.fun(1);  a.fun(2);  a.fun(3);//undefined,0,0,0
var b = fun(0).fun(1).fun(2).fun(3);//undefined,0,1,2
var c = fun(0).fun(1);  c.fun(2);  c.fun(3);//undefined,0,1,1
//问:三行a,b,c的输出分别是什么?

1、第一行a

var a = fun(0);  a.fun(1);  a.fun(2);  a.fun(3);

可以得知,第一个fun(0)是在调用第一层fun函数。第二个fun(1)是在调用前一个fun的返回值的fun函数,所以:

第后面几个fun(1),fun(2),fun(3),函数都是在调用第二层fun函数

遂:

在第一次调用fun(0)时,o为undefined;

第二次调用fun(1)时m为1,此时fun闭包了外层函数的n,也就是第一次调用的n=0,即m=1,n=0,并在内部调用第一层fun函数fun(1,0);所以o为0;

第三次调用fun(2)时m为2,但依然是调用a.fun,所以还是闭包了第一次调用时的n,所以内部调用第一层的fun(2,0);所以o为0

第四次同理;

即:最终答案为undefined,0,0,0

 

2、第二行b

var b = fun(0).fun(1).fun(2).fun(3);//undefined,?,?,?

先从fun(0)开始看,肯定是调用的第一层fun函数;而他的返回值是一个对象,所以第二个fun(1)调用的是第二层fun函数,后面几个也是调用的第二层fun函数。

遂:

在第一次调用第一层fun(0)时,o为undefined;

第二次调用 .fun(1)时m为1,此时fun闭包了外层函数的n,也就是第一次调用的n=0,即m=1,n=0,并在内部调用第一层fun函数fun(1,0);所以o为0;

第三次调用 .fun(2)时m为2,此时当前的fun函数不是第一次执行的返回对象,而是第二次执行的返回对象。而在第二次执行第一层fun函数时时(1,0)所以n=1,o=0,返回时闭包了第二次的n,遂在第三次调用第三层fun函数时m=2,n=1,即调用第一层fun函数fun(2,1),所以o为1;

第四次调用 .fun(3)时m为3,闭包了第三次调用的n,同理,最终调用第一层fun函数为fun(3,2);所以o为2;

即最终答案:undefined,0,1,2

 

3、第三行c

var c = fun(0).fun(1);  c.fun(2);  c.fun(3);//undefined,?,?,?

根据前面两个例子,可以得知:

fun(0)为执行第一层fun函数,.fun(1)执行的是fun(0)返回的第二层fun函数,这里语句结束,遂c存放的是fun(1)的返回值,而不是fun(0)的返回值,所以c中闭包的也是fun(1)第二次执行的n的值。c.fun(2)执行的是fun(1)返回的第二层fun函数,c.fun(3)执行的是fun(1)返回的第二层fun函数。

遂:

在第一次调用第一层fun(0)时,o为undefined;

第二次调用 .fun(1)时m为1,此时fun闭包了外层函数的n,也就是第一次调用的n=0,即m=1,n=0,并在内部调用第一层fun函数fun(1,0);所以o为0;

第三次调用 .fun(2)时m为2,此时fun闭包的是第二次调用的n=1,即m=2,n=1,并在内部调用第一层fun函数fun(2,1);所以o为1;

第四次.fun(3)时同理,但依然是调用的第二次的返回值,遂最终调用第一层fun函数fun(3,1),所以o还为1



猜你喜欢

转载自blog.csdn.net/chjunjun/article/details/80660579