JavaScript 数据访问(通译自High Performance Javascript 第二章) [转]

JavaScript 数据访问(通译自High Performance Javascript 第二章)

提问者:lilei335260(ID:160310) | 悬赏 0.0 希赛币 | 回答数:12 | 关注度:32 | 提问时间:2014-05-03
 
JavaScript 数据访问(翻译自High Performance Javascript 第二章)

  计算机科学中一个经典的问题是决定如何存储数据,以便进行快速的读取和写入操做。 在代码执行期间,数据如何存储将会关系到它们的检索速度。在Javascript中,因为只存在少数的操做来进行数据存储, 这个问题彷佛 变得简单了。可是,与其余语言同样,Javascript中数据的存储方式将决定它们访问速度。下面是Javascript中能够进行数据存储的四种基本方式: 正则表达式

  •   字面量值(Literal values)
      任何仅仅描述自身,且没有被存储在一个特定位置上的值。Javascript能够将字符串,数字,布尔值,对象,数组,函数,正则表达式 以及特殊值null和undefined 做为字面量。编程

  • 变量
      任何开发者使用var关键字定义的数据存储位置.
  • 数组项
      Javascript数组中使用数字进行索引的位置
  • 对象成员
      Javascript对象中使用字符串进行索引的位置.

  对于上述数据存储位置而言,它们每一个都有其特定的读写花费。虽然实际上的性能差别是强烈依赖于代码所运行的浏览器的。 但在大多数状况下,从字面量访问信息与从本地变量访问信息的性能差别是微不足道的。而数组项和对象成员的访问则较昂贵。

