第七章:JS函数表达式
本章内容:
- 函数表达式的特征
- 使用函数实现递归
- 使用闭包定义私有变量
函数表达式是 JavaScript 中的一个既强大又容易令人困惑的特性。定义函数的 方式有两种:一种是函数声明,另一种就是函数表达式。
关于函数声明,它的一个重要特征就是函数声明提升(function declaration hoisting),意思是在执行 代码之前会先读取函数声明。这就意味着可以把函数声明放在调用它的语句后面。
sayHi();
function sayHi(){
alert("Hi!");
}
第二种创建函数的方式是使用函数表达式。函数表达式有几种不同的语法形式。下面是最常见的一种形式
var functionName = function(arg0, arg1, arg2){ //函数体
};
这种形式看起来好像是常规的变量赋值语句,即创建一个函数并将它赋值给变量 functionName。 这种情况下创建的函数叫做匿名函数(anonymous function),因为 function 关键字后面没有标识符。
匿名函数有时候也叫拉姆达函数。
理解函数提升的关键,就是理解函数声明与函数表达式之间的区别。
7.1 递归
递归函数是在一个函数通过名字调用自身的情况下构成的,
function factorial(num){
if (num <= 1){
 return 1;
} else {
 return num * factorial(num-1);
}
}
这是一个经典的递归阶乘函数。虽然这个函数表面看来没什么问题,但下面的代码却可能导致它出错。
var anotherFactorial = factorial;
factorial = null;
alert(anotherFactorial(4)); //出错
在接下来调用 anotherFactorial()时
,由于必须执行 factorial(),而 factorial 已经不再是函数,所以就会导致错误
在这种情况下,使用argu- ments.callee
可以解决这个问题。我们知道,arguments.callee是一个指向正在执行的函数的指针,因此可以用它来实现对函数的递归调用,
五种基本类型:Undefied,Null,Number,Boolean,String
引用类型:Object / Array / Function / Date / RegExp / Error / Map / Set
…
闭包
有不少开发人员总是搞不清匿名函数和闭包这两个概念,因此经常混用。闭包是指有权访问另一个 函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数。
一般来讲, 当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域(全局执行环境的变量对象)。但是,闭包的情况又有所不同。
在另一个函数内部定义的函数会将包含函数(即外部函数)的活动对象添加到它的作用域链中。
解除对匿名函数的引用(以便释放内存)
compareNames = null;
由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多,我们建议读者只在绝对必要时再考虑使用闭包。虽然像V8等优化后的JavaScript引擎会尝试回收被闭包占用的内存,但请大家还是要慎重使用闭包
看来关于内存泄露的问题,以后要谈谈
7.2.1 闭包与变量
作用域链的这种配置机制引出了一个值得注意的副作用,即闭包只能取得包含函数中任何变量的最 后一个值。别忘了闭包所保存的是整个变量对象,而不是某个特殊的变量。下面这个例子可以清晰地说 明这个问题。
function createFunctions(){
var result = new Array();
for (var i=0; i < 10; i++){
result[i] = function(){
return i; };
}
return result;
}
因为每个函数的作用域链中都保存着createFunctions()函数的活动对象,所以它们引用的都是同一个变量i。当 createFunctions()函数返回后,变量i的值是10,此时每个函数都引用着保存变量i的同一个变量 对象,所以在每个函数内部 i的值都是10。但是,我们可以通过创建另一个匿名函数强制让闭包的行为符合预期,如下所示。
function createFunctions(){
var result = new Array();
for (var i=0; i < 10; i++){
result[i] = function(num){
return function(){
return num;
return result;
}
定义了一个匿名函数,并将立即执行该匿名函数的结果赋给数组
这样一来,result数组中的每个函数都有自己num变量的一个副本,因此就可以返回各自不同的数值了
7.2.2 关于this对象
this 对象是在运行时基于函数的执 行环境绑定的:在全局函数中,this 等于 window,而当函数被作为某个对象的方法调用时,this 等 于那个对象。不过,匿名函数的执行环境具有全局性,因此其 this 对象通常指向 window1。但有时候 由于编写闭包的方式不同,这一点可能不会那么明显。如下:
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()()); //"The Window"(在非严格模式下)
为什么匿名函数没 有取得其包含作用域(或外部作用域)的 this 对象呢?
前面曾经提到过,每个函数在被调用时都会自动取得两个特殊变量:this和arguments。内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量。
不过,把外部作用域中的 this 对象保存在一个闭包能够访问 到的变量里,就可以让闭包访问该对象了,如下所示。
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
var that = this;
return function(){
return that.name;
}; }
};
alert(object.getNameFunc()()); //"My Object"
代码中突出的行展示了这个例子与前一个例子之间的不同之处。在定义匿名函数之前,我们把this对象赋值给了一个名叫that 的变量。而在定义了闭包之后,闭包也可以访问这个变量,因为它是我们在包含函数中特意声名的一个变量。即使在函数返回之后,that也仍然引用着object,所以调用object.getNameFunc()()就返回了”My Object”。
this和arguments也存在同样的问题。如果想访问作用域中的arguments对象,必须将对该对象的引用保存到另一个闭包能够访问的变量中。
7.2.3 内存泄漏
由于 IE9之前的版本对JScript对象和COM对象使用不同的垃圾收集例程(第4章曾经讨论过),因此闭包在IE的这些版本中会导致一些特殊的问题。具体来说,如果闭包的作用域链中保存着一个HTML元素,那么就意味着该元素将无法被销毁。
function assignHandler(){
var element = document.getElementById("someElement");
element.onclick = function(){
alert(element.id);
};
}
以上代码创建了一个作为 element 元素事件处理程序的闭包,而这个闭包则又创建了一个循环引 用(事件将在第 13 章讨论)。由于匿名函数保存了一个对 assignHandler()的活动对象的引用,因此 就会导致无法减少 element 的引用数。只要匿名函数存在,element 的引用数至少也是 1,因此它所 占用的内存就永远不会被回收。不过,这个问题可以通过稍微改写一下代码来解决,如下所示。
function assignHandler(){
var element = document.getElementById("someElement"); var id = element.id;
element.onclick = function(){
alert(id);
};
element = null;
}
在上面的代码中,通过把element.id的一个副本保存在一个变量中,并且在闭包中引用该变量消除了循环引用。但仅仅做到这一步,还是不能解决内存泄漏的问题。必须要记住:闭包会引用包含函数的整个活动对象,而其中包含着element。即使闭包不直接引用element,包含函数的活动对象中也仍然会保存一个引用。因此,有必要把element变量设置为null。这样就能够解除对DOM对象的引用,顺利地减少其引用数,确保正常回收其占用的内存。
7.3 模仿块级作用域
如前所述,JavaScript没有块级作用域的概念。这意味着在块语句中定义的变量,实际上是在包含 函数中而非语句中创建的,来看下面的例子。
function outputNumbers(count){
for (var i=0; i < count; i++){
alert(i); }
alert(i); //计数
}
个函数中定义了一个 for 循环,而变量 i 的初始值被设置为 0。在 Java、C++等语言中,变量 i 只会在 for 循环的语句块中有定义,循环一旦结束,变量 i 就会被销毁。可是在 JavaScrip 中,变量 i 是定义在 ouputNumbers()的活动对象中的,因此从它有定义开始,就可以在函数内部随处访问它。
匿名函数可以用来模仿块级作用域并避免这个问题。用作块级作用域(通常称为私有作用域)的匿名函数的语法如下所示。
(function(){
//这里是块级作用域
})();
将函数声明包含在一对圆括号中,表示它实际上是一个函数表达式。一对圆括号将函数声明转换成函数表达式。
无论在什么时候,只要临时需要一些变量,就可以使用私有作用域。
function outputNumbers(count){
(function () {
for (var i=0; i < count; i++){
alert(i);
}
})();
alert(i); // 导致一个错误!
}
这种技术经常在全局作用域中被用在函数外部,从而限制向全局作用域中添加过多的变量和函数。
这种做法可以减少闭包占用的内存问题,因为没有指向匿名函数的引用。只要函数执行完毕,就可以立即销毁其作用域链了。
7.4 私有变量
严格来讲,Javascript中没有私有成员的概念;所有对象属性都是公有的。不过,倒是有一个私有变量的概念。任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数的外部访问这些变量。私有变量包括函数的参数,局部变量和在函数内部定义的其他函数。
如果在这个函数内部创建一个闭包,那么闭包通过自己的作用域链也可以访问这些变量。而利用这一点,就可以创建用于访问私有变量的公有方法。
我们把有权访问私有变量和私有函数的公有方法称为特权方法(privileged method)。
有两种在对象 上创建特权方法的方式。第一种是在构造函数中定义特权方法,基本模式如下。
function MyObject(){
//私有变量和私有函数
var privateVariable = 10;
function privateFunction(){
return false;
}
//特权方法
this.publicMethod = function (){
privateVariable++;
return privateFunction();
};
}
不过,在构造函数中定义特权方法也有一个缺点,那就是你必须使用构造函数模式来达到这个目的。
曾经讨论过,构造函数模式的缺点是针对每个实例都会创建同样一组新方法,而使用静态私有变量来实现特权方法就可以避免这个问题。
7.4.1 静态私有变量
通过在私有作用域中定义私有变量或函数,同样也可以创建特权方法
(function(){
//私有变量和私有函数
var privateVariable = 10;
function privateFunction(){
return false;
}
//构造函数
MyObject = function(){ };
//公有/特权方法
MyObject.prototype.publicMethod = function(){
privateVariable++;
return privateFunction();
};
})();
需要注意的是,这个模式在定义构造函数时并没有使用函数声明,而是 使用了函数表达式。函数声明只能创建局部函数,但那并不是我们想要的。出于同样的原因,我们也没 有在声明 MyObject 时使用 var 关键字。记住:初始化未经声明的变量,总是会创建一个全局变量。 因此,MyObject 就成了一个全局变量,能够在私有作用域之外被访问到。但也要知道,在严格模式下给未经声明的变量赋值会导致错误。
这个模式与在构造函数中定义特权方法的主要区别,就在于私有变量和函数是由实例共享的。由于特权方法是在原型上定义的,因此所有实例都使用同一个函数。而这个特权方法,作为一个闭包,总是保存着对包含作用域的引用。来看一看下面的代码。
(function(){
var name = "";
Person = function(value){
name = value;
};
Person.prototype.getName = function(){
return name;
};
Person.prototype.setName = function (value){
name = value;
};
})();
var person1 = new Person("Nicholas"); alert(person1.getName()); //"Nicholas" person1.setName("Greg"); alert(person1.getName()); //"Greg"
var person2 = new Person("Michael");
alert(person1.getName()); //"Michael"
alert(person2.getName()); //"Michael"
以这种方式创建静态私有变量会因为使用原型而增进代码复用,但每个实例都没有自己的私有变量。到底是使用实例变量,还是静态私有变量,最终还是要视你的具体需求而定。
查找作用域链中的一个层次,就会在一定程度上影响查找速度。而这正是使用 闭包和私有变量的一个显明的不足之处。
7.4.2 模块模式
模块模式通过为单例添加私有变量和特权方法能够使其得到增强,其语法形式如下:
var singleton = function(){
//私有变量和私有函数
var privateVariable = 10;
function privateFunction(){
return false;
}//特权/公有方法和属性
return {
publicProperty: true,
publicMethod : function(){
privateVariable++;
return privateFunction();
}
}; }();
7.4.3 增强的模块模式
有人进一步改进了模块模式,即在返回对象之前加入对其增强的代码。这种增强的模块模式适合那 些单例必须是某种类型的实例,同时还必须添加某些属性和(或)方法对其加以增强的情况。