不论从性能的角度来看,还是从功能的角度来看,作用域概念都是理解JavaScript的关键。作用域对JavaScript有许多影响,从确定哪些变量可以被函数访问,到确定this的值。JavaScript作用域也关系到性能,但要理解速度与作用域的关系,首先要理解作用域的工作原理。
(1)作用域链和标识符解析
每一个JavaScript函数都被表示为对象。函数对象如其他对象一样,拥有可以编程访问的属性和一系列不能被程序访问仅供JavaScript引擎使用的内部属性。其中一个内部属性是scope,由ECMA-262标准第三版定义。
内部属性scope包含一个函数被创建的作用域中对象的集合。此集合被称为函数的作用域链,它决定哪些数据可由函数访问。此函数作用域链中的每个对象被称为一个可变对象,每个可变对象都以“键-值对”的形式存在。在一个函数被创建后,它的作用域链被填充以对象,这些对象代表创建此函数的环境中可访问的数据。例如,下面这个全局函数:
function add(num1,num2){
var sum=num1+num2;
return sum;
}
在add函数被创建后,它的作用域链中填入一个单独的可变对象,此全局对象代表了所有在全局范围内定义的变量。此全局对象包含诸如窗口、浏览器和文档之类的访问接口。Add函数的作用域链将会在运行时用到。例如,下面的代码:
var total=add(5,10);
在运行add函数时将建立一个内部对象,称之为“运行期上下文”。一个运行期上下文定义了一个函数运行时的环境。对函数的每次运行而言,由于每个运行期上下文都是独一的,所以多次调用同一个函数就会导致多次创建运行期上下文。在函数执行完毕时运行期上下文就被销毁。
一个运行期上下文有它自己的作用域链,用于标识符解析。当运行期上下文被创建时,它的作用域链被初始化,连同运行函数的scope属性中所包含的对象也被初始化。scope属性值按照它们出现在函数中的顺序,被复制到运行期上下文的作用域链中。这项工作一旦完成,一个被称做“激活对象”的新对象就为运行期上下文创建好了。此激活对象作为函数执行期的一个可变对象,包含访问所有局部变量、命名参数、参数集合和this的接口。然后,此对象被推入作用域链的前端。当作用域链被销毁时,激活对象也一同被销毁。
在函数运行过程中,每遇到一个变量,标识符识别过程要决定从哪里获得或存储数据。在此过程中搜索运行期上下文的作用域链,查找同名的标识符。搜索工作从运行函数的激活目标的作用域链的前端开始。如果找到了,那么就使用这个具有指定标识符的变量;如果没找到,那么搜索工作将进入作用域链的下一个对象。此过程持续运行,直到标识符被找到,或者没有更多对象可以搜索(在这种情况下标识符将被认为是未定义的)。在函数运行时每个标识符都要经过这样的搜索过程。例如,在上面示例中,函数访问sum、num1、num2时都会产生这样的搜索过程。
(2)标识符识别性能
标识符识别不是“免费”的。事实上没有哪种计算机操作可以不产生性能开销。在运行期上下文的作用域链中,一个标识符所处的位置越深,它的读写速度就越慢。因此,在函数中,局部变量的访问速度总是最快的,全局变量通常是最慢的。全局变量总是处于运行期上下文作用域链的最后一个位置上,总是最后才被访问到。
总的趋势是,对所有浏览器来说,一个标识符所处的位置越深,读写它的速度就越慢。采用了优化的JavaScript引擎的浏览器,如Safari 4,在访问域外标识符时没有这种性能损失,而IE和其他浏览器则有较大幅度的性能损失。值得注意的是,早期浏览器(如IE 6和Firefox 2)将会耗费更多的时间访问域外变量。
通过以上内容可以了解,在没有优化JavaScript引擎的浏览器中,最好尽可能使用局部变量。一个好的经验法则:用局部变量存储本地范围之外的变量值,如果它们在函数中的使用多于一次。
function initUI{
var bd=document.body,links=document.getElementsByTagName_r(/"a/"),i=0,len=links.length;
while(i<len){
update(links[i++]);
}
document.getElementById(/"go-btn/").onclick=function{
start;
};
bd.className=/"active/";
}
上面示例中的函数包含3个对document的引用。document是一个全局对象,搜索此变量,必须遍历整个作用域链,直到在全局变量对象中找到它。可以通过这种方法减轻重复的全局变量访问对性能的影响:首先将全局变量的引用存储在一个局部变量中,然后使用这个局部变量代替全局变量。例如,上面的代码可以这样重写:
function initUI{
var doc=document,bd=doc.body,links=doc.getElementsByTagName_r(/"a/"),i=0,len=links.length;
while(i<len){
update(links[i++]);
}
doc.getElementById(/"go-btn/").onclick=function{
start;
};
bd.className=/"active/";
}
在上面代码中,首先将document的引用存入局部变量doc中。现在访问全局变量的次数是1次,而不是3次。用doc替代document更快,因为它是一个局部变量。当然,因为数量的原因,这个简单的函数不会显示出巨大的性能改进,不过可以想象一下,如果几十个全局变量被反复访问,那么性能改进显然会很明显。