闭包的问题

闭包属于一种特殊的作用域,称为 静态作用域。它的定义可以理解为: 父函数被销毁 的情况下,返回出的子函数的[[scope]]中仍然保留着父级的单变量对象和作用域链,因此可以继续访问到父级的变量对象,这样的函数称为闭包。

  • 闭包会产生一个很经典的问题:

    • 多个子函数的[[scope]]都是同时指向父级,是完全共享的。因此当父级的变量对象被修改时,所有子函数都受到影响。
  • 解决:

    • 变量可以通过 函数参数的形式 传入,避免使用默认的[[scope]]向上查找
    • 使用setTimeout包裹,通过第三个参数传入
    • 使用 块级作用域,让变量成为自己上下文的属性,避免共享

在ECMAScript中,函数是一等对象。这也就意味着函数可以作为参数传递给其他的函数(这种情况叫做"funargs",“functional arguments”的缩写)。获取"funargs"的函数叫做higher-order函数,函数也会在其他函数中返回。那么返回函数的函数叫做function valued functions(返回函数值的函数)

     函数参数问题的第一种形式就是向上查找的问题。当一个使用了上面提到的自由变量的函数从另一个函数返回时。为了能够获取父上下文的变量,即使父上下文已经终结。内部函数在创建时把它的父作用域链保存在[[Scope]]属性中。当这个函数被激活时,结合活动对象和[[Scope]]属性,这个函数上下文的作用域链就形成了。

1    Scope chain = Activation object + [[Scope]]

     再次注意一件主要的事情-在创建时刻-函数保存了父作用域链。因为这个被保存的作用域链在后期的函数调用中,将会用于变量查找

function foo(){
    var x=10;
    return function bar(){
        console.log(x);
    };
}
//"foo"返回一个函数,这个返回的函数使用了自由变量“x”
var returnedFunction=foo();
//全局变量“x”
var x=20;
//执行已返回的函数
returnedFunction();//10,不是20

     这种作用域叫做静态(词法)作用域。我们看到在返回的bar函数——保存的[[Scope]]属性中找到了变量x。一般的思想,在上面的例子中会有一个动态的作用域保存x的变量值为20,而不是10.然而ECMAScript不存在动态作用域。

     函数参数问题的第二种形式就是向下查找的问题。在这种情况下,父作用域可能是存在的,但可能是以一个模糊的标示符保存的。问题是:标示符的值应该使用哪 一个作用域——静态保存在函数创建时的作用域还是在执行时动态形参的作用域(调用者的作用域)?为了避免这种模棱两可的作用域并产生闭包,决定使用静态作 用域:

 1 //global x
 2 var x=10;
 3 //全局函数
 4 function foo(){
 5     console.log(x);
 6 }
 7 (function(funArg){
 8     //local x
 9     var x=20;
10     //这里是不模糊的,因为我们使用的全局变量x,静态的保存在foo函数的[[Scope]]中,而不是激活函数的调用者作用域中的x
12     funArg();//10,不是20
13 })(foo);

     我们可以得出这样的结论:在语言中的闭包必须需要静态作用域。然而,有些语言提供了静态和动态作用域以供编程者选择-使用闭包或者不使用。由于ECMAScript只有静态作用域(例如我们解决的两种函数参数问题)。ECMAScript使用函数的[[Scope]]属性完整的支持闭包,下面给出闭包的准确定义:

     闭包就是代码块(ECMAScript中,这个代码块是函数)和静态保存的父作用域的结合。因此通过这个保存的作用域链,函数可以很容易的找到自由变 量。注意,每一个函数在创建的时候都保存了[[Scope]],理论上,ECMAScript中所有的函数都是闭包。

      还需要记住的一个事实,许多函数可能有一个相同的(这真的是一个很常规的情形,比如有两个内部/全局函数)父作用域,这种情况下储存在[[Scope]]属性的变量在所有拥有同一个父作用域链的函数是共享的。一个闭包变量的改变会影响另个闭包读取的变量:

 1 function baz(){
 2     var x=1;
 3     return {
 4         foo:function foo(){return ++x;},
 5         bar:function bar(){return --x;}
 6             };
 7 }
 8 var closures=baz();
 9 console.log(closures.foo(),//2
10     closures.bar()//1)

 

这段代码可以用下图来阐释:

闭包的问题_第1张图片

 

 

      在循环中构建一些函数的迷惑也是和这个特点相关的。在构造的函数中使用循环计数器,一些编程者获得了一些意外的结果,当在函数中使用了同一个计数器时。现在清楚为什么了吧-因为所有的这些函数都有同样的[[Scope]],在这个[[Scope]]中计数器是最后一次计数值。

 

1    var data = []; 
3    for (var k = 0; k < 3; k++) {
4      data[k] = function () {
5        alert(k);
6      };
7    }    
9    data[0](); // 3, but not 0
10    data[1](); // 3, but not 1
11    data[2](); // 3, but not 2

 

这里有一些技巧来解决这些问题。一种技巧就是在作用域链中使用一个附加对象:

 

var data=[];
for(var k=0;k<3;k++){
    data[k]=(function(x){
        return function(){
            alert(x);
        };
    })(k)
}
data[0]();//0
data[1]();//1
data[2]();//2

你可能感兴趣的:(js)