日期:2014-05-16  浏览次数:20436 次

学习面向对象javascript(三)闭包

在这篇文章中,我们会了解到一些闭包的知识。在了解闭包之前,我们先了解一下 javascript 作用域的概念。

作用域链

我们知道,在javascript中,是以function作为域的界限,而不是像其他很多语言一样,是以大括号作为边界的。一个定义在函数内部的变量,在函数外部是访问不了的,但是在代码块里面定义的变量,比如一个for循环中定义的变量,在代码块外面是可以被访问到的。

var a = 1; 
function f(){
    var b = 1;
    return a;
}
f();  // 1
b;  // b is not defined

变量a是一个全局域的,然而b则是在函数f()内部作用域的,所以:
在f()中,a和b都可以被访问到。
在f()外,a可以被访问到,但是b不能。
如果你在f()内部定义一个function n(),那么n()将有访问他自己域和父函数f()域的权限。这就是作用域链,并且链可以任意深。

词法作用域

在javascript中,函数具有词法作用域。意思是,在函数被定义的时候创建他的作用域,而不是在被调用时才创建。例如:

function f1(){
    var a = 1; 
    f2();
}
function f2(){
    return a;
}
f1();  // a is not defined

在函数f1()内部调用了函数f2()。因为变量a也在f1()内部,所以你的第一反应可能会觉得在f2()中可以访问a,但是并不是这样的。在f2被定义的时候,并没有a这个变量。f2()就像f1()一样,只能访问它自己的作用域和全局作用域。他们并没有共享出他们的本地作用域。

当一个函数被定义时,它会记住它自己的环境、作用域链。这并不意味着他能够知道他作用域里的每一个变量。相反的,我们可以添加、删除、更新函数作用域里面的变量,并且函数可以得到最新的变量的状态。如何在上面的例子中加一个全局变量a,那么f2()就可以访问得到。

var a = 5;
f1();  // 5
a = 55;
f1();  // 55
delete a;  // true
f1();  // a is not defined

这种特性给了javascript很大的灵活性。你可以添加和删除变量,并且重新添加他们之后,一样可以继续运行。下面我们试一下把f2()删除以后重新定义他。

delete f2;  // true
f1();  // f2 is not defined
var f2 = function(){
    return a * 2;
}
var a = 5; 
f1();  // 10

用闭包切断链

首先,我们用一些图来描述一下闭包。
这是一个全局作用域,就像一个宇宙,包含了所有东西。

它包含了变量a和函数F。

函数有它自己的私有空间来存储它内部的变量和函数,在某些情况下,会像下面的图一样。

如果你在a点,那么你在全局空间中。如果你在函数F中的b点,那么你即在全局空间中,又在函数F的空间中。如果你在函数N中的c点,那么你即在全局空间中,又在函数F的空间中,还在函数N的空间中。但是反过来,b不在函数N的空间里,a不在函数F和函数N的空间里。因此,在a点,不能访问b和c的东西,在b点,不能访问c点的东西。但是在c点可以访问b和a的东西,b点可以访问a点的东西。但是,当函数N从F里面出来到全局空间的时候,这就形成了闭包。

这时,N已经像a一样在全局空间中,并且函数N记住了他自己的状态,N仍然可以访问F的空间中的b。
如何让N像上面这样切断链呢?下面来看一下具体的代码是怎么实现的。
第一种可以把N声明成为一个全局的函数。

function f(){
  var b = "b"; 
  n = function(){
    return b;
  }
}

在函数中定义了一个变量n,由于没有加var,所以它成为了一个全局函数,但是为了养成好的习惯,建议还是在全局加上一个变量声明。

var n;
function f(){
  var b = "b"; 
  n = function(){
    return b;
  }
}
f();

当我们执行了函数f以后,函数n有了他的方法体,由于n在函数f里面,所以可以访问到f的变量b。就是出了函数f的作用域成为一个全局函数,它一样可以访问到f的变量b。

n();  // "b"

第二种可以在F中,把N返回到全局空间中。
仍然使用上面的函数,但是这次的函数和前面的有点区别。

function f(){
  var b = "b"; 
  return function(){
    return b;
  }
}

?函数包含一个局部变量b,因此,从全局是无法访问到的。

b; b is not defined

但是函数返回了另一个函数,我们可以把他想象成上面的N。这个新的函数可以访问到他的私有空间,f的空间和全局的空间。所以,他可以访问到b。由于在全局区域可以访问函数f,因此,可以把他的返回值重新赋值给一个新的全局变量。这就成为一个可以访问函数f私有空间的一个全局函数。

var n = f();
n();  // "b"

因此,你可以认为闭包就是创建一个能够在其父函数return以后仍然在父作用域中保持关联的一个函数。
当你传入参数到一个函数,你也可以创建一个函数来return他的父函数的参数。

function f(arg) {
  var n = function(){
    return arg;
  }; 
  arg++; 
  return n;
}
var m = f(123);
m();  // 124

循环中的闭包

首先来看一下这个程序。

function f() {
  var a = [];
  var i; 
  for(i = 0; i < 3; i++) {
    a[i] = function(){
      return i;
    }
  } 
  return a;
}

?运行此函数以后赋值给数组a

var a = f();

?现在,我们运行数组a中的各个元素。

for(var j=0;j<a.length;j++){
    alert(a[j]());
}

可以发现,浏览器弹出3次3。其实,我们在创建了3个指向局部变量i的闭包。闭包并没有记住i的值,而是指向i并返回它当前的值。所以在循环之后,i的值是3。所以就会有我们看到的结果。
要是我们想得到结果是弹出0,1,2的话,应该怎么改呢?其实我们可以利用自执行函数来实现。