js之匿名函数
js之匿名函数
这篇博文内容来自于javascript高级程序设计第2版,向作者致敬
函数声明:
function functionName(arg0,arg1){
//函数体
}
函数表达式:
var functionName = function(arg0,arg1){
//函数体
}
函数声明和函数表达式之间的主要区别是前者会在代码执行前被加载到作用域中,而后者是在代码执行到那一行的时候才会有定义。另一个区别是函数声明会给函数指定一个名字,而函数表达式则创建一个匿名函数,然后将这个函数赋给一个变量
1 递归
递归函数是在一个函数通过名字调用自身的情况下构成的,例如:
function factorial(num){
if (num<=1){
return 1;
}
else{
return num*factorial(num-1);
}
}
上面这个经典的递归阶乘函数表面看来没有问题,但下面的代码却可能导致它出错:
var anotherFactorial = factorial;
factorial = null;
alert(anotherFactorial(4)); //出错
以上代码先把factorial()函数保存在变量anotherFactorial中,然后将factorial变量设置为null,结果指向原始函数 的引用只剩下一个。但在接下来调用anotherFactorial()时,由于必须执行factorial(),而factorial已经不再是函数, 所以就会导致错误。这种情况下使用arguments.callee可以解决这个问题。
2 闭包
Nicholas认为闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式是在一个函数内部创建另一个函数,以createComparisonFunction()函数为例:
function createComparisonFunction(propertyName){
return function(object1,object2){
var value1 = object1[propertyName];
var value1 = object2[propertyName];
if (value1 < value2){return -1;}
else if (value1>value2){return 1;}
else {return 0;}
};
}
上面红色的两行代码是内部函数(一个匿名函数)中的代码,这两行代码访问了外部函数中的变量propertyName。即使这个内部函数被返回了,且在其 他地方被调用了,但它仍可访问变量propertyName.之所以还能够访问这个变量,是因为内部函数的作用域链中包含 createComparisonFunction()的作用域。
当某个函数第一次被调用时,会创建一个执行环境及相应的作用域链,并把作用域链赋值给一个特殊的内部属性(即[[Scope]])。然后使用this、 arguments和其他命名参数的值来初始化函数的活动对象。但在作用域链中,外部函数的活动对象始终处于第二位,直到作用域链终点的全局执行环境。
在函数执行过程中,为读取和写入变量的值,需要在作用域中查找变量:
function compare(value1,value2){
if (value2<value2){
return -1;
}
else if (value1>value2){
return 1;
}
else {
return 0;
}
}
var result = compare(5,10);
上面的代码先定义了compare()函数,又在全局作用域调用了它。第一次调用compare()时,会创建一个包含this、 arguments、value1、value2的活动对象。全局执行环境的变量对象(包含this,result,compare)在 compare()执行环境的作用域中处于第二位,如图:
后台的每个执行环境都有一个表示变量的对象--变量对象。全局环境的变量对象始终存在,而像compare()函数这样的局部环境的变量对象则只在函数执
行过程中存在。在创建compare()函数时,会创建一个预先包含全局变量对象的作用域链,这个作用域链被保存在内部的[[Scope]]属性中。当调
用compare函数时,会为函数创建一个执行环境,然后通过复制函数的[[Scope]]属性中的对象构建起执行环境的作用域链。
此后又有一个活动对象(在此作为变量对象使用)被创建并被推入执行环境作用域链的前端。对于这个例子中compare()函数的执行环境而言,其作用域链中包含两个变量对象:本地活动对象和全局变量对象。
作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象
一般来讲在当函数执行完毕后,局部活动对象就会被销毁,内在中仅保存全局作用域,但闭包情况有所不同。另一个函数内部定义的函数会将包含函数的活动对象添加到它的作用域链中。因此,createComparisonFunction()函数内定义的匿名函数的作用域链中,实际上将会包含外部函数createComparisonFunction()的活动对象,
var
compare = createComparisonFunction("name");
var result =
compare({name:'Nicholas"},{name:"Greg"});
如图:
在匿名函数从createComparisonFunction()中被返回后,它的作用域链被初始化为包含createComparisonFunction()函数的活动对象和全局变量对象。这样createComparisonFunction()函数执行完毕后其活动对象也不会被销毁,因为匿名函数的作用域链仍然在引用这个活动对象。换句话说,createComparisonFunction()函数返回后,其执行环境的作用域链会被销毁,但它的活动对象仍会留在内存中;直到匿名函数被销毁后,createComparisonFunction()的活动对象才会被销毁
闭包和变量
作用域链的这种配置机制引出了一个值得注意的副作用,即闭包只能取得包含函数中任何变量的最后一个值。因为闭包保存的是整个变量对象,而不是某个特殊的变量。
function createFunctions(){
var result = new
Array();
for (var
i=0;i<10;i++){
result[i] = function(){return i;}
}
return result;
}
var funcs = createFunctions();
for (var i=0;i<funcs.length;i++){
document.write(funcs[i]()+"<br/>");
}
会输出10个10
这可能通过将第一个for循环中的代码更改为如下代码来修正:
result[i] = function(num){
return
function(){
return num;
}
}(i);
关于this对象
this对象是在运行时基于函数的执行环境绑定的:在全局函数中,this等于window,而当函数被作为某个对象的方法调用时,this等于那个对
象。不过匿名函数的执行环境具有全局性,因此其this对象通常指向window.当然,在通过call()或apply()改变函数执行环境的情况
下,this就会指向其他对象。
var name = "the window";
var object={
name:'My Object',
getNameFunc:function(){
return function(){
return
this.name;
}
}
}
alert(object.getNameFunc()());
//"the window"
因为每个函数在调用时,其活动对象都会自动取得两个特殊变量:this和arguments,内部函数在搜索这两个变量时只会搜索到其活动对象为止
当然,可以通过将getNameFunc修改为
function(){
var that = this;
return
function(){
return that.name;
}
}
这样就可以访问该对象了
内存泄漏
由于IE对JScript对象和COM对象使用不同的垃圾收集例程,因此闭包在IE中会导致一些问题。具体来说,若闭包作用域链中保存着一个HTML元素,就意味着该元素将无法被销毁:
function assignHandler(){
var element =
document.getElementByIdx_x_x_x_x_x_x_x_x('someElement');
element.onclick = function(){
alert(element.id);
}
}
以上代码创建了一个作为element元素事件处理程序的闭包,而这个闭包创建了一个循环引用。因为匿名函数保存了一个对assignHandler()
的活动对象的引用,因此就会导致无法减少element的引用数。只要匿名函数存在,element的引用数至少为1,因此它所占用的内存永远不会被回
收。但可修改如下:
function assignHandler(){
var element =
document.getElementByIdx_x_x_x_x_x_x_x_x('someElement');
var id = element.id;
element.onclick = function(){
alert(id);
}
element = null;
}
上面的代码中,把element.id副本保存在一个变量中,且在闭包中引用该变量消除了循环引用。但这还不够解决内存泄漏,因为闭包会引用包含函数的整
个活动对象,而其中包含着element。即使闭包不直接引用element,包含函数的活动对象中也仍然会保存一个引用。因此有必要把element变
量设置为null。这样就能够解除对DOM对象的引用,顺利地减少其引用数,确保正常回收其占用的内存。
模仿块级作用域
用作块级作用域(通常称为私有作用域)的匿名函数的语法如下所示:
(function(){
})();
var functionName = function(){
}();
上面函数表达式定义后立即执行的语法没错,但function(){}()会出错,因为javascript把function关键字当作一个函数声明的
开始,而函数声明后面不能跟圆括号。而函数表达式的后面可以跟圆括号。要将函数声明转换为函数表达式,只要加上一对圆括号即可:
(function(){})()
匿名函数中定义的任何变量都会在执行结束时被销毁。在由很多开发人员参与的大型应用中,通过创建私有作用域使每个开发人员既可使用自己的变量,又不必担心搞乱全局作用域。
私有变量
任何函数中定义的变量都可认为是私有变量,因为不可在函数外部访问这些变量。私有变量包括函数的参数,局部变量和在函数内部定义的其他函数。如果在函数内部创建一个闭包,则闭包通过自己的作用域链也可访问这些变量,利用这一点可创建用于访问私有变量的仅有方法。
我们把有权访问私有变量和私有函数的公有方法称为特权方法。有两种在对象上创建特权方法的方式。
第一种是在构造函数中定义特权方法:
function MyObject(){
var privateVariable =
10;
function
privateFunction(){
return false;
}
this.publicMethod =
function(){
privateVariable++;
return privateFunction();
}
}
在创建MyObject实例后,除了使用publicMethod()外没有任何办法可以直接访问privateVariable和privateFunction()
在构造函数中定义特权方法有一个缺点,即必须使用构造函数模式来达到目的
静态私有变量:
通过在私有作用域中定义私有变量或函数,同样可以创建特权方法,基本模式如下:
(function(){
//私有变量和私有函数
var privateVariable =
10;
function
privateFunction(){
return false;
}
//构造函数
MyObject =
function(){};
//公有/特权方法
MyObject.prototype.publicMethod =
function(){
privateVariable++;
return privateFunction();
}
})();
这个模式创建了一个私有作用域,并在其中封装了一个构造函数及相应的方法。在私有作用域中,首先定义了私有变量和私有函数,然后又定义了构造函数及其公有
方法。公有方法是在原型上定义的,这是原型模式。需要注意的是,这个模式在定义构造函数时并没有使用函数声明,而是使用了函数表达式。函数声明只能创建局
部函数,出于同样的原因,MyObject创建为了全局变量。
模块模式:
前面的模式是用于为自定义类型创建私有变量和特权方法的。而模块模式是为单例创建私有变量和特权方法。所谓单例,指的就是只有一个实例的对象。
var singleton={
name:value,
method:function(){}
}
模块模式通过为单例添加私有变量和特权方法能够使其得到增强,其语法形式如下:
var singleton = function(){
var
privateVariable = 0;
function privateFunction(){return false;}
//特权/公有方法和属性
return
{
publicProperty:true,
return
privateFunction();
}
}();
这个模块模式使用了一个返回对象的匿名函数。在这个匿名函数内部,首先定义了私有变量和函数。然后将一个对象直接量作为函数的值返回。返回的对象直接量中
只包含可以公开的属性和方法。由于这个对象是在匿名函数内部定义的,因此它的公有方法有权访问私有变量和函数。本质上讲,这个对象直接量定义的是单例的公
共接口。这种模式在需要对单例进行某些初始化,同时又需要维护其私有变量时是非常有用的。例如:
function BaseComponent(){}
function OtherComponent(){}
var application = function(){
var
components = new Array(); //私有变量和函数
//初始化
components.push(new BaseComponent());
//公共
return{
getComponent:function(){return components.length;},
registerComponent:function(component){
if(typeof component == "object")
components.push(component);
}
}
}();
在web应用程序中,经常需用一个单例来管理应用程序级的信息。这个例子创建了一个用于管理组件的application对象。通常,若需创建一个对象并以某些数据对其进行初始化,同时还要公开一些能访问私有数据的方法,就可以使用模块模式。
增强的模块模式:
适合那些单例必须是某种类型的实例,同时还须添加某些属性和方法对其加以增强的情况。如若上例中application对象须是BaseComponent的实例,则可用以下代码:
var application=function(){
var components = new
Array();
components.push(new
BaseComponent());
var app = new
BaseComponent();
app.getComponentCount =
function(){
return components.length;
}
app.registerComponent =
function(component){
if(typeof component == 'object'){
components.push(component);
}
}
return app;
}();