代码说

code is poetry

代码说    
碎碎念:下雨天留客天留人不留  换一换

嵌套函数声明与闭包

作者:coderzheng 发布于:2015-12-4 1:39 Friday 分类:javascript  阅读模式

本文内容节选自《JavaScript编程全解》一书。

6.7.1 对闭包的初步认识

为了让没有接触过闭包(closure)这个词的人也能理解其含义,这里先撇开语言的严密性,试着从表面上说明。请看下面的代码示例。
js> var fn=f(); // 将函数f的返回值赋值给变量fn
js> fn(); // 对fn的函数调用
1
js> fn();
2
js> fn();
3
函数f的内容将在之后说明, 在此先不必在意。函数f的返回值是函数(对象的引用), 这里将其赋值给了变量fn。在调用函数fn时,
其输出结果每次都会增加1。可以以Java的方式对其内部实现作如下推测:该对象具有一个私有域作为内部计数器。当方法被
调用时,内部计数器将会增加。不过从外表上只能看到函数调用这一行为。
函数f的内部结构如下所示。更为详细的内容将在下一节说明。
function f() {
     var cnt=0;
     return function() {return ++cnt;}
}
从表面上来看,闭包是一种具有状态的函数。如果只是希望简单地使用闭包,这样理解就可以了。或者也可以将闭包的特征理解为,
其相关的局部变量在函数调用结束之后将会继续存在。
例如,在上面的例子中,函数内的局部变量cnt在函数f的调用之后依然有效。

6.7.2 闭包的原理

嵌套的函数声明
闭包的前提条件是需要在函数声明的内部声明另一个函数(即嵌套的函数声明)。下面是一个嵌套函数声明的简单例子。在下面的例子中使用的是函数声明语句,若使用匿名函数表达式也是一样的结果。
js> function f() { // 该函数具有嵌套函数声明
     function g() {
     print('g is called');
     }
     g();
}
// 对函数进行调用
js> f();
g is called
在函数f中包含函数g的声明以及调用语句。在调用函数f时,就间接地调用了函数g。这一行为本身并没有难以理解之处,不过为了能更好地理解该过程,在此对其内部机制进行说明。
在最外层代码中对函数f的声明,具有Function对象的生成以及通过变量f来引用该Function对象这两层含义。同时,变量f是全局对象的属性。之后将不再使用变量这个词,而改用属性这一术语。
在JavaScript中,调用函数时将会隐式地生成Call对象。方便起见,我们将调用函数f时生成的Call对象称作Call-f对象。在函数调用完成之后,Call对象将被销毁。
函数f内的函数g的声明将会独立生成Call对象,因此在调用函数g时将会隐式地生成另一个Call对象。方便起见,我们将该Call对象称作Call-g对象。
离开函数g之后,Call-g对象将被自动销毁。类似地,离开函数f之后,Call-f对象也将自动销毁。此时,由于属性g将与Call-g对象一起被销毁,所以由g所引用的Function对象将会失去其引用,而最终(通过垃圾回收机制)被销毁。
嵌套函数与作用域
首先我们像下面这样对代码作少许修改。
function f(){
     var n = 123;
     function g() {
          print('n is ' + n);
          print('g is called');
     }
     g();
}
// 对函数进行调用
js> f();
n is 123
g is called
这一结果和代码给人的直观感受也可以说是一致的。根据其形式,可以像下面这样来理解其作用域,即在内层进行声明的函数g可以访问外层的函数f的局部变量(在这里指变量n)。5.4节提到过,函数内的变量名的查找是按照先Call对象的属性再全局对象的属性这样的顺序进行的。对于嵌套声明的函数,内部的函数将会首先查找被调用时所生成的Call对象的属性,之后再查找外层函数的Call对象的属性。
嵌套函数的返回
我们进一步对之前的代码作如下修改。
function f() {
     var n = 123;
     function g() {
          print('n is '+n);
          print('g is clalled');
     }
     return g; // 在内部返回已被声明的函数(未对函数进行调用)
}
// 对函数进行调用
js> f();
function g() {
     print('n is ' + n);
     print('g is called');
}
由于return g语句,函数f将会返回一个Function对象(的引用)。调用函数f的结果是一个Function对象。这时,虽然会生成与函数f相对应的Call对象(Call-f对象)(并在离开函数f后被销毁),但由于不会调用函数g,所以此时还不会生成与之对应的Call对象(Call-g对象),请对此加以注意。
闭包
现尝试将函数f的返回值赋值给另一个变量。虽然也可以直接调用函数而不赋值,不过为了使整个过程更易于理解,还是采用赋值操作。变量名为g2,即通过g2来调用函数。
js> var g2 = f(); // 将返回的函数赋值给变量
js> g2(); // 调用函数(函数f内的函数g)
n is 123
g is called
这一结果说明可以从函数f的外部调用函数g。进一步说,这说明函数f的局部变量n在函数f被调用之后依然存在。在表面上看,这与Java等其他的过程式程序设计语言中的规则是相反的(一般来说,离开函数之后其局部变量就会无效)。
函数f被调用时生成的Call对象(Call-f对象)的属性g所引用的Function对象(已经反复强调,对象本身是没有名称的),为g2所引用。只要引用的变量还存在,对象就不会成为垃圾回收机制的目标。因此,只要名称g2还存在,Function对象就会存在。该Function对象具有Call-f对象的引用(被用于作用域链)。因此,如果被名称g2所引用的这个Function对象还存在,Call-f对象也会继续存在。这就是为什么在离开了函数f之后局部变量n依然存在的原因。图6.4对以上关系进行了整理。
图6.4 闭包

