首页 » 编写高质量代码:改善JavaScript程序的188个建议 » 编写高质量代码:改善JavaScript程序的188个建议全文在线阅读

《编写高质量代码:改善JavaScript程序的188个建议》建议85:谨慎处理对象的Scope

关灯直达底部

在面向对象的JavaScript编程中,包装继承了大量的对象,同时对象之间还有很多复杂的引用关系,这就使得很多在function被调用时的Scope不是想要的Scope。产生这些问题的原因都源于没有深入了解JavaScript的机制。

在JavaScript中,function是作用于词法范围而不是动态运行范围的,也就是说,function的作用范围是它声明的范围,而不是它在执行时的范围。简单地说,一个function在执行时的上下文环境Context在其定义时就固定下来了,就是它定义时的作用范围。有一点需要注意,很多时候动态地将某个方法注入到一个对象内部,然而在运行时总是得不到想要的上下文环境,这是因为没有正确理解JavaScript的Scope。

当一个function被JavaScript引擎调用执行时,这个function的Scope才起作用,并且被加到Scope链中。然后,将一个名为Call Object的调用对象或运行对象加到Scope的最前面。这个调用对象在初始化时会加入一个arguments属性,用来引用function调用时的参数。如果这个function显式地定义了调用参数,那么这些参数也会被加入到这个对象中,之后在这个函数运行过程中所有的局部变量也都将包含在这个对象中。这也就是在function体内部既可以通过arguments数组,又可以直接通过显式定义参数名来引用调用时传入参数的原因。

调用对象和通过this关键字引用的对象是两个概念,调用对象是function在运行时的Scope,其中包含了function在运行时的全部参数和局部变量。通过this关键字引用对象,是当function作为一个对象的方法运行时对这个对象进行引用。如果这个function没有被定义在一个对象中,传给this的对象是全局对象,那么在这个function内部通过this取到的变量就是全局定义的变量。例如,下面代码运行结果应该弹出“Hello,I am Qin Jian”。


var hello=/"Hello,I am Shao Yu!/";

function sayHello{

var hello=/"Hello,I am Qin Jian./"

function anotherFun{

alert(hello);

}

anotherFun;

}

sayHello;


在JavaScript中,允许定义匿名function和在function中嵌套定义另一个function。由于Scope链的作用,function的内部总是可以访问外部的变量而外部却不能访问内部的变量。另外,由于Call Object的机制,可以使用function嵌套定义和调用来做很多事情。在JavaScript中,这样的调用称为Closure闭包。例如,下面代码使用闭包生成唯一ID。


uniqueID=(function{

var id=0;

return function{

return id++;

};

});

alert(uniqueID);//0

alert(uniqueID);//1

alert(uniqueID);//2


上面这段代码很清楚地说明了闭包做了什么事情。当外层的function被执行时,它的Scope被加入到Scope chain上,然后为它创建Call Object并加入到Scope中,之后又创建了局部变量id并将它保存在该function的Call Object中。如果没有“return function{return id++;};”这条语句,那么外层的function将会运行结束并退出,同时它的Call Object会被释放,Scope会从Scope chain上移除。由于“return function{return id++;};”这条语句创建了一个内部的function,并且将其引用返回给一个变量,因此内部function的Scope会被添加到之前外部function的Scope之下,使得在外部function运行结束后它的Scope不能被撤销和释放。这样就是用外部function的Call Object保存了变量id,并且除了内部的function以外没有别的程序能访问到这个变量。虽然看起来有些复杂,但是闭包确实是一项非常有用的功能,经常用来保存变量、控制访问域等。例如,在下面示例中利用Call Object和闭包保存数据。


function makefunc(x){

return function{

return x;

}

}

var a=[makefunc(/"I am Qin Jian/"),makefunc(/"I am shao yu/"),makefunc(/"I am xu ming/")];

alert(a[0]);//I am Qin Jian

alert(a[1]);//I am shao yu

alert(a[2]);//I am xu ming


JavaScript中提供了两个非常有意思的方法:call和apply,使用它们可以将一个function作为另一个Object的对象方法来调用,也就是说,可以在选择function调用时,将其传入给this关键字的对象。这两个方法第一个参数是相同的,都是想要传入给this关键字的对象。不同之处是,call方法直接将function参数列在后面,而apply方法是将所有function参数以一个数组的形式传入。例如:


var fun=function(arg1,arg2){

//...

}

fun.call(object,arg1,arg2);

fun.apply(object,[arg1,arg2]);


这两个方法在面向对象的JavaScript编程中是非常有用的,因为有时希望给某个对象添加一个事件监听,然而回调方法的context却不一定是需要的,这时就需要使用call或apply方法了。

