计算机科学中一个经典的问题是决定如何存储数据,以便进行快速的读取和写入操做。 在代码执行期间,数据如何存储将会关系到它们的检索速度。在Javascript中,因为只存在少数的操做来进行数据存储, 这个问题彷佛 变得简单了。可是,与其余语言同样,Javascript中数据的存储方式将决定它们访问速度。下面是Javascript中能够进行数据存储的四种基本方式: 正则表达式
字面量值(Literal values)
任何仅仅描述自身,且没有被存储在一个特定位置上的值。Javascript能够将字符串,数字,布尔值,对象,数组,函数,正则表达式 以及特殊值null和undefined 做为字面量。编程
对于上述数据存储位置而言,它们每一个都有其特定的读写花费。虽然实际上的性能差别是强烈依赖于代码所运行的浏览器的。 但在大多数状况下,从字面量访问信息与从本地变量访问信息的性能差别是微不足道的。而数组项和对象成员的访问则较昂贵。
数组
虽然某些JS引擎对数组项访问进行了优化,使其能变得更快。但即便如此,一般的建议是尽量的使用字面值和本地变量,并限制数组项和对象成员的使用. 为了达到这个目的,有以下几个模式可用来查找和避免问题,并优化你的代码.
一.管理做用域(Manaing Scope)
在Javascript中, 做用域(Scope)是一个关键的概念。其不只是从性能的角度,并且也从函数的角度解释了各类问题。做用域在Javascript中产生了诸多影响,从肯定函数能够访问那些变量到this上值的分配. 在使用Javascript 做用域的时候,也有一些性能上的考虑。但为了理解其如何关联到速度上,首先须要理解做用域是如何工做的。
浏览器
做用域链(Scope Chain)与标识符解析
Javascript中的每一个函数都被表示成一个对象--更具体的说,是做为函数的实例. 就像其余对象同样,函数对象也能够包含属性(properties),这些属性包括能够编程访问的常规属性以及一系列Javascript引擎所使用到的内部属性。内部属性没法经过代码来访问。其中一个内部属性是在ECMA-262,第三版规范中定义的 [ [Scope] ] 属性.
[ [Scope] ]内部属性包含了函数被建立时表示其所在做用域的对象集合(The internal [[Scope]] property contains a collection of objects representing the scope in which the function was created)。该集合被称为函数的做用域链,它决定了一个函数所能访问到的数据。函数做用域链中的每一个对象都称为可变对象. 每一个可变对象包含一些键值对(Key-Value Pairs). 当一个函数被建立时,它的做用域链会填充一些在其建立环境内能够访问到的数据对象。例如,请考虑下面的全局函数:缓存
function add(num1, num2){ var sum = num1 + num2; return sum; }闭包
当 add() 函数被建立时,他的做用域链将会填充一个单独的可变对象: 即全局范围内包含全部值的全局对象(global object).该全局对象包含了诸如window, navigator 和document等。下图显示了该关系(注意,图中的全局对象只显示了部分属性值,但实际上它还包含了许多其余属性):var total = add(5, 10);函数
执行add函数的时候,将会建立一个称为执行上下文(execution context) 的内部对象。执行上下文定义了函数执行的环境. 每一个执行上下文都是惟一的,因此对相同函数的屡次调用将会产生多个执行上下文。当函数执行完成后,执行上下文将会被销毁。
标识符解析的性能
标识符解析并非不消耗资源的,由于事实上有没哪项计算操做能够不产生性能开销。当在执行上下文的做用域链中进行深度查找时,读写操做将会变得缓慢。所以,本地变量是函数内部访问数据最快的方式。而通常状况下全局变量的访问则是最慢的(优化过的Javascript引擎会在一些条件下优化该过程)。请记住,全局变量老是处于执行上下文的做用域链中最后一个,因此老是产生最多的解析花费。下面2张图显示了标识符在做用域链上不一样深度的解析速度.深度为1则表示本地变量.
读操做:
写操做:
对全部浏览器而言,总的趋势是标识符在做用域链中的位置越深,它的读写操做将会变得更慢。虽然一些优化过Javascript引擎的浏览器,例如Chrome 和 Safari 4 在访问外部做用域(out-of-scope)中的标识符时并无这种性能损耗,然而 IE, Safari 3.2 以及其余浏览器则产生了较大的影响。值得一提的时,一些早期的浏览器,例如IE 6 以及 Firefox 2 将会产生很是大的性能差距.
有了这些信息,咱们最好尽量的使用本地变量来在未优化JS引擎的浏览器中加强性能。一个好的经验是当外部做用域的值在函数中使用了不止一次时,老是将其保存为本地变量。请考虑下面的例子:性能
function initUI(){ var bd = document.body, links = document.getElementsByTagName("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是全局对象,对该对象的搜索将会遍历整个做用域链.你能够经过将document保存为本地变量来减小重复的全局变量访问,进而加强代码的性能.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"; }this
修改事后的initUI() 函数会先使用本地对象来保存document 的引用。而不是原来那样进行3次全局对象的访问。固然,在这个简单的函数中这么作可能并不会显示出巨大的性能加强,但能够想象,在一个大量编码的函数中许多全局变量被重复访问的状况下,该方式将会带来可观的性能加强。
做用域链扩大(Scope Chain Augmentation)
通常来讲,一个执行上下文的做用域链并不会改变。可是,有2个语句能够在函数执行时临时地扩大执行上下文的做用域链。第一个语句是 with.
With 语句能够用来对指定对象的全部属性建立一个默认操做变量。该特性是模仿其余语言中类似的特性。其本意是避免重复编写相同的代码。前面的initUI函数能够被改写为下面这样:
function initUI(){ with (document){ //avoid! var bd = body, links = getElementsByTagName("a"), i= 0, len = links.length; while(i < len){ update(links[i++]); } getElementById("go-btn").onclick = function(){ start(); }; bd.className = "active"; } }
try { methodThatMightCauseAnError(); } catch (ex){ alert(ex.message); // 此处做用域链已被扩大 }
须要注意的是,只要catch子句结束执行,做用域链将会回到前面的状态。try { methodThatMightCauseAnError(); } catch (ex){ handleError(ex); //delegate to handler method }
此处的catch子句中只使用一个handleError()方法来处理. 而handleError能够自由地选择适宜的处理方式。由于此时只包含了单条语句执行而且没有本地变量地访问,临时的做用域链扩大并无影响代码的性能。
动态做用域
with语句和try-catch 中的catch 子句,以及一个包含evel()调用的函数, 均被认为是动态做用域。动态做用域只存在于代码执行期间,所以并不能简单地经过静态分析(查看代码结构)来决定. 例如:
function execute(code) { eval(code); function subroutine(){ return window; } var w = subroutine(); //w 如何取值 };
这里execute()函数使用到了evel()函数, 所以它是一个动态做用域. 此处w的值是否改变是基于参数code的值。在大多数状况下,w将等于全局对象window, 但请考虑下面的代码:execute("var window = {};")
在这种状况下,evel() 在execute() 内部建立了一个名为window的本地变量.因此 w 也最终等于该本地变量而非全局的window. 这种状况在代码执行以前是没法知晓,也意味着标识符window的值没法预先决定.
闭包,做用域与内存
闭包是Javascript最强大的方面之一,它容许一个函数访问其本地做用域以外的数据。闭包的使用已由Douglas Crockford 所写的文章普及,而且在大多数复杂的Web程序中无处不在. 不过,闭包的使用也关联了一些性能影响. 为了理解闭包的性能问题,请考虑下面的代码:
function assignEvents(){ var id = "xdi9592"; document.getElementById("save-btn").onclick = function(event){ saveDocument(id); }; }
assignEvents 函数为DOM元素分配了事件处理器, 这个事件处理器既是一个闭包,由于它是在assignEvents执行时建立的,但可以在其包含范围内访问到外部的id变量.为了使这个闭包访问到id变量, Javascript引擎必须建立一个特殊的做用域链。
二.对象成员(Object Members)
大多数Javascript脚本都使用了面向对象的风格。不管是自定义的对象仍是像DOM或BOW(Browser Object Model)中的嵌入对象。在这些状况下,都会产生许多针对对象成员的访问操做。
在此处,对象成员既指属性,也能够指方法. 在Javascript中,对象的属性和方法之间并无多大的区别。一个对象的命名成员能够包含任何类型的数据。由于函数是被表示为对象的关系,对象成员也能够包含一个函数,就像包含传统数据类型那样。当一个命名成员引用了一个函数时,该成员被称为方法. 而当引用的是非函数的数据类型时,该成员被称为属性。
本文前面曾经讨论过,对象成员的访问是慢于字面量访问和变量访问的。而且在某些浏览器中,它也慢于数组项的访问。为了理解为何会发生这种状况,首先须要理解Javascript中对象的本质。
原型(Prototypes)
Javascript中的对象是基于原型的。原型是一个做为其余对象基础的对象, 它定义并实现了新对象必须拥有的成员。这与传统面向对象编程中”类”的概念是彻底不一样的。OOP中的”类”定义的是建立新对象的处理过程。
对于一个给定的类型,其原型对象被全部的实例所共享,所以全部的实例都可以对原型对象中的成员进行访问。
对象是使用一个内部属性来关联到其原型的。在Firefox, Safari 以及 Chrome中,这个属性被开放为 [_proto_] 属性,并容许开发者访问。但其余浏览器则不容许脚本访问该属性。
因而可知,一个对象自身所包含的成员能够分为两类: 实例成员(也称为”全部(own)”成员) 和原型成员。实例成员直接存在于对象自身,但原型成员则是继承自原型对象.请考虑下面的例子:
var book = { title: "High Performance JavaScript", publisher: "Yahoo! Press" }; alert(book.toString()); //"[object Object]"
在上面的代码中,book对象拥有两个实例成员: title 与 publisher. 请注意这里并无定义toString()方法,但在toString()方法调用时并无出现错误。由于此处的toString()方法是book对象继承的原型成员。下图显示了此关系:var book = { title: "High Performance JavaScript", publisher: "Yahoo! Press" }; alert(book.hasOwnProperty("title")); //true alert(book.hasOwnProperty("toString")); //false alert("title" in book); //true alert("toString" in book); //true
在上面的代码中,由于title 是对象的实例成员,因此当传入”title”给hasOwnProperty方法时,该方法返回true. 而由于toString是一个原造成员,因此传入 “toString” 时返回false. 但当对两者进行in操做时,均返回true. 由于in 操做并不区分实例成员和原型成员。
原型链
对象的原型决定了对象实例的类型。默认状况下,全部的对象均是Object的实例,并所以继承了Object中全部的基础方法。例如 toString(). 你能够经过定义和使用构造式来建立一个新的原型。以下所示:
function Book(title, publisher){ this.title = title; this.publisher = publisher; } Book.prototype.sayTitle = function(){ alert(this.title); }; var book1 = new Book("High Performance JavaScript", "Yahoo! Press"); var book2 = new Book("JavaScript: The Good Parts", "Yahoo! Press"); alert(book1 instanceof Book); //true alert(book1 instanceof Object); //true book1.sayTitle(); //"High Performance JavaScript" alert(book1.toString()); //"[object Object]"
Book构造式用来建立一个新的Book实例。此时book1 实例的原型(_proto_)为Book.prototype.而Book.prototype的原型则是Object. 该过程建立了一个原型链.使得book1和book2继承了该原型链上全部的方法。下图显示了这种关系:
嵌套成员
由于对象的成员能够包含其余成员,因此常常能够见到诸如 window.location.href 这类的Javascript代码。这些嵌套成员致使Javascript引擎在每遇到一个点号(.)后都会进行成员解析处理。下图显示了对象成员深度和访问时间之间的关系:
结果并不令人吃惊,成员的嵌套数越多,其数据访问速度将越慢。所以 location.href 将会快于window.location.href, 类似地,window.location.href 将快于 window.location.href.toString(). 若是这些属性不存在于对象的实例中,成员的解析还将会持续到对象的原型链上。
缓存对象成员的值
因为对象成员关联了以上性能问题,你应该在可能的状况下避免使用它们。更精确地说,你应该只在必要的状况下使用对象成员。例如,在单个函数中是没有理由从成员变量中进行多于一次的访问操做的。
function hasEitherClass(element, className1, className2){ return element.className == className1 || element.className == className2; }
在上面的代码中,对element.className 进行了2次访问。明显地,在这段代码的执行过程当中,className属性的值将不会改变,但此处却产生了2次成员查找的性能开销。你能够经过将属性值保存为本地变量来减小一次查找过程。function hasEitherClass(element, className1, className2){ var currentClassName = element.className; return currentClassName == className1 || currentClassName == className2; }
上面修改后的函数将对成员的查找减小到了1次。由于两次读取的都是相同的属性值,因此值读取一次并将其保存为本地变量是有意义的。在后面对本地变量的访问操做将会快不少。function toggle(element){ if (YAHOO.util.Dom.hasClass(element, "selected")){ YAHOO.util.Dom.removeClass(element, "selected"); return false; } else { YAHOO.util.Dom.addClass(element, "selected"); return true; } }
上面的代码重复了三次 YAHOO.util.Dom 的使用,其以此来获取对不一样方法的访问。对于每一个方法,该操做都产生了3此成员查找。那么总共产生了9次成员查找处理。这使得上述代码效率很低。一个更好的方式是将YAHOO.util.Dom 保存为本地变量,并在以后的操做中访问该本地变量。function toggle(element){ var Dom = YAHOO.util.Dom; if (Dom.hasClass(element, "selected")){ Dom.removeClass(element, "selected"); return false; } else { Dom.addClass(element, "selected"); return true; } }
上面修改后的代码将对成员的查找处理从9次下降到了5次。除了在所需值肯可能变化的状况下,你不该该在单个函数中进行多于一次的对象成员查找。
三.总结
在Javascript中如何存储和访问数据将会对代码的整体性能产生重要的影响。能够从如下4个地方对数据进行访问: 字面量, 变量,数组项 以及对象成员。这些位置均有不一样的性能考虑。
经过使用这些策略,你能够极大地加强Web应用程序的实际性能。对于那些须要大量JavaScript代码的应用而言,性能提高将更加可观。