您的位置:首页 > Web前端 > JavaScript

高性能Javascript【二】数据访问

2011-09-01 21:19 155 查看
【本文系外部转载,原文地址:http://www.hotels2map.com/blog/?p=116

程序运行的快慢和程序运行中数据的读写分不开,也就是数据的访问,而数据的读写效率很大程度上取决于数据的存取位置。

Javascript中常见的基本数据存储有:直接量、变量、数组元素、对象成员,访问直接量和局部变量的速度最快,数组元素和对象成员相对较慢。

直接量只代表她本省,不存在任何特定位置,因此他的访问速度是最快的,马上返回数据。
变量是用var定义的数据存储单元,她的访问速度和他的变量的作用域有关,局部变量的访问速度和直接量接近,也是非常快的。
数组元素和对象成员,因为需要根据索引来在数组或者对象中来查找,而且对象存在原型链,因此访问的速度要比直接量和变量慢,其速度和具体的浏览器有关。

可以通过以下方法来优化数据访问的性能:

可以通过把常量的对象成员、数组元素、跨域变量保存在局部变量中来,因为局部变量访问速度更快。

http://www.nczonline.net/blog/2009/02/10/javascript-variable-performance/
控制原型链的深度。
减少对象的嵌套成员。
缓存对象成员。


管理作用域和理解对象原型,是优化数据的访问速度的关键。


作用域链(scope chain)


作用域不仅关系到变量的作用范围、this的指向等,也和性能有直接的关系。每一个Javascript函数都有一个内部属性[[scope]],因为函数是对象(一切皆对象),[[scope]] 包含函数创建时的作用域(scope)信息,是一个对象列表,这个集合就是作用域链,作用域链用于标识符的解析,他决定了哪些数据能被函数访问,如何访问;

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

函数创建时的作用域链
当一个函数创建的时候,作用域链就会被一系列的对象填充,作用域链中的每一个对象被称为一个可变对象,这些对象表示了在此函数作用域内哪些数据可以被访问到。比如上面的add函数,创建的时候,作用域链中被被唯一的一个可变对象填充:全局对象,包括document、window等被全局定义的变量;
var total = add(5,10);

函数执行的作用域链
当一个函数执行的时候,会创建一个“执行期上下文”(execution content),作用域链初始化,创建“活动对象”(activation object),并且把活动对象添加到作用域链的头部,活动对象包含了局部变量、命名参数、参数几何以及this。
当add(5,10)函数执行的时候,一个执行期上下文被创建,他的作用域链被初始化为函数add(5,10)的[[scope]]属性中的对象,形成活动对象,并把活动对象添加到作用域链的头部,这时候作用域链中就有了2个对象:活动对象,全局对象。
标识符解析(identifier resolution)
在函数执行过程中,每遇到一个变量,都会经历一次标识符解析的过程,来决定从哪里获取数据和把数据存储到哪里。
标识符解析的过程会搜索执行期上下文的作用域链,搜索的过程是从作用域链的头部开始的,也就是从活动对象开始,如果在作用域链的活动对象中找到了需要的标识符就返回,没有则进入作用域链的第二个对象,如此推进,知道找到为止,如果所有的对象中都没有找到,则认为未定义,这个不断搜索的过程是影响性能的关键。
比如上面的add(5,10),执行的时候,遇到num1的时候,会先作用域链的活动对象中找,因为num1是参数,存在于活动对象中,马上就会被返回,如果没有,就会继续进行作用域链搜索,到全局对象中去找。
在执行期上下文的作用域链中,标识符所在的对象的位置越靠后,需要搜索的路径就越长,读写操作的速度就越慢,所以局部变量的读写通常都是最快的,因为他在作用域链的最前面,饿全局变量是最慢的,因为他在作用域链的最后面
所以优化性能的时候,尽量使用局部变量,如果一个跨作用域的变量被引用一次以上,可以把他存为一个局部变量
function initUI(){
var doc = document,
bd = doc.body,
links = doc.getElementsByTagName("a"),
i= 0,
len = links.length;
while(i < len){
update(links[i++]);
}
doc.getElementById("go-btn").onclick = function(){
start();
};
bd.className = "active";
}


改变作用域链

with和try-catch可以改变作用域链,with运行的时候,一个新的属于with的可变对象会被添加到作用域链的头部,形成with的可变对象->活动对象->其他对象的结构,这时标志符解析的速度会变慢,影响性能,try-catch中的catch子句也类似。
此外还有eval函数,这三个都被认为是动态作用域,他们作用域链和具体的环境和代码有关,会致标识符的解析无法预知,因此非必要时不提倡使用。


闭包、作用域和内存