像下面这样调用函数f两次之后,g2与g3将分别引用两个不同的Function对象。之后,这两个Function对象将会引用各自不同的Call-f对象,因为Call对象会在每次调用函数调用被独立生成。
js> var g2 = f();
js> var g3 = f();
为了更清晰地展示g2与g3所引用的函数在被调用时的不同, 代码清单6.9使用了一种比较有技巧性的做法。由于这两个Call-f对象是不相同的,因此可以分别通过g2与g3对各自对象的属性(从函数f的角度来看即全局变量n)访问。
代码清单6.9 闭包
function f(arg) {
     var n = 123 + Number(arg);
     function g() {
          print('n is ' + n);
          print('g is called');
     }
     return g;
}
// 对代码清单6.9的调用
js> var g2 = f(2);
js> var g3 = f(3);
js> g2();
n is 125
g is called
js> var n = 7; // 对全局变量n进行定义, 但这对结果没有影响
js> g3();
n is 126
g is called
闭包与执行环境
现不考虑内部执行过程,而是以抽象的方式重新对g2与g3的调用结果不同的这一想象。这意味着可以通过同一段代码生成具有不同状态的函数。这就是所谓的闭包。说得专业一点,闭包指的是一种特殊的函数,这种函数会在被调用时保持当时的变量名查找的执行环境。
由于本书并非理论专著,所以仅将闭包解释为具有状态的函数。这种较为通俗的说明会更易于理解。不过,闭包仅仅是保持了变量名的查找的状态,而并没有保持对象的所有状态,对此请加以区分。也就是说,闭包虽然会保持(在嵌套外层进行函数调用时被隐式地生成的)Call对象,但无法保持Call对象的属性所引用的之前的对象的状态。由于这一原因而产生的一些需要注意的地方,将在下一节中进行说明。
像下面这样,在匿名函数表达式中直接使用return语句的写法很常见,所以请记住这种闭包的习惯用法。
function f(arg) {
     var n = 123 + Number(arg);
     return function() {
          print('n is ' + n);
          print('g is called');
     }
}

6.7.3 闭包中需要注意的地方

如果在函数f内有两个函数声明,这两者将会引用同一个Call-f对象(代码清单6.10)。这是在使用JavaScript的闭包时容易出错的地方。
代码清单6.10 闭包中需要注意的地方
function f(arg) {
     var n = 123 + Number(arg);
     function g() {print('n is ' + n);print('g is called');}
     n++;
     function gg() {print('n is '+n);print('gg is called');}
     return [g,gg];
}

// 对代码清单6.10的调用
js> var g_and_gg = f(1);
js> g_and_gg[0](); // 对闭包g的调用
n is 125
g is called