数组

  670x474

  虽然某些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等。下图显示了该关系(注意,图中的全局对象只显示了部分属性值,但实际上它还包含了许多其余属性):

    563x169
    add 函数的做用域链将会在其执行时用到。例如假设运行如下代码:

      var total = add(5, 10);函数

      执行add函数的时候,将会建立一个称为执行上下文(execution context) 的内部对象。执行上下文定义了函数执行的环境. 每一个执行上下文都是惟一的,因此对相同函数的屡次调用将会产生多个执行上下文。当函数执行完成后,执行上下文将会被销毁。
      一个执行上下文自身也包含了做用域链,该做用域链将用来进行标识符解析。当执行上下文建立时,首先会把其执行函数的[ [Scope] ] 属性中的对象复制到自身的做用域链中。该复制过程将会以对象在 [ [Scope] ]属性中出现的位置依次进行。当该过程完毕后,将会为执行上下文建立一个称为激活对象(activation object)的新对象. 该激活对象包含了全部的本地变量,命名参数, 参数集合(arguments)以及this。接着,激活对象将会被推入做用域链的最顶端,做为该次执行中的可变对象。当执行上下文被销毁时,该激活对象也同时销毁。下图显示了前面代码中的执行上下文和做用域链.

    645x407
      在函数执行时,每遇到一个变量,将会产生一个标识符解析的过程,该过程将决定数据检索和存储的位置。在这个过程当中,将会在执行上下文的做用域链中查找一个与变量名称相同的标识符. 查找将会从做用域链的顶端开始(即激活对象),依次遍历做用域链。当找到相同名称的标识符时,将使用该标识符。而当遍历完整个做用域链后均没有找到标识符时,标识符将会被就看作是未定义的(undefined). 函数执行时,每一个标识符的查找都将经历上面的过程.之前面的例子来讲,add函数中的sum, num1 和 num2 将会产生这一查找过程。而正是这个搜索过程影响了性能。
      注意在做用域链中不一样的部分可能会存在两个名称相同的变量。此时,标识符解析将会以首先找到的对象为准。然后面部分中的对象将会被遮蔽(shadow).

     

  •   标识符解析的性能
      标识符解析并非不消耗资源的,由于事实上有没哪项计算操做能够不产生性能开销。当在执行上下文的做用域链中进行深度查找时,读写操做将会变得缓慢。所以,本地变量是函数内部访问数据最快的方式。而通常状况下全局变量的访问则是最慢的(优化过的Javascript引擎会在一些条件下优化该过程)。请记住,全局变量老是处于执行上下文的做用域链中最后一个,因此老是产生最多的解析花费。下面2张图显示了标识符在做用域链上不一样深度的解析速度.深度为1则表示本地变量.

    读操做:
      596x536

    写操做:

      595x533
      
      对全部浏览器而言,总的趋势是标识符在做用域链中的位置越深,它的读写操做将会变得更慢。虽然一些优化过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"; } }


      这个修改后的initUI函数使用了with语句来避免处处使用document引用。虽然这看起来彷佛更有效率,但它实际上却产生了一个性能问题.
      当代码执行进入with语句时,执行上下文的做用域链将会临时地扩大。这将产生一个新对象,该新对象包含了with语句所指定对象的全部属性值。接着该新对象将会被推入做用域链的顶端.这也意味着此时函数中全部的本地变量变成了做用域链中的次级节点. 所以带来了额外的访问开销.

    702x603

      当将document对象传递进with语句时,一个新的包含了document全部属性的可变对象将会被推入做用域链的顶端。这会使对document的访问变得更快可是却下降了对本地变量的访问。处于这个理由,最好避免使用with语句,而是使用前面所述的只是简单地将document存储在一个本地变量中,并以此得到性能上的提高.

      With语句并非Javascript中惟一一个能够扩大执行上下文的做用域链的方法。Try-catch语句中的catch 子句也会产生相同的效果。当try块中出现一个错误时,执行将会自动地转入catch块且异常对象将会被推入一个可变对象,并放在做用域链的顶端。所以在catch块中,函数中全部的本地变量将会变成次级做用域对象。

      try { methodThatMightCauseAnError(); } catch (ex){ alert(ex.message); // 此处做用域链已被扩大 }

      须要注意的是,只要catch子句结束执行,做用域链将会回到前面的状态。
    在使用得当的状况下,try-catch 语句是很是有用的。因此彻底避免使用try-catch 是没有意义的。若是你正准备使用try-catch语句,请确保你理解了出错的可能性。Try-catch 语句不该该做为Javascript错误的解决方案。若是你已经知道了一个错误常常出现,那么说明代码自己出现了问题,而且应该被修正。
      你能够经过只在catch 子句中执行少许必要的代码来最小化性能冲击。一个好的模式是在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引擎,例如Safari’s Nitro 试图经过分析代码中给定时间内那些变量能够被访问来加速标识符解析。这些引擎尝试使用索引标识符的方式来加速解析过程,并避免采用传统的做用域链查找. 可是,当出现一个动态做用域时,这些优化技术将变得无效,Javascript引擎不得不切换回较慢的哈希查找方式来进行标识符解析。这更相似于传统的做用域链查找。
    处于这个理由,只应该在绝对必要的状况下使用动态做用域。

     

  •   闭包,做用域与内存
      闭包是Javascript最强大的方面之一,它容许一个函数访问其本地做用域以外的数据。闭包的使用已由Douglas Crockford 所写的文章普及,而且在大多数复杂的Web程序中无处不在. 不过,闭包的使用也关联了一些性能影响. 为了理解闭包的性能问题,请考虑下面的代码:

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

    assignEvents 函数为DOM元素分配了事件处理器, 这个事件处理器既是一个闭包,由于它是在assignEvents执行时建立的,但可以在其包含范围内访问到外部的id变量.为了使这个闭包访问到id变量, Javascript引擎必须建立一个特殊的做用域链。
    当assignEvents执行时,将会建立一个只包含ID变量的激活对象。该激活对象将会成为执行上下文的做用域链中的第一个对象,全局对象后处在第二位。当闭包被建立时,它的 [ [Scope] ]属性将被初始化赋值为与外部执行上下文相同的对象.以下图所示:

    693x408
      由于闭包的 [ [Scope]]属性包含了其外部环境中对象引用,这也产生了一个负面影响。典型状况下,一个函数的激活对象在其执行上下文销毁时随之销毁,但当有闭包存在时,激活对象并没及时销毁,由于此时闭包的[ [Scope] ]属性仍然保存这一个对该对象的引用.这也意味着,使用闭包的函数相对于非闭包函数将会带来更多的内存开销。对于大型Web应用而言,这将会是一个问题。 对于IE而言更是如此。 IE将DOM对象做为非本地Javascript对象来实现,于是使用闭包时将会可能产生内存泄露。
      当闭包被执行时,将会建立一个执行上下文. 该执行上下文的做用域链将会被初始化为其[ [Scope] ]属性中所引用的对象(此处为2个)。且一个针对闭包自身的激活对象将会被建立. 以下图所示:

    693x517
      请注意,在闭包中所使用到的id和saveDocument等标识符,此时均处于做用域链的尾部,这也是闭包性能问题的主要关注点: 你常常访问一个最远距离的标识符,并所以带来了性能损失。
      在编写你本身的脚本时,最好时刻关注闭包的使用,由于他们可能带来内存泄露和执行速度的问题。可是,你能够经过本文前面所讨论的,将外部做用域中经常使用到的变量存储为本地变量来下降对执行速度的影响。

     

  二.对象成员(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对象继承的原型成员。下图显示了此关系: 

      527x260
    对象成员的解析过程很是类似于变量的解析。当book.toString()被调用时,将会首先从对象自身搜索一个名为”toString”的成员,当没有找到时,将会继续搜索对象的原型对象.而在原型对象中,将会找到并执行这个toString方法。经过这种方式,book对象能够访问其原型对象上的每一个属性或方法。
     你可使用hasOwnProperty 方法来肯定一个对象是否拥有所给名称的实例成员。当须要肯定对象是否能够访问所给名称的成员时(不管是实例成员仍是原型成员),可使用 in 操做。 以下所示:

      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继承了该原型链上全部的方法。下图显示了这种关系:

    721x344
      注意此时Book的2个实例均是共享相同的原型链。每一个实例拥有本身的title和publisher属性,但其余全部的属性都是继承而来的。
      如今当book1.toString()被调用时,搜索过程必须深刻到原型链的最底层(Object)处来解析toString. 如你所料,成员属性在原型链中的位置越深,它的查找速度将会越慢。下图显示了原型链中成员深度与访问时间的关系:

    588x458
      虽然优化过Javascript引擎的浏览器在执行时表现很好,但较老的浏览器,例如IE和Firefox 3.5 将会随着原型链查找深度的增长而带来性能损耗。请注意,原型链中成员的查找过程将会比从字面量或变量中访问数据更昂贵。而对原型链进行遍历将会扩大这种差距.

     

  •   嵌套成员
      由于对象的成员能够包含其余成员,因此常常能够见到诸如 window.location.href 这类的Javascript代码。这些嵌套成员致使Javascript引擎在每遇到一个点号(.)后都会进行成员解析处理。下图显示了对象成员深度和访问时间之间的关系:

    582x505
      结果并不令人吃惊,成员的嵌套数越多,其数据访问速度将越慢。所以 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次。由于两次读取的都是相同的属性值,因此值读取一次并将其保存为本地变量是有意义的。在后面对本地变量的访问操做将会快不少。
      通常来讲,若是你在一个函数中屡次访问了对象的属性,最好将该属性保存为本地变量。在随后的处理中使用这个本地变量来代替对属性的访问。以此来避免查找过程所带来的性能开销。这在处理嵌套对象成员时尤为重要,它将会对执行速度产生可观的影响。
    Javascript命名空间,例如YUI中所使用的技术,是常常进行嵌套属性访问的来源,以下所示:

      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个地方对数据进行访问: 字面量, 变量,数组项 以及对象成员。这些位置均有不一样的性能考虑。

  • 访问字面量以及本地变量的速度是很是快的,数组项和对象成员的访问则较慢。
  • 本地变量的访问将快于外部范围内的变量。由于本地变量存在于函数做用域链中的第一个可变对象内(激活对象). 变量在做用域链中的位置越深。其访问的时间也就越长。由于全局变量处在做用域链中的最后位置,因此对它的访问老是最慢的。
  • 避免使用with语句,由于他们扩大了执行上下文的做用域链。同时,也须要注意try-catch语句中的catch子句,它也会产生相同的效果。
  • 嵌套对象成员遭受着重大的性能影响,应当最小化它的使用.
  • 属性或方法在原型链中的位置越深,访问它们的速度亦越慢。
  • 通常来讲,你能够经过将常用的对象成员,数组项以及外部变量保存为本地变量来加强代码的性能。针对本地变量的访问将快于原始的访问方式。

  经过使用这些策略,你能够极大地加强Web应用程序的实际性能。对于那些须要大量JavaScript代码的应用而言,性能提高将更加可观。

相关文章
相关标签/搜索