闭包允许函数访问局部作用域之外的数据,当某个函数中有闭包时,这个函数和闭包会引用作用域链中相同的活动对象,导致这个函数执行完之后,他的活动对象不会马上销毁,会消耗更多的内存,在ie中还会导致内存泄漏。
此外,闭包执行的时候,会产生2个活动对象,一个是闭包本身的活动对象,在作用域链的第一位(下面代码中的this),一个是包含闭包的函数的活动对象,在作用域链的第二位(下面代码中的变量id),之后才是其他的可变对象,而闭包一般都需要在第二个活动对象中获取需要的数据,这样标识符解析的过程会比直接冲第一个活动对象获取慢,导致性能的损失。闭包影响到内存和性能速度,需要小心使用。
function assignEvents(){
var id = "xdi9592";
document.getElementById("save-btn").onclick = function(event){
saveDocument(id);
};
}


原型

Javascript中的对象是基于原型的,一旦创建一个对象实例,她会自动拥有一个Object实例作为原型,许多的属性和方法通过原型自动继承而来的,因此对象成员有2中类型:实例成员和原型成员。
var book = {
title : "High Performance Javascript"
}
alert(book instanceof Object);//true book是Object的一个实例
alert(book.title); //High Performance Javascript
alert(book.toString());//[object Object]
//http://www.nczonline.net/blog/2010/07/27/determining-if-an-object-property-exists/
alert(book.hasOwnProperty("title")); //true
alert(book.hasOwnProperty("toString")); //false
alert("title" in book); //true
alert("toString" in book); //true

这里的title是book的实例成员(可以通过hasOwnProperty方法来判断),而toString是book的原型成员,如果需要确定某个对象是否(包括实例成员和原型成员)拥有某个成员可以试用in操作。


原型链

对象和原型之间是通过__proto__属性来自动绑定的,__proto__属性是不可修改的,但是可以通过prototype构造器来创建另一种类型的原型:
function Book(title){
this.title = title;
}
Book.prototype.sayTitle = function(){
alert(this.title);
}
//book1的原型(__proto__)是Book.prototype,Book.prototype的原型是Object,形成一个原型链
var book1 = new Book("High Performance Javascript");
var book2 = new Book("Javascript: The Good Parts");
//通过prototype创建的原型成员,可以被所有的实例对象继承,即使prototype的原型成员是在实例对象创建之后声明的。
Book.prototype.sayTest = function(){
alert("test=" + this.title);
}
alert(book1 instanceof Book);//true
alert(book1 instanceof Object);//true
book1.sayTest();//High Performance Javascript
alert(book1.toString());//[object Object]


控制原型链的深度
由于原型的这种自动继承机制,一个对象有她的原型对象(一级原型),原型对象又可能有她自己的原型(二级原型)…如此循环;于是当你访问某个实例对象的某个属性的时候,这个属性肯是她的实例成员,也可能是一级原型成员,也可能是二级原型成员,也可能是三级原型成员…可以不断的网上追溯。但是这个属性在原型链上的位置越深,找到她需要经历的路径就越长,访问对象的实例成员的速度本来就没有直接量和变量快,如果再加上原型链的追溯、遍历,速度就更加的慢了。


减少对象的嵌套成员

因为每次遇到点操作,Javascript都会搜索所有的对象成员,如果需要的某个成员嵌套的很深的话,会导致多次搜索,速度也就越慢。因此location.href总是比window.location.href要快。


缓存对象成员

如果对象成员的值没有变化的话,可以用变量把对象成员缓存起来,减少对对象成员的访问次数。
function hasEitherClass(ele,cls1,,cls2){
return ele.className == cls1 || ele.className == cls2;
}

可以写成这样:
function hasEitherClass(ele,cls1,,cls2){
var curClass = ele.className;//减少对ele对象的成员className的访问
return curClass == cls1 || curClass == cls2;
}


tips:

如果一个相同的变量存在于作用域链的不同部分,那么会返回遍历作用域链时最先找到的那个,某些情况可以理解为局部变量会覆盖全局变量。
在大部分的浏览器中用object.name和object["name"]来访问对象成员的速度区别不大,只有在safari中点号始终要快,但是如果成员的名称是变量的时候,中括号才是硬道理。
缓存对象成员的方法适用于对象的属性,不适用与对象的方法,尤其是这个对象的方法中有使用this关键字的时候。

每次看到作用域和闭包相关的理论时都比较晕,以上有些属于个人理解,也可参考下面的文章。

JavaScript对象模型-执行模型:http://www.cnblogs.com/RicCC/archive/2008/02/15/javascript-object-model-execution-model.html
理解 JavaScript 闭包:http://www.cn-cuckoo.com/2007/08/01/understand-javascript-closures-72.html
学习Javascript闭包(Closure):http://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.htm
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: