高性能Javascirpt之数据访问

第二章:数据访问

在Javascript中有四种基本的数据位置:直接量、变量、数组项、对象成员,对一种数据存储位置都具有特定的读写操作负担。总的来说,对直接量和局部变量的访问速度要快于数组项和对象成员的访问速度。
作用域链和标识符解析
函数内部的[[Scope]]属性包含了一个函数被创建的作用域中对象的集合。此集合称为函数的作用域链,它决定哪些数据可由函数访问。此函数作用域链中的每个对象被称为一个可变对象。

function add(num1,num2){
    var sum = num1 + num2;
    return sum;
}   

当add()函数创建后,它的作用域链中填入了一个单独的可变对象,此全局对象代表了所有全局范围定义的变量。此全局变量包含了诸如窗口、浏览器和文档之类的访问接口。
函数作用域链将在运行时用到。如 var total = add(5,10);运行此add函数时,建立一个内部对象,称作”运行期上下文”。一个运行期上下文定义了一个函数运行时的环境。对函数的每次运行而言,每个运行时的上下文都是独一的,所以多次调用同一个函数就会导致多次创建运行期上下文。当函数执行完毕,运行期上下文就会被销毁。

一个运行期上下文有它自己的作用域链,用于标识符解析。当运行期上下文被创建时,它的作用域链被初始化,连同运行函数的[[Scope]]属性中所包含的对象。这些值按照它们出现在函数中的顺序,被复制到运行期上下文的作用域链中。这项工作一旦完成,一个被称作”激活对象”的新对象就为运行期上下文创建好了。
在函数运行过程中,每遇到一个变量,标识符识别过程要决定从哪里获得或者存储数据,此过程搜索运行期上下文的作用域链,查找同名的标识符。搜索工作从运行函数的激活目标之作用域链的前端开始。如果找到后,直接返回,若没有找到则进入作用域链的下一个对象。
在运行期上下文的作用域链中,一个标识符所处的位置越深,它的读写速度就越慢。所以,函数中局部变量的访问速度总是最快的,而全局变量的访问通常是最慢的。全局变量总是处于运行期上下文作用域链的最后一个位置。

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"; 
}  

改变作用域链
一般来说,一个运行期上下文的作用域链不会被改变。但是,有两种表达式可以在运行期临时改变运行期的上下文作用域链。第一个是with表达式。
with表达式为所有对象属性创建一个默认操作变量。在其他语言中通常用来避免书写一些重复代码。当代码流执行到一个with表达式时,运行期上下文的作用域链被临时改变了。一个新的可变对象将创建,它包含指定对象的所有属性。此对象被插入到作用域链的前端,意味着现在函数的所有局部变量都被推入第二个作用域链对象中,所以访问代价更高了。
在Javascript中不只是with表达式人为地改变运行期上下文的作用域链,try-catch表达式的catch子句具有相同的效果。当try块发生错误时,程序流程自动转入catch块,并将异常对象推入作用域链前端的一个可变对象中。在catch块中,函数的所有局部变量现在被放在第二个作用域链对象中。例如:

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

只要catch子句执行完毕,作用域链就会返回到原来的状态。在程序中可以通过精简代码的办法最小化catch子句对性能的影响。一个很好的模式是将错误交给一个专用函数来处理。例子如下:

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

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

动态作用域
无论是with表达式还是try-catch表达式的catch子句,以及包含()的函数,都被认为是动态作用域。一个动态作用域只因代码运行而存在,因此无法通过静态分析(查看代码结构)来确定。

闭包、作用域和内存
闭包是Javascript最强大的一个方面,它允许函数访问局部变量之外的数据。

function assignEvents(){
    var id = "xdi9592";
    document.getElementById("save-btn").onclick = function(event){
       saveDocument(id); 
    };
}   

assignEvents函数为一个DOM元素指定了一个事件处理句柄。此事件句柄是一个闭包,当assignEvents()执行时创建,可以访问其范围内部的id变量。用这种方法封闭了对id变量的访问,必须创建一个特殊的作用域链。
在assignEvents()被执行时,一个激活对象被创建,并包含了一些应有的内容,其中包含id变量。它将成为运行期上下文作用域链上的第一个对象,全局对象是第二个。当闭包创建时,[[Scope]]属性与这些对象一起被初始化。
由于闭包的[[Scope]]属性包含与运行期上下文作用域链相同的对象引用,会产生副作用。通常,一个函数的激活对象与运行期上下文一起销毁。当涉及闭包时,激活对象就无法销毁,因为引用仍存在与闭包的[[Scope]]属性中。这意味着脚本中的闭包和非闭包函数相比,需要更多的内存开销。
当闭包被执行时,一个运行期上下文被创建,它的作用域链与[[Scope]]中引用的两个相同的作用域链同时被初始化,然后一个新的激活对象为闭包自身被创建。

对象成员
对象可以有两种类型的成员:实例成员和原形成员。实例成员直接存于实例自身,而原形成员则从对象原形继承。
一般来说,如果在同一个函数中你要多次读取同一个对象属性,最好将它存入一个局部变量。以局部变量替代属性,避免多余的属性查找带来性能开销。

Fork me on GitHub