高性能的JavaScript--数据访问(1)

时间:2024-09-24 15:04:44
写在前面

数据存储在哪里,关系到代码运行期间数据被检索到的速度。在JavaScript中,此问题相对简单,因为数据存储只有少量方式可供选择。正如其他语言那样,数据存储位置关系到访问速度。在JavaScript中有四种基本的数据访问位置:

1.Literal values 直接量

直接量仅仅代表自己,而不存储于特定位置。 JavaScript的直接量包括:字符串,数字,布尔值,对象,数组,函数,正则表达式,具有特殊意义的空值,以及未定义。

2.Variables 变量

开发人员使用var关键字创建用于存储数据值。

3.Array items 数组项

具有数字索引,存储一个JavaScript数组对象。

4.Object members 对象成员

具有字符串索引,存储一个JavaScript对象。

每一种数据存储位置都具有特定的读写操作负担。大多数情况下,对一个直接量和一个局部变量数据访问的性能差异是微不足道的。访问数组项和对象成员的代价要高一些,具体高多少,很大程度上依赖于浏览器。总的来说,直接量和局部变量的访问速度要快于数组项和对象成员的访问速度。,如果关心运行速度,那么尽量使用直接量和局部变量,限制数组项和对象成员的使用。

管理作用域

1.作用域链和标识符解析

每一个JavaScript函数都被表示为对象。进一步说,它是一个函数实例。函数对象正如其他对象那样,拥有你可以编程访问的属性,和一系列不能被程序访问,仅供JavaScript引擎使用的内部属性。其中一个内部属性是[[Scope]],由ECMA-262标准第三版定义。内部[[Scope]]属性包含一个函数被创建的作用域中对象的集合。此集合被称为函数的作用域链,它决定哪些数据可由函数访问。此函数作用域链中的每个对象被称为一个可变对象,每个可变对象都以“键值对”的形式存在。当一个函数创建后,它的作用域链被填充以对象,这些对象代表创建此函数的环境中可访问的数据。

当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain,不简称sc)来保证对执行环境有权访问的变量和函数的有序访问。作用域第一个对象始终是当前执行代码所在环境的变量对象(VO)

例如:

function add(x,y){
var b=x+y;
return b;
}

当add()函数创建后,它的作用域链中填入一个单独的可变对象,此全局对象代表了所有全局范围定义的变量。此全局对象包含诸如窗口、浏览器和文档之类的访问接口。

如图:
高性能的JavaScript--数据访问(1)

上图就是函数Add()的作用域链。

Add函数的作用域链将在运行时用到。如果运行下面的代码

var total = add(5, 10);

运行此add函数时建立一个内部对象,称作“运行期上下文”。一个运行期上下文定义了一个函数运行时的环境。对函数的每次运行而言,每个运行期上下文都是独一的。所以每次调用同一个函数就会导致多处调用上下文。当函数执行完毕,运行期上下文就被销毁
一个运行期上下文有它自己的作用域链,用于标示符解析。当运行期上下文被创建时,它的作用域被初始化,连同运行函数的[[Scope]]属性中所包含的对象。这些值按照它们出现在函数中的顺序,被复制到运行期上下文的作用域链中。这项工作一旦完成,一个被称作“激活对象”的新对象就为运行期上下文创建好了。此激活对象作为函数执行期的一个可变对象,包含访问所有局部变量,命名参数,参数集合,和this的接口,然后,这个对象被推入作用域的前端。当作用域链被销毁时,激活对象也一同销毁。

高性能的JavaScript--数据访问(1)

上图是运行时Add()函数的作用域链。

在函数运行过程中,没遇到一个变量,标识符识别过程要决定从哪里获得或者存储数据,此过程收索运行期上下文的作用域链,查找同名的标识符。搜索工作从运行函数的激活目标之作用域链的前端开始。如果找到了,那么就使用这个具有指定标识符的变量,如果没有找到,搜索工作将进入作用域链的下一个对象。此过程持续进行,直到找到标示符。如果整个过程都没有找到那么被认为是undefined。正是这种搜索过程影响了性能。

2.标识符解析的性能

标示符识别不是免费的,事实上没有哪种电脑操作可以不产生性能开销。在运行期山下文的作用域链中,一个标示符所处的位置越深,它的读写速度就越慢。所以,函数中局部变量的访问速度总是最快的,而全局变量通常是最慢的(优化的JavaScript引擎在某些情况下可以改变这种情况,如谷歌浏览器)。全局变量总是处于运行前上下文作用域链的最后一个位置。所以总是最远才能触及。
用局部变量存储本地范围之外的变量值,如果它们在函数中的使用多于一次。考虑下面的例子:

高性能的JavaScript--数据访问(1)
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";
}
高性能的JavaScript--数据访问(1)

此函数包括三个对document的引用,document是一个全局对象。搜索此变量,必须遍历整个作用域链,指导最后在全局变量对象中找到它。你可以通过这种方法减轻重复的全局变量访问对性能的影响;首先将全局变量的引用放在一个局部变量中,然后使用整个局部变量代替全局变量。 代码重写如下:

高性能的JavaScript--数据访问(1)
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";
}
高性能的JavaScript--数据访问(1)

initUI()的新版本首先将document的引用存入局部变量doc中,现在访问全局变量的次数是1次,而不是3次。用doc替代document更快,因为它是一个局部变量。当然,这个简单的函数不回显示出巨大的性能改进,因为数量的原因。不过如果几十个全局变量被反复访问,那么性能改进将明显的多么出色。

3.改变作用域链

一般来说,一个运行期上下文的作用域链不会突然被改变。但是,有两种表达式可以在运行时临时改变运行期上下文作用域链。第一个是with表达式。

with表达式为所有的对象属性创建了一个默认操作变量。在其他语言中,类似的功能通常用来避免书写一些重复的代码。initUI()函数可以重写成如下样式:

function  initUI(){
with (document){ //avoid!
var bd = body,
links = getElementsByTagName_r("a"),
i = 0,
len = links.length;
while(i < len){
update(links[i++]);
}
getElementById("go-btn").onclick = function(){
start();
};
bd.className = "active";
}
}

此重写的initUI()版本使用了一个with表达式,避免多次书写document,这看起来似乎更有效率,而实际上却产生了一个性能问题。
当代码流执行到一个with表达式时,运行期上下文的作用域链被临时改变了。一个新的可变对象将被创建,她包含指定对象的所有属性。此对象被插入到作用域链的前端,意味着现在函数的所有局部变量都被推入第二个作用域链对象中,所以访问代价更高了。

通过将document对象传递给with表达式,一个新的可变对象容纳了document对象的所有属性,被插入到作用域链的前端。这使得访问document的属性非常快,但是访问局部变量的速度却变慢了,例如bd变量。正因为这个原因,最好不要使用with表达式。正如前面提到的,只要简单地将document存储在一个局部变量中,就可以获得性能上的提升。

在JavaScript中不只是with表达式人为地改变运行期上下文的作用域链,try-catch表达式的catch子句具有相同效果。当try块发生错误时,程序流程自动转入catch块,并将异常对象推入作用域链前端的一个可变对象中。在catch块中,函数的所有局部变量现在被放在第二个作用域链对象中。例如:

try {
methodThatMightCauseAnError();
} catch (ex){
alert(ex.message); //scope chain is augmented here
}

但是,只要catch子句执行完毕,作用域链就会返回到原来的状态。

如果使用得当,try-catch表达式是非常有用的语句,所以不建议完全避免。如果你计划使用一个try-catch语句,请确保你了解可能发生的错误。一个try-catch语句不应该作为JavaScript错误的解决办法。如果你知道一个错误会经常发生,那说明应当修正代码本身的问题

你可以通过精缩代码的办法最小化catch子句对性能的影响。一个很好的模式是将错误交给一个专用函数来处理。如:

try {
methodThatMightCauseAnError();
} catch (ex){
handleError(ex); //delegate to handler method
}

handleError()函数是catch子句中运行的唯一代码。此函数以适当方法*地处理错误,并接收由错误产生的异常对象。由于只有一条语句,没有局部变量访问,作用域链临时改变就不会影响代码的性能。