经典计算机科学的一个问题是确定数据应当存放在什么地方,以实现最佳的读写效率。数据存储在哪里,关系到代码运行期间数据被检索到的速度。在JavaScript 中,此问题相对简单,因为数据存储只有少量方式可供选择。正如其他语言那样,数据存储位置关系到访问速度。在JavaScript 中有四种基本的数据访问位置:
(1)直接量:仅仅代表自己,而不存储于特定位置。JavaScript的直接量包括:字符串,数字,布尔值,对象,数组,函数,正则表达式,具有特殊意义的空值,以及未定义。
(2)变量:开发人员使用var 关键字创建用于存储数据值。
(3)数组项:具有数字索引,存储一个JavaScript 数组对象。
(4)对象成员:具有字符串索引,存储一个JavaScript 对象。
每一种数据存储位置都具有特定的读写操作负担。大多数情况下,对一个直接量和一个局部变量数据访问的性能差异是微不足道的。访问数组项和对象成员的代价要高一些,具体高多少,很大程度上依赖于浏览器。
老一些的浏览器使用传统的JavaScript 引擎,如Firefox 3,Internet Explorer 和Safari 3.2,它们比优化后的JavaScript 引擎耗费太多时间。总的来说,直接量和局部变量的访问速度要快于数组项和对象成员的访问速度。只有一个例外,Firefox 3,优化过数组项访问所以非常快。即使如此,一般的建议是,如果关心运行速度,那么尽量使用直接量和局部变量,限制数组项和对象成员的使用。为此,有几种模式来查看、避免并优化你的代码。
作用域概念是理解JavaScript的关键,不仅从性能的角度,而且从功能的角度。作用域对JavaScript 有许多影响,从确定哪些变量可以被函数访问,到确定this 的值。JavaScript作用域也关系到性能,但是要理解速度与作用域的关系,首先要理解作用域的工作原理。
一、作用域链和标识符解析
每一个JavaScript函数都被表示为对象。进一步说,它是一个函数实例。函数对象正如其他对象那样,拥有你可以编程访问的属性,和一系列不能被程序访问,仅供JavaScript引擎使用的内部属性。其中一个内部属性是[[Scope]],由ECMA-262标准第三版定义。内部[[Scope]]属性包含一个函数被创建的作用域中对象的集合。此集合被称为函数的作用域链,它决定哪些数据可由函数访问。此函数作用域链中的每个对象被称为一个可变对象,每个可变对象都以“键值对”的形式存在。当一个函数创建后,它的作用域链被填充以对象,这些对象代表创建此函数的环境中可访问的数据。例如下面这个全局函数:
function add(num1, num2){
var sum = num1 + num2;
return sum;
}
当add()函数创建后,它的作用域链中填入一个单独的可变对象,此全局对象代表了所有全局范围定义的变量。此全局对象包含诸如窗口、浏览器和文档之类的访问接口。图2-2 指出它们之间的关系(注意:此图中只画出全局变量中很少的一部分,其他部分还很多)。add 函数的作用域链将会在运行时用到。假设运行下面的代码:
var total = add(5, 10);
运行此add 函数时建立一个内部对象,称作“运行期上下文”。一个运行期上下文定义了一个函数运行时的环境。对函数的每次运行而言,每个运行期上下文都是独一的,所以多次调用同一个函数就会导致多次创建运行期上下文。当函数执行完毕,运行期上下文就被销毁。一个运行期上下文有它自己的作用域链,用于标识符解析。当运行期上下文被创建时,它的作用域链被初始化,连同运行函数的[[Scope]]属性中所包含的对象。这些值按照它们出现在函数中的顺序,被复制到运行期上下文的作用域链中。这项工作一旦完成,一个被称作“激活对象”的新对象就为运行期上下文创建好了。此激活对象作为函数执行期的一个可变对象,包含访问所有局部变量,命名参数,参数集合,和this的接口。然后,此对象被推入作用域链的前端。当作用域链被销毁时,激活对象也一同销毁。
在函数运行过程中,每遇到一个变量,标识符识别过程要决定从哪里获得或者存储数据。此过程搜索运行期上下文的作用域链,查找同名的标识符。搜索工作从运行函数的激活目标之作用域链的前端开始。如果找到了,那么就使用这个具有指定标识符的变量;如果没找到,搜索工作将进入作用域链的下一个对象。此过程持续运行,直到标识符被找到,或者没有更多对象可用于搜索,这种情况下标识符将被认为是未定义的。函数运行时每个标识符都要经过这样的搜索过程,例如前面的例子中,函数访问sum,num1,num2时都会产生这样的搜索过程。正是这种搜索过程影响了性能。
二、标识符识别性能
标识符识别不是免费的,事实上没有哪种电脑操作可以不产生性能开销。在运行期上下文的作用域链中,一个标识符所处的位置越深,它的读写速度就越慢。所以,函数中局部变量的访问速度总是最快的,而全局变量通常是最慢的(优化的JavaScript 引擎在某些情况下可以改变这种状况)。请记住,全局变量总是处于运行期上下文作用域链的最后一个位置,所以总是最远才能触及的。
总的趋势是,对所有浏览器来说,一个标识符所处的位置越深,读写它的速度就越慢。采用优化的JavaScript 引擎的浏览器,如Safari 4,访问域外标识符时没有这种性能损失,而Internet Explorer,Safari 3.2,和其他浏览器则有较大幅度的影响。值得注意的是,早期浏览器如Internet Explorer 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";
}
此函数包含三个对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";
}
initUI()的新版本首先将document 的引用存入局部变量doc 中。现在访问全局变量的次数是1 次,而不是3 次。用doc 替代document更快,因为它是一个局部变量。当然,这个简单的函数不会显示出巨大的性能改进,因为数量的原因,不过可以想象一下,如果几十个全局变量被反复访问,那么性能改进将显得多么出色。