js> g_and_gg[1](); // 对闭包gg的调用
n is 125
gg is called
函数g与函数gg保持了各自含有局部变量n的执行环境。由于声明函数g时的n值与声明函数gg时的值是不同的,因此闭包g与闭包gg貌似将会表示各自不同的n值。但实际上两者将会表示相同的值。这是因为两者引用了同一个Call对象(Call-f对象)。

6.7.4 防范命名空间的污染

模块
接下来,我们来介绍几种运用闭包的实际示例。
在JavaScript中,在最外层代码(函数之外)所写的名称(变量名与函数名)具有全局作用域,即所谓的全局变量与全局函数。如果没有像本书第6部分所介绍CommonJS那样另外提供的模块功能,JavaScript的程序代码即使在被分割为多个源文件之后,也能相互访问全局名称。在JavaScript的语言规范中不存在所谓模块的语言功能。
因此,对于客户端JavaScript,如果在一个HTML文件中对多个JavaScript文件进行读取,则它们相互之间的全局名称会发生冲突。也就是说,在某个文件中使用的名称无法同时在另一个文件中使用。即使在独立开发中这也很不方便,在使用他人开发的库之类时就更加麻烦了。
此外,全局变量还降低了代码的可维护性。不过也不能就简单下定论说问题只是由全局变量造成的。这就如同在Java这种语言规范不支持全局变量的语言中,同样可以很容易地创建出和全局变量功能类似的变量。也就是说,不应该只是一味地减少全局变量的使用,而应该形成一种尽可能避免使用较广的作用域的意识。对于较广的作用域,其问题在与修改了某处代码之后,会难以确定该修改的影响范围,因此代码的可维护性会变差。
避免使用全局变量
从形式上来看,在JavaScript中减少全局变量的数量的方法是很简单的。首先我们按照下面的代码这样预设一下全局函数与全局变量。
// 全局函数
function sum(a, b) {
     return Number(a) + Number(b);
}
// 全局变量
var position = {x:2, y:3};
在像下面这样,借助通过对象字面量生成的对象的属性,将名称封入对象的内部。于是从形式上来看,全局变量就减少了。
// 封入对象字面量中
var MyModule = {
     sum: function(a,b) {
          return Number(a) + Number(b);
     },
     position:{x:2,y:3}
};
// 对其进行调用
js>MyModule.sum(3,3);
6
js>print(MyModule.position.x);
2
上面的例子使用了对象字面量,不过也可以像下面这样不使用对象字面量。
var MyModule = {}; // 也可以通过new表达式生成
MyModule.sum = function(a,b) {return Number(a)+Number(b);};
MyModule.position = {x:2,y:3};
在这个例子中,方便起见,我们就MyModule称为模块名。如果完全采用这种方式,对于1个文件来说,只需要1个模块名就能消减全局变量的数量。当然,模块名称之间仍然可能产生冲突。不过这一问题在其他的程序设计语言中也是一个无法避免的问题。
通过这种将名称封入对象之中的方法,可以避免名称冲突的问题。但是这并没有解决全局名称的另一个问题,也就是作用域过广的问题。在上面的代码中,通过MyModule.position.x这样一个较长的名称,就可以从代码的任意一处访问该变量。
通过闭包实现信息隐藏
JavaScript语言并没有提供可用于信息隐藏的语法功能,不过灵活运用闭包之后,就能够是的名称无法从外部被访问。代码清单6.11是个具体例子。不过代码清单6.11的代码仅仅是为了对此说明,并没有实际意义。
代码清单6.11 使用了闭包的模块
// 在此调用匿名函数
// 由于匿名函数的返回值是一个函数, 所以变量sum是一个函数
var sum = (function(){
     // 无法从函数外部访问该名称
     // 实际上,这变成了一个私有变量
     // 一般来说,在函数被调用之后该名称就将无法再被访问
     // 不过由于是在被返回的匿名函数中,所以仍可以继续被使用
     var position = {x:2,y:3};
     
     // 同样是一个从函数外部无法被访问的私有变量
     // 将其命名为sum也可以。不过为了避免混淆,这里采用其他名称
     function sum_internal(a, b) {
          return Number(a) + Number(b);
     }
     // 只不过是为了使用上面的两个名称而随意设计的返回值
     return function(a,b) {
          print('x = ', position.x);
          return sum_internal(a,b);
     }
})();

