本章内容
- 函数表达式的特征
- 使用函数实现递归
- 使用闭包定义私有变量
第五章曾介绍过,定义函数的方式有两种:一种是函数声明,另一种是函数表达式
关于函数声明,它的一个重要特征是函数提升,这就意味着可以把函数声明放在调用它的语句后面。
sayHi();//不会抛出错误。
function sayHi(){
alert("hi");
}
关于函数表达式,它有几种不同的语法形式。
var functionName = function(arg0, arg1, arg2){
//函数体
};
这种形式好像常规的变量赋值语句,即创建一个函数并将它赋值给变量。functionName.这种情况下创建的函数叫做匿名函数。匿名函数的name属性是空字符串。
函数表达式与其他表达式一样,在使用前必须先赋值。以下代码会导致错误。
sayHi();//错误,函数还不存在
var sayHi = function(){
alert("hi");
}
理解函数提升的关键,就是理解函数声明与函数表达式的区别。
能够创建函数再赋值给变量,也能够把函数作为其他函数的值返回。
一、递归
递归函数是指一个函数通过名字调用自身,如下是一个经典的阶乘函数。
function factorial(num){
if(num <= 1){
return 1;
} else {
return num * factorial(num -1);
}
}
//下面代码可能导致阶乘函数出错
var anotherFactorial = factorial;
factorial = null;
alert(anotherFactorial (4));//出错
上面出错的原因是将factorial变量置为null,结果指向原始函数的引用只剩下一个。但在调用anotherFactorial 时,在函数内部必须执行factorial(num -1),而factorial已不是函数,导致出错。有两种方法解决这个问题。
//1.arguments.callee是指向正在执行的函数的指针
function factorial(num){
if(num <= 1){
return 1;
} else {
return num * arguments.callee(num -1);
}
}
//2. 若在严格模式下,不能通过脚本访问arguments.callee。使用命名函数表达式。
//就算将factorial赋值给另外一个变量,函数f仍然有效。
var factorial = (function f(num){
if(num <= 1){
return 1;
} else {
return num * f(num -1)
})
二、闭包
要区分匿名函数与闭包的区别。闭包是指有权访问另一个函数作用域中变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数。如下:
function createComparionFunction(propertyName){
return function(object1, object2){
var value1 = object1[propertyName];
var value2 = object2[propertyName];
if(value1 < value2){
return -1;
}else if(value1 > value2){
return 1;
}else{
return 0;
}
};
}
当某个函数被调用时,会创建一个执行环境及相应的作用域链。然后通过arguments和其他命名参数的值来初始化函数的活动对象。后台的每个执行环境都有一个表示变量的对象---变量对象。全局环境的变量对象始终存在,而局部环境的变量对象则只在函数执行过程中存在。作用域链的本质是一个指向变量对象的指针列表,它只引用但不包含实际的变量对象。
在上述例子中,闭包函数可以访问外部函数中的propertyName。即使这个内部函数被返回了,而且是在其他地方被调用了,但它仍然可以访问变量propertyName。之所以还能够访问这个变量是因为内部函数的作用域链中包含createComparsionFunction()的作用域。
在匿名函数从createComparisonFunction()中返回以后,它的作用域链被初始化为包含createComparsionFunction()函数的活动对象和全局变量对象。这样匿名函数就可以访问createComparsionFunction()中定义的变量。更为重要的是createComparsionFunction()函数在执行完毕之后,其活动对象也不会被销毁,因为匿名函数的作用域链仍然在引用这个活动对象。换句话说,当createComparsionFunction()函数返回后,其执行环境的作用域会被销毁,但它的活动对象仍然会留在内存中,直到匿名函数被销毁后,createComparsionFunction()函数的活动对象才会被销毁。例如:
//创建函数
var compareNames = createComparsionFunction("name");
//调用函数
var result = compareNames({name:"Nicholas"},{name:"Greg"});
//解除对匿名函数的引用(以便释放内存)
comparNames = null;
2.1 闭包与变量
作用域链的这种配置机制,引出了一个值得注意的副作用,即闭包只能取得包含函数中任何变量的最后一个值。
function createFunctions(){
var result = new Array();
for(var i=0; i<10; i++){
result[i] = function(){
return i; //直接把闭包赋值给数组
};
}
return result;
}
上述每个函数都返回10,因为每个函数作用域链中都保存着createFunctions()的活动对象,所以他们引用的是同一个变量。但我们可以用以下匿名函数强制让闭包的行为符合预期。
function createFunctions(){
var result = new Array();
for(var i = 0; i<10; i++){
result[i] = function(num){
return function (){ return num; }//按值传递num,定义一个匿名函数返回num
}(i);
}
return result;
}
在重写了函数之后,每个函数就会返回各自不同的索引值了。在这个版本中,我们没有直接把闭包赋值给数组,而是定义了一个匿名函数,并将立即执行该匿名函数的结果赋值给数组。这个匿名函数有一个参数num,并且是按照值传递的,所以会将变量i的当前值赋值给num。而在这个匿名函数内部,又创建并返回了一个访问num的闭包。这样result数组中都有自己的num的一个副本,因此就可以返回各自不同的值了。
2.2 关于this对象
我们知道,this对象是在运行时基于函数的执行环境绑定的:在全局函数中,this等于window,而当函数被作为某个对象的方法调用时,this等于哪个对象。不过匿名函数的执行环境具有全局性,因此this通常指向window。
var name = "This window";
var object = {
name:"My Object",
getNameFunc:function(){
return funcion () {
return this.name;
}
}
};
alert(object.getNameFunc()());//"This window"
上述例子调用匿名函数并立即执行,为什么返回的是全局name变量的值而没有取得其包含作用域(或外部作用域)的this对象呢?
每个函数在被调用时都会自动取得两个特殊的变量:this和arguments。内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量。不过把外部作用域中的this对象保存在一个闭包能够访问的变量里,就可以让闭包访问该对象了。如下所示
var name = "This window";
var object = {
name:"My object";
getNameFunc: function(){
var that = this;
return function(){
return that.name;
}
}
};
alert(object.getNameFunc()()); //"My object"
2.3 内存泄漏
闭包有时会导致一些特殊的问题。具体来说,如果闭包的作用域链中保存着一个HTML元素,那么就意味着该元素将无法被销毁。
function assignHandler(){
var element = document.getElementById("someElement");
element.onclick = function(){
alert(element.id);
}
}
以上代码创建了一个作为element元素事件处理程序的闭包,而这个闭包又创建了一个循环引用。由于匿名函数中保存了一个对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。
三、模仿块级作用域
Javascript中没有块级作用域的概念。这意味着在块语句中定义的变量,实际上是在函数中而非语句中定义的。看下面的例子。
function outputNumbers(count){
for(var i=0;i
这个函数中定义了一个for循环,而变量i的初始值被设为0。在java/C++等语言中,变量i只会在for循环语句中有定义,循环一旦结束,变量i就会被销毁。可是在JavaScript中,变量i是定义在outputNumbers()的活动对象中的。因此函数中可以随处访问这个变量。即使重新声明,Javascript也只会对后续的声明视而不见。
用作块级作用域(通常称作私有作用域)的匿名函数语法如下所示:
(function(){
//这里是块级作用域。
})();
将匿名函数的函数声明包含在圆括号中表示它实际上是一个函数表达式。而紧随其后的另一对圆括号表示立即调用这个函数。在匿名函数中定义的任何变量,都会在执行结束时被销毁。
无论在什么地方,只要临时需要一些变量,就可以使用私有作用域。这种技术经常在全局作用域中被用在函数外部,从而限制向全局作用域中添加过多的变量和函数。通过创建私有作用域,每个开发人员既可以使用自己的变量,又不必担心搞乱全局作用域。
四、私有变量
严格来讲,Javascript没有私有成员的概念,所有的对象属性都是公有的。但有一个私有变量的概念。任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数外部访问这些变量。私有变量包括函数的参数、局部变量和在函数中定义的其他函数。
在函数内部定义的私有变量和私有函数,在函数外部不能访问他们。如果在函数内部创建一个闭包,那么闭包通过自己的作用域链可以访问他们。
我们把有权访问私有变量和私有函数的方法称为特权方法。有两种在函数中创建特权函数的方法。第一种是在构造函数中定义特权方法,基本模式如下:
function MyObject(){
//私有变量和私有函数
var privateVariable = 10;
function privateFunction(){
return false;
}
//特权方法
this.publicMethod = function(){
privateVariable++;
return privateFunction();
}
}
这个模式在构造函数内部定义了所有私有变量和函数。然后又继续创建了能够访问这些私有成员的特权方法。能够在构造函数中定义特权方法,是因为特权方法作为闭包有权访问在构造函数中定义的所有变量和函数。
除了利用特权方法访问私有变量。利用私有和特权成员,还可以隐藏哪些不应该被直接修改的数据。
在构造函数中定义特权方法也有一个缺点,即每个实例都会创建同一组新方法。使用静态私有变量可以避免这个问题。
4.1 静态私有变量
通过在私有作用域中定义私有变量或函数,同样也可以创建特权方法,其基本模式如下所示。
(function(){
//私有变量和私有函数
var privateVariable = 10;
function privateFunction(){
return false;
}
//构造函数,MyObject是一个全局变量
MyObject = function(){};
//公有/特权方法
MyObject.prototype.publichMethod = 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(person2.getName()); //"Michael"
alert(person2.getName()); //"Michael"
可以看出,在上述模式下,变量name变成了一个静态的,由所有实例共享的属性。也就是说,调用setName会影响所有实例。不过,到底是使用实例变量还是使用静态私有变量需要视情况而定。
4.2 模块模式
模块模式是指为单例创建私有变量和特权方法。所谓单例,指的是只有一个实例的对象。按照惯例,Javascript利用对象字面量的方式来创建对象.
模块模式通过为单例添加私有变量和特权方法使其功能得到增强。
var singleton =function(){
//私有变量和私有函数
var privateVariable = 10;
function privateFunction(){
return false;
}
//特权/公有方法和属性
return {
publicProperty:true,
publicMethod:function(){
privateVariable++;
return privateFunction();
}
};
}();
这个模块模式使用了一个返回对象的匿名函数。在这个匿名函数内部,首先定义了私有变量和函数。然后,将一个对象字面量作为函数的值返回。返回的对象字面量中值包含可以公开的属性和方法。由于这个对象字面量是在匿名函数中定义的,因此它的公有方法有权访问私有变量和函数。从本质上讲,这个对象字面量定义的是单例的公共接口。这种模式在需要对单例进行某些初始化,同时又需要维护其私有变量时是非常有用的。
4.3增强的模块模式
这种增强的模块模式适合那些单例必须是某种类型的实例,同时还必须添加某些属性和方法以对其实现增强的情况。如下application对象必须是BaseComponent类型的实例。
var application = function(){
//私有变量和私有函数
var components = new Array();
//初始化
components.push(new Basecomponent());
//创建一个application的局部副本
var app = new BaseComponent();
//公共接口
app.getComponentCount = function(){
return components.length;
}
app.registerComponents = function(component){
if(typeof component == "object"){
components.push(component)
}
};
//返回这个副本
return app;
}();
上述例子的不同之处在于命名变量app的创建过程,因为它必须是BaseComponent的实例。