eval方法用于执行某个字符串中的JavaScript脚本,并返回执行结果。它允许动态生成的变量、表达式、函数调用、语句段得到执行,使得JavaScript程序的设计过程更为灵活,如通过Ajax方式从服务器端获得代表JavaScript脚本的字符串,然后就可以用eval方法在浏览器端执行这段脚本。传统的Ajax通信设计更多的是在服务器端与浏览器端交换数据,但是在eval方法的支持下可以在两者之间交换逻辑,这是一种很有趣的事情。

Eval方法很灵活也比较简单,在调用此方法时将要执行的JavaScript脚本字符串作为参数。比较常用的就是将服务器端发送过来的JSON字符串在浏览器端转化为JSON对象。例如,使用eval方法将JSON字符串转换为对象。


var JSONString=/"{/'name/':{/'qinjian/':/'I am qinjian/',/'shaoyu/':/'I am shaoyu/'}}/";

var JSONObject=eval(/"(/"+JSONString+/")/");

alert(JSONObject.name.qinjian)


在上面的代码中,在JSON字符串中又加上了一对括号,这样做可以迫使eval方法在评估JavaScript代码时强制将原最外层括号内的内容作为表达式来执行从而得到JSON对象,而不是作为语句来执行。因此,只有用新的小括号将原来的JSON字符串包含起来才能够转换出所需的JSON对象。

对于执行一般的包含在字符串中的JavaScript语句,自然就不需要像上面那样再次添加括号了,例如:


function testStatement{

eval(/"var m=1/");

alert(m);

}

testStatement;


上面的函数会在弹出的提示对话框中输出变量m的值。虽然eval函数中的语句只是一个简单的赋值,但是有一个问题值得注意,那就是eval中的语句是在什么样的Scope中执行,为了更好地说明这个问题,执行下面的代码:


function testStatement{

eval(/"var m=1/");

alert(m);

}

testStatement;

alert(typeof m);


可以得到,变量m所在的Scope是在testStatement函数内,从eval调用的位置来看,这个执行结果是合理的。接下来用window.eval替换上面代码中的eval后,在Firefox浏览器(注意是Firefox)上执行代码,从执行后得到的结果中可以发现,这时的m的作用域变成了window,也就是说,m变成了一个全局变量。那么在IE上执行会如何呢?经测试发现,使用window.eval和eval会在IE上得到相同的结果,即m的作用域在testStatement函数中。但IE提供了另外一个方法execScript,它会将输入的JavaScript脚本字符串放到全局作用域下执行。总结一下:eval方法是将输入的代码在局部作用域下执行,若要使JavaScript字符串中包含的代码具有全局作用域执行效果,要么把eval放到全局作用域下调用,要么在Firefox中使用window.eval,在IE中使用execScript。最后需要提醒的是,因为eval的执行效率较低,所以在程序中最好不要频繁使用。为了避免Scope问题,应该注意下列几方面内容。

(1)巧用闭包

了解了闭包利用Call Object的产生原理后,就可以很容易利用闭包,如利用namespace隔离和保存局部数据等。同时,闭包也很容易使this实例不是我们想要的this实例,这时就可以利用内层能够访问外层变量的特点将外层this实例赋给一个变量,内层可以通过这个变量顺利访问外层this实例。


switchAds:function(index){

var_this=this;

dojo.fadeOut({

node:_this.adBack,

duration:500,

onEnd:function{

dojo.fadeIn({

node:_this.adBack,

duration:500,

onEnd:function{

_this.currentAd=index;

}

}).play;

}

}).play;

}


(2)使用call和apply指定function调用时的Scope

我们经常会提供一种类似回调函数的机制,在设计某个接口的时候并不提供函数的实现而只负责调用该接口。例如,对外提供一个onclick事件接口,由于JavaScript的Scope机制属于词法范围而不是动态运行范围,因此回调函数运行的Scope往往不是我们想要的。对于这种情况,可以在注册回调函数时将Scope一起传进来,在需要调用该回调函数时使用call和apply方法来调用这个函数,同时将需要的Scope传递给这个函数。例如,dojo的事件机制就是这样处理的,在注册回调函数的同时可以指定函数调用的Scope。


dojo.connect(node,/"onclick/",this,this._collapse);


(3)慎用eval

eval可以动态地从字符串中执行代码,它使JavaScript的功能更加强大。通常,eval有全局或局部两种运行Scope方式。当其运行在一个局部的function中时,需要注意,这时eval运行在这个function的Call Object中,它可以通过this关键字访问到这个function的Scope。另外,eval方法运行效率非常低,并且运行的脚本是未经验证的,因此在使用eval方法时要十分慎重。