// 调用代码清单6.11
js> sum(3, 4);
x = 2
7
代码清单6.11可以被抽象为下面这种形式的代码。在利用函数作用域可以封装名称,以及闭包可以使用名称在函数调用结束后依然存在这两个特性后,信息隐藏得以实现。
(function(){函数体})(); 像上面这样,当场调用匿名函数的代码看起来或许有些奇怪。一般的做法是先在某处声明函数,之后在需要时调用。不过这种做法就是JavaScript的一种习惯用法,还请加以掌握。
代码清单6.11的匿名函数的返回值是一个函数,不过即使返回值不是函数,也同样能采用这一方法。例如,可以像代码清单6.12这样返回一个对象字面量以实现信息隐藏的功能。
// 代码清单6.12 将代码清单6.11的返回值更改为对象字面量
var obj = (function(){
     // 从函数外部无法访问该名称
     // 实际上,这是一个私有变量
     var position = {x:2, y:3};
     
     // 这同样是一个无法从函数外部进行访问的私有函数
     function sum_internal(a, b) {
          return Number(a) + Number(b); 
     }

     // 只不过是为了使用上面的两个名称而随意设计的返回值
     return {
          sum:function(a,b) {return sum_interval(a,b);},
          x:position.x
     };
})();

// 调用代码清单6.12
js> obj.sum(3,4);
7
js>print(obj.x);
2
本节使用的方法,也能够直接被用于下一节中使用了闭包的类中。

6.7.5 闭包与类

5.7.3节已经对JavaScript的类的定义作了介绍。对于构造函数的类来说,存在以下问题。
无法对属性值进行访问控制(private或public等)
JavaScript没有与访问控制有关的语法结构。不过只要利用函数作用域与闭包,就可以实现访问控制。按照本节介绍的方法,就能够生成无法变更状态的不可变对象。相关内容可参见5.12节。
基本的思路就是利用上一节中提到的模块。在上一节中,模块的函数在被声明之后直接就对其调用,而使用了闭包的类则能够在生成实例调用。即使如此,这种做法在形式上仍然只是单纯的函数声明。下面是一个通过闭包来对类进行定义的例子(代码清单6.13),这个类与5.7.3节中的代码清单5.9的MyClass是等价的。
代码清单6.13 代码清单5.9中的MyClass的闭包实现版本
// 用于实例生成的函数
function myclass(x,y) {
     return {show:function(){print(x,y);}};
}
// 通过代码清单6.13生成实例
js> var obj = myclass(3,2);
js> obj.show();
3 2
这里再举一个具体的例子,一个实现了计数器功能的类(代码清单6.14)。请根据注释来理解这种通过闭包实现的类的结构。
代码清单6.14 实现了计数器的功能的类
function counter_class(init) { // 初始值可以通过参数设定
     var cnt = init || 0; // 设置默认参数的习惯做法(参见5.5节)
     // 如有必要,可在此声明私有变量与私有函数
     return {
          // 公有方法
          show:function(){print(cnt);},
          up:function(){cnt++;return this;} , // return this在使用方法链时很方便
          down:function(){cnt--;return this;}  
     };
}

// 使用代码清单6.14的示例
js> var counter1 = counter_class();
js> counter1.show();
0
js> counter1.up();
js> counter.show();
1
js> var counter2 = counter_class(10);
js> counter2.up().up().show(); // 方法链
13
专栏
表达式与闭包
JavaScript有一种自带的增强功能,称为支持函数型程序设计的表达式闭包(Expression closure)。
从语法结构上来看,表达式闭包是函数声明表达式的一种省略形式。可以像下面这样省略只有return的函数声明表达式中的return与{}。
var sum = function(a,b) {return Number(a)+Number(b);}
可以省略为
var sum = function(a,b) Number(a)+Number(b);





Over.

标签: javascript js闭包

你可以发表评论、引用到你的网站或博客,或通过RSS 2.0订阅这个博客的所有文章。
上一篇: 第十话:爆裂!五连发夹弯  |  下一篇:中奖概率问题