2. 模仿块级作用域
前面我们说到,JavaScript中没有块级作用域的概念。这意味着在块语句中定义的变量,实际上是在包含函数中而非语句中创建的。下面是一个例子:
function outputNumber(count){
for (var i=0; i<count; i++){
alert(i);
}
alert(i);
}
上面的代码是完全可执行的,因为在for语句中定义的变量i是出于函数作用域的,这也说明了JavaScript中没有块级作用域。另外,JavaScript从来不会在意是否多次声明了同一个变量,如果遇到这种情况(例如在for循环结束后,使用var i;
再次定义一个i变量,也不会改变i的值,除非你在定义时显示的初始化了),它只会直接忽略后续的声明。
我们可以通过匿名函数来模拟块级作用域。下面是一个例子:
function outputNumber(count){
(function(){
for (var i=0; i<count; i++){
alert(i);
}
})();
alert(i); //error!
}
在这个重写后的 outputNumbers()函数中,我们在 for 循环外部插入了一个私有作用域。在匿名 函数中定义的任何变量,都会在执行结束时被销毁。因此,变量 i 只能在循环中使用,使用后即被销毁。 而在私有作用域中能够访问变量 count,是因为这个匿名函数是一个闭包,它能够访问包含作用域中的 所有变量。
3. 私有变量
严格来说,JavaScript中没有私有成员的概念;所有对象属性都是公有的。不过,倒是有一个私有变量的概念:任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数外部访问这些变量。私有变量包括函数参数,局部变量和函数内部定义的其他函数。下面是一个例子:
function MyObject(){
//私有变量
var privateVariable = 10;
//私有函数
function privateFunction(){
return false;
}
this.publicMethod = function(){
privateVariable ++;
return privateFunction();
}
}
我们把有权访问私有变量和私有函数的公有方法称为特权方法(privileged method)。上面的例子定义了一个MyObject引用类型,在构造函数内部定义了所有私有变量和函数。然后,有定义了一个名为publicMethod()的特权方法。能够在构造函数中定义特权方法,是因为特权方法作为闭包有权访问在构造函数中定义的 所有变量和函数。在创建 MyObject 的实例后,除了使用 publicMethod()这一个途径外,没有任何办法可以直接访问 privateVariable 和 privateFunction()。
函数中定义特权方法也有一个缺点,那就是你必须使用构造函数模式来达到这个目的。第 6章曾经讨论 过,构造函数模式的缺点是针对每个实例都会创建同样一组新方法,而使用静态私有变量来实现特权方 法就可以避免这个问题。
3.1 静态私有变量
通过在私有作用域中定义私有变量或函数,同样也可以创建特权方法。下面是一个例子:
(function(){
//私有变量
var privateVariable = 10;
//私有函数
function privateFunction(){
return false;
}
//构造函数
MyObject = function(){
};
//特权方法
MyObject.prototype.publicMethod = function(){
privateVariable ++;
return privateFunction();
}
})();
这个模式创建了一个私有作用域,并在其中封装了一个构造函数及相应的方法。公有方法是在原型上定义的, 这一点体现了典型的原型模式。需要注意的是,这个模式在定义构造函数时并没有使用函数声明,而是 使用了函数表达式。函数声明只能创建局部函数,但那并不是我们想要的。出于同样的原因,我们也没 有在声明 MyObject 时使用 var 关键字。记住:初始化未经声明的变量,总是会创建一个全局变量。 因此,MyObject 就成了一个全局变量,能够在私有作用域之外被访问到。
但也要知道,在严格模式下给未经声明的变量赋值会导致错误。
以这种方式创建静态私有变量会因为使用原型模式而增进代码复用(函数是在对象实例间共享的),但缺点是每个实例都没有自己的私有变量(属性也在实例间共享,因此属性全部都是静态属性)。到底是使用实例变量,还是静态私有变量,终还是要视你的具体需求而定。
3.2 模块模式
前面介绍的模式都是用于为自定义类型创建私有变量和特权方法的,而模块模式主要用于为单例创建私有变量和特权方法。按照惯例,JavaScript是通过字面量来创建单例对象的:
var singleton = {
name: "Ivan",
getName: function(){
return this.name;
}
}
使用字面量方式创建的单例对象,其属性都是公开的。因此就有了模块模式:
var singleton = function(){
//私有变量
var privateVariable = 10;
//私有函数
function privateFunction(){
return false;
}
return {
publicMethod: function(){
privateVariable++;
return privateFunction();
}
}
}();
模块模式使用一个匿名函数返回一个对象。字面量对象中定义了公开属性和特权方法。这种模式在需要对单例进行某些初始化,同时又需要维护其私有 变量时是非常有用的,例如:
var application = function(){
//私有变量和函数
var components = new Array();
//初始化
components.push(new BaseComponent());
//公共
return {
getComponentCount : function(){
return components.length;
},
registerComponent : function(component){
if (typeof component == "object"){
components.push(component);
}
}
};
}();
有人进一步改进了模块模式,即在返回对象之前加入对其增强的代码。这种增强的模块模式适合那 些单例必须是某种类型的实例,同时还必须添加某些属性和(或)方法对其加以增强的情况。来看下面 的例子:
var application = function(){
//私有变量和函数
var components = new Array();
//初始化
components.push(new BaseComponent());
//创建application 的一个局部副本
var app = new BaseComponent();
//公共接口
app.getComponentCount = function(){
return components.length;
};
app.registerComponent = function(component){
if (typeof component == "object"){
components.push(component);
}
};
//返回这个副本
return app;
}();
在这个重写后的应用程序(application)单例中,首先也是像前面例子中一样定义了私有变量。主 要的不同之处在于命名变量 app 的创建过程,因为它必须是 BaseComponent 的实例。这个实例实际上 是 application 对象的局部变量版。此后,我们又为 app 对象添加了能够访问私有变量的公有方法。 后一步是返回 app 对象,结果仍然是将它赋值给全局变量 application。
4. 总结
在JavaScript中,函数表达式是一种非常重要的技术,使用函数表达式无需对函数进行命名,从而实现动态编程。下面是函数表达式的一些显著特点:
- 函数声明要求一定具有名字,而函数表达式则不需要。没有名字的函数表达式也被称为匿名函数。
当在函数内部定义了其他函数时,就创建了闭包。闭包有权访问包含函数中的所有变量,原理如下;
- 闭包函数的作用域链包含着自己的活动对象,包含函数的变量对象以及全局函数的变量对象。因此能够沿着作用域链访问包含函数中的变量。
- 通常,函数的作用域及其所有变量都将在函数执行完毕后被销毁。
- 但是当函数返回一个闭包时,这个函数的作用域会一直存在直到闭包被销毁为止。
使用闭包可以在JavaScript中模仿块级作用域,要点如下:
- 定义并立即调用一个函数,这样即可以执行其中的代码,又不会在内存中留下该函数的引用。
闭包还可以用于在对象中创建私有变量,相关要点如下:
- 即使 JavaScript中没有正式的私有对象属性的概念,但可以使用闭包来实现公有方法,而通过公 有方法可以访问在包含作用域中定义的变量。
- 有权访问私有变量的公有方法叫做特权方法。
- 可以使用构造函数模式、原型模式来实现自定义类型的特权方法,也可以使用模块 模式、增强 的模块模式来实现单例的特权方法。