函数表达式是JavaScript当中一个既强大又令人困惑的特性,特别是其中涉及到的闭包,更是令许多的初学者困惑不已。
在之前的章节中有介绍过,定义函数的方法有两种:一种是函数声明。
function functionName(arg0, arg1, arg2) {
//函数体
}
对于函数声明,有一个重要的特征就是可以把函数声明放在调用它的语句后面,因为解释器会在执行语句之前先读取函数的声明。
另一种方法是使用函数表达式。
var functionName = function(arg0, arg1, arg2){
//函数体
};
函数表达式与函数声明的一个主要不同就是,它必须在定义之后才能使用,否则将会报错。函数表达式中的函数是一个匿名函数,即function
关键字后面没有标识符。既然可以把函数赋值给变量,也就可以把函数作为其它函数的返回值。
JavaScript中的递归有个问题,将保存函数的变量赋值给另一个变量时,因为函数名称改变了,所以在递归调用的时候会出现问题:
function factorial(num){
if (num <= 1){
return 1;
} else {
return num * factorial(num-1);
}
}
var anotherFactorial = factorial;
factorial = null;
alert(anotherFactorial(4)); //出错!
要解决这个问题,可以使用arguments.callee
。这个属性是一个指向正在执行的函数的指针,所以可以利用它来代替函数名,这就确保递归调用时函数名称改变也不会出错。但是,在严格模式下,使用argments.callee
会导致错误。我们可以使用命名函数来达成相同的结果:
var factorial = (function f(num){
if (num <= 1){
return 1;
} else {
return num * f(num-1);
}
});
闭包是一个容易令人困惑的概念。闭包是指有权访问另一个函数作用域中的变量的函数。创建装饰的常见方式就是嵌套定义函数。
我们在第4章的时候了解过作用域链。而对于作用域链清晰地理解,是理解闭包的重要关键。
当调用函数的时候,会为函数创建一个执行环境,并将该环境的活动对象加入到作用域链的前端。作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。
当在一个函数内部定义另一个函数的时候,外部函数的活动对象会被添加到内部函数的作用域链中。一般情况下,当函数执行完毕后,它的活动对象就会被销毁。但是,当有内部函数与其构成闭包时,因为内部函数的作用域链仍在引用外部函数的活动对象,因此只有当内部的函数也被销毁后,这个外部活动对象才会被销毁。
闭包只能取得包含函数(即外部函数)中任何变量的最后一个值。
function createFunctions(){
var result = new Array();
for (var i=0; i < 10; i++){
result[i] = function(){
return i;
};
}
return result;
}
这个函数返回的函数数组中的每个函数都会返回10。因为每个函数的作用域链中都保存着createFunctions()
的活动对象,所以它们引用的都是同一个变量i
。当createFunctions()
返回后,i
的值是10,所以每个函数引用保存的变量i都是10。可以使用另一个匿名函数来修正这个问题:
function createFunctions(){
var result = new Array();
for (var i=0; i < 10; i++){
result[i] = function(num){
return function(){
return num;
};
}(i);
}
return result;
}
this
对象是在运行时,基于函数的执行环境绑定的。但是,匿名函数的执行环境具有全局性,因此其this
对象通常指向window
。
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()()); //"The Window"(在非严格模式下)
这是因为每个函数都有自己的this
,所以当内部函数在搜索这个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问到外部函数中的this
。
之前讲过,在JavaScript当中没有块级作用域。但是,我们可以使用匿名函数来模仿块级作用域。
(function(){
// 这里是块级作用域
})();
在函数的声明包含在一对圆括号中,表示它实际上是一个函数表达式,而紧随其后的另一对圆括号会立即调用这个函数。
在需要用到块级作用域的时候,就可以这样使用:
function outputNumbers(count){
(function () {
for (var i=0; i < count; i++){
alert(i);
}
})();
alert(i); //导致一个错误!
}
从技术角度来讲,JavsScript中是没有私有成员的概念的,所有对象属性都是公开的。因但是对于函数而言,函数里面的变量对外部都是私有的,我们可以利用在函数里面创建一个闭包,来创建用于访问函数内部私有变量的公有方法。
对于有权访问私有变量和私有函数的公有方法,我们称之为特权方法(privileged method)。创建特权方法的方式有两种:一是在构造函数当中定义特权方法。
function MyObject(){
//私有变量和私有函数
var privateVariable = 10;
function privateFunction(){
return false;
}
//特权方法
this.publicMethod = function (){
privateVariable++;
return privateFunction();
};
}
对于这个例子而言,变量privateVariable
和函数privateFunction()
只能通过特权方法publicMethod()
来访问,我们无法直接访问到内部私有的变量。
使用模式的缺点是针对每个实例都会创建同样一组新方法,可以使用另一种方法,静态私有变量来避免这个问题。
通过在私有作用域中定义私有变量和函数,也可以创建特权方法:
(function(){
//私有变量和私有函数
var privateVariable = 10;
function privateFunction(){
return false;
}
//构造函数
MyObject = function(){
};
//公有/特权方法
MyObject.prototype.publicMethod = function(){
privateVariable++;
return privateFunction();
};
})();
这个模式使用了函数表达式来定义特权方法,因为函数声明只能创建局部的函数,同样地,对于MyObject
我们也没有使用var
关键字,这样可以使其成为一个全局变量。
这个模式与构造函数模式的主要区别在于,私有变量和函数是由实例共享的。由于特权方法是在原型上定义的,因此所有实例都使用同一个函数。而这个特权方法作为一个装饰,问题保存着对包含作用域的引用。
模块模式是为单例创建私有变量和特权的方法。所谓单例,即只有一个实例的对象。
一般情况下,JavaScript是以对象字面量的方式来创建单例对象的。
模块模式通过为单例添加私有变量和特权方法能够使其得到增强,其语法形式如下:
var singleton = function(){
//私有变量和私有函数
var privateVariable = 10;
function privateFunction(){
return false;
}
//特权/公有方法和属性
return {
publicProperty: true,
publicMethod : function(){
privateVariable++;
return privateFunction();
}
};
}();
如果必须创建一个对象并以某些数据对其进行初始化,同时还要公开一些能够访问这些私有数据的方法,那么就可以使用模块模式。
这个模式即在返回对象之前加入对其增强的代码。这种增强的模块模式适合那些单例必须是某种类型的实例,同时还必须添加某些属性或方法对其加以增加的情况。
var singleton = function(){
//私有变量和私有函数
var privateVariable = 10;
function privateFunction(){
return false;
}
//创建对象
var object = new CustomType();
//添加特权/公有属性和方法
object.publicProperty = true;
object.publicMethod = function(){
privateVariable++;
return privateFunction();
};
//返回这个对象
return object;
}();
这章主要讨论了JavaScript当中的函数表达式与闭包。理解闭包的一个重要基础就是要透彻理解执行环境和作用域链。
函数表达式和闭包都是极其有用的特性,利用它们可以实现很多功能。不过,因为创建闭包必须维护额外的作用域,所以过度使用它们可能会占用大量内存。