计算机科学中一个经典的问题是决定如何存储数据,以便进行快速的读取和写入操作。 在代码执行期间,数据如何存储将会关系到它们的检索速度。在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"; }
修改过后的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代码的应用而言,性能提升将更加可观。
JavaScript 数据访问(通译自High Performance Javascript 第二章) [转],布布扣,bubuko.com
JavaScript 数据访问(通译自High Performance Javascript 第二章) [转]
原文:http://www.cnblogs.com/anorthwolf/p/3810363.html