离线下载
PDF版 ePub版

开源中国2015 · 更新于 2018-11-28 11:00:42

浅谈 Javascript 嵌套函数及闭包

嵌套函数

JavaScript 允许嵌入的函数,允许函数用作数据,并且在函数词法作用域下面,可以产生与传统面向对象语言不同的惊人地方。

首先,JavaScript 的函数是通过词法来划分作用域的,而不是动态的划分作用域的,于是,函数的是在定义它们的作用域中运行,而不是在执行它们的作用域中运行,所以,当嵌套函数和它的外围函数定义在同一个词法作用域中的时候,是很容易理解的。比如下面很平淡无奇的代码:

var x = 'global';   
function f () {   
var x = 'local';   
function g() {   
alert(x);   
}   
g();   
}   
f(); // 'local'  

当 f()调用的时候,作用域链可以理解为由两部分组成,包含 f 这一调用的调用对象,然后后面是全局对象。此时查找 x 的值,会先从 f 的调用对象中查找,如果没有,再查找后面全局对象中 x。同理,g 因为是 f 的一个嵌套函数,那么,g 调用的时候,作用域链应该就是由三部分组成了,g 的调用对象,f 的调用对象,和全局对象。函数 g 是要输出 x 的值,所以会先在 g 的调用对象中查找 x 的值,g 中没有定义,接下来查找外围 f 调用对象中 x 的定义,于是找到了 x='local',那么就会输出 x,而不会继续往下查找全局对象了。 如果 f 中也没定义 x 的值,那么就会继续查找作用域链后面的全局对象,结果就是 global 了。如果全局对象中也没定义,那么自然就是 undefined。

好了,我们对作用域链有了个初步的理解,同时我们知道,闭包有两个比较常用的用途,一个是可以利用它访问到局部变量,另一个是可以把它外围作用域中的变量值存储在内存中而不在函数调用完毕后就销毁。

下面接着看一个平淡无奇的例子,或许可以帮助理解为什么闭包可以把外部变量值保存在内存中了。

function makeFunc (x) {   
return function () {return x++}   
}   
var a = [makeFunc(0), makeFunc(1), makeFunc(2)];   
alert(a[0]());   
alert(a[1]());   
alert(a[2]());  

执行结果为 0,,1,2;也没有什么特别的地方,这也是严格的词法作用域的正常表现。每次 makeFunc 调用完毕后,它的调用对象会从作用域链中移除,再没有任何对它的引用,最终通过垃圾收集而完结。说的详细一点,我们可以这样理解。

makeFunc 每次调用的时候,会为他创建一个调用对象放置到作用域链中。针对 makeFunc 这个函数而言,这个调用对象包含一个属性 x(也就是函数的参数,因为函数参数可以看做调用对象的一个属性),makeFunc 会返回一个匿名嵌套函数的引用,接下来这个匿名嵌套函数执行,又会创建一个调用对象,放置到作用域链中,匿名函数返回 x 的值,(注意:匿名函数的调用对象中是没有 x 的定义的,于是它会引用到它外围的函数 makeFunc 的调用对象,访问到 x)然后 x 加 1,至此,匿名函数执行完毕,它调用对象从作用域链中移除, 然后 makeFunc 也执行完毕,makeFunc 调用对象也被移除。由于它的调用对象中包含 x,所以 x 也随着它的销毁而销毁。不会保存下来。

以上就是函数的详细的执行过程,请仔细理解后看看下面改动的代码:

var x = 0;   
function makeFunc () {   
return function () {return x++}   
}   
var a = [makeFunc(), makeFunc(), makeFunc()];   
alert(a[0]());   
alert(a[1]());   
alert(a[2]());  

现在 x 是一个全局变量了,执行结果为 0,1,2;但是这个结果就与上面的有些不同了。下面我们还是从作用域链的方向来理解这个结果产生的原因。

同样,makeFunc 每次调用的时候会创建一个调用对象到作用域链中,由于它返回内部嵌套函数的引用,所以内部嵌套函数开始执行,又创建一个嵌套函数的调用对象到作用域链。然后返回 x 的值,注意,这里就不同了,嵌套函数的调用对象中没有 x,它外围的 makeFunc 的调用对象中也没有 x,只能接着往下查找到全局对象中,在全局对象中找到了 x 的定义,于是正常执行,返回 x 的值,x 加 1,然后嵌套函数完毕,调用对象移除,接着 makeFunc 完毕,调用对象也移除,可是因为他们的调用对象中都没有 x,他们的调用对象销毁根本不会影响到 x。于是,全局变量 x 值的改变就这样被保存下来了。

注意,上面说的访问外围的调用对象只是为了帮助理解而不严格的说法,JavaScript 不会以任何方式直接访问调用对象,但是,它定义的属性作为调用对象中作用域链的一部分,还是 “活的”。另外,如果一个外围函数包含了两个或多个嵌套函数都对全局对象有引用,那么这些嵌套函数都共享同一个全局调用对象,并且其中一个对全局对象的改变对其他的都是可见的。

好了,在 JavaScript 里,函数是将要执行的代码以及执行这些代码的作用域构成的一个综合体,广义的说,我们就可以把这种代码和作用域的综合体叫做闭包。

闭包

我们偶尔需要写一个需要通过调用来记住一个变量值的函数。于是,如果我们了解了作用域,就会知道,局部变量是很难做到的,因为函数的调用对象不能在调用后一直维持。全局变量可以做到,就如上面的例子一样,但是这样很容易造成全局变量污染。既然调用对象不能维持,那么我们不把值保存在调用对象中不就行了?!所以,下面是实现的一种方法:用函数对象自身的属性来保存。

uniqueID = function () {   
if (!arguments.callee.id) arguments.callee.id = 0;   
return arguments.callee.id ++;   
}   
alert(uniqueID()); //0   
alert(uniqueID()); //1  

如上,因为函数本身就是一个对象,所以,我们用它自身的一个属性来保存是可行的,但是这样做有一个问题,就是任何人在任何时候都可以通过 unqueID.id 强制访问到我们原本保存到的值并作出修改。这是我们不愿看到的。

所以,通常,我们使用闭包来实现这件事。如下:

_uniqueID = (function(){   
var id = 0;   
return function () {return id ++}   
})();   
alert(_uniqueID()); //0   
alert(_uniqueID()); //1  

同样,我们也用作用链域来解释下结果。注意到_uniqueID 本身就是一个匿名函数,它内部又有个匿名嵌套函数,我们直接调用的是_uniqueID(),也就是说,我们直接调用的其实是_uniqueID 内部的嵌套函数,而它本身的调用对象没有定义 id,于是引用外围的调用对象中的 id,并返回,id 加 1,执行完毕,内层嵌套函数调用对象移出作用域链。而外围的 id 并没有被销毁,于是就这样保存了下来。

有人可能会疑惑,不是说调用对象在函数执行完毕后就移除了作用域链吗,外围匿名函数 (function(){})();也是调用完毕了的,应该调用对象也没了才对。

是的,调用对象是在当前函数执行完毕后就结束引用,但是这里不要误解了上面_uniqueID() 的调用,他并不是直接调用的外围函数,而是调用的嵌套函数,嵌套函数的作用域链是包含外围函数的作用域链的。所以在它的调用对象移除作用域链的时候是能够访问到这条作用域链上其他对象的属性并改变的。

闭包本身就是个难以理解但是又非常有用的东西,希望能对有需要的人一些帮助吧。此外,资历所限,本人理解也可能有误,如发现,敬请指正。