虽然 JavaScript 天生就是一副随随便便的样子,但是随着浏览器能够完成的事情越来越多,这门语言也也越来越经常地摆出正襟危坐的架势。在复杂的逻辑下, JavaScript 需要被模块化,模块需要封装起来,只留下供外界调用的接口。闭包是 JavaScript 中实现模块封装的关键,也是很多初学者难以理解的要点。最初,我也陷入迷惑之中。现在,我自信对这个概念已经有了比较深入的理解。为了便于理解,文中试图封装一个比较简单的对象。
我们试图在页面上维护一个计数器对象 ticker ,这个对象维护一个数值 n 。随着用户的操作,我们可以增加一次计数(将数值 n 加上 1 ),但不能减少 n 或直接改变 n 。而且,我们需要时不时查询这个数值。
门户大开的 JSON 风格模块化
一种门户大开的方式是:
复制代码 代码如下:
var ticker = {
n:0,
tick:function(){
this.n++;
},
};
这种方式书写自然,而且确实有效,我们需要增加一次计数时,就调用 ticker.tick() 方法,需要查询次数时,就访问 ticker.n 变量。但是其缺点也是显而易见的:模块的使用者被允许自由地改变 n ,比如调用 ticker.n-- 或者 ticker.n=-1 。我们并没有对 ticker 进行封装, n 和 tick() 看上去是 ticker 的“成员”,但是它们的可访问性和 ticker 一样,都是全局性的(如果 ticker 是全局变量的话)。在封装性上,这种模块化的方式比下面这种更加可笑的方式,只好那么一点点(虽然对有些简单的应用来说,这一点点也足够了)。
复制代码 代码如下:
var ticker = {};
var tickerN = 0;
var tickerTick = function(){
tickerN++;
}
tickerTick();
值得注意的是,在 tick() 中,我访问的是 this.n ——这并不是因为 n 是 ticker 的成员,而是因为调用 tick() 的是 ticker 。事实上这里写成 ticker.n 会更好,因为如果调用 tick() 的不是 ticker ,而是其他什么东西,比如:
复制代码 代码如下:
var func = ticker.tick;
func();
这时,调用 tick() 的其实是 window ,而函数执行时会试图访问 window.n 而出错。
事实上,这种“门户大开”型的模块化方式,往往用来组织 JSON 风格的数据,而不是程序。比如,我们可以将下面这个 JSON 对象传给 ticker 的某个函数,来确定 ticker 从 100 开始计数,每次递进 2 。
复制代码 代码如下:
var config = {
nStart:100,
step:2
}
作用域链和闭包
来看下面的代码,注意我们已经实现了传入 config 对 ticker 进行自定义。
复制代码 代码如下:
function ticker(config){
var n = config.nStart;
function tick(){
n += config.step;
}
}
console.log(ticker.n); // ->undefined
你也许会疑惑,怎么 ticker 从对象变成了函数了?这是因为 JavaScript 中只有函数具有作用域,从函数体外无法访问函数内部的变量。 ticker() 外访问 ticker.n 获得 undefined ,而 tick() 内访问 n 却没有问题。从 tick() 到 ticker() 再到全局,这就是 JavaScript 中的“作用域链”。
可是还有问题,那就是——怎么调用 tick() ? ticker() 的作用域将 tick() 也掩盖了起来。解决方法有两种:
•1)将需要调用方法作为返回值,正如我们将递增 n 的方法作为 ticker() 的返回值;
•2)设定外层作用域的变量,正如我们在 ticker() 中设置 getN 。
复制代码 代码如下:
var getN;
function ticker(config){
var n = config.nStart;
getN = function(){
return n;
};
return function(){
n += config.step;
};
}
var tick = ticker({nStart:100,step:2});
tick();
console.log(getN()); // ->102
请看,这时,变量 n 就处在“闭包”之中,在 ticker() 外部无法直接访问它,但是却可以通过两个方法来观察或操纵它。
在本节第一段代码中, ticker() 方法执行之后, n 和 tick() 就被销毁了,直到下一次调用该函数时再创建;但是在第二段代码中, ticker() 执行之后, n 不会被销毁,因为 tick() 和 getN() 可能访问它或改变它,浏览器会负责维持n。我对“闭包”的理解就是:用以保证 n 这种处在函数作用域内,函数执行结束后仍需维持,可能被通过其他方式访问的变量 不被销毁的机制。
可是,我还是觉得不大对劲?如果我需要维持两个具有相同功能的对象 ticker1 和 ticker2 ,那该怎么办? ticker() 只有一个,总不能再写一遍吧?