JS性能优化——数据存取

首先,了解几个概念:

字面量:它只代表自身,不存储在特定的位置。JavaScript中的字面量有:字符串、数字、布尔值、对象、数组、函数、正则,以及特殊的null和undefined值

本地变量:使用var 定义的数据单元

数组元素:存储在JavaScript数组对象内部,以数字作为索引

对象成员:存储在JavaScript对象内部,以字符串作为索引。

每一种数据存储的位置都有不同的读写消耗。大多数情况下,从一个字面量和局部变量中存取数据的性能差异是微不足道的。访问数组元素和对象成员的代价则要高一些,高多少主要取决于浏览器。

字面量和局部变量的访问速度快于数组项和对象成员的访问速度。

通常的建议是尽量使用字面量和局部变量,减少数字向和对象成员的使用。为此有下边几种模式可优化代码:

管理作用域

每一个JavaScript函数都表示为一个对象,确切的说是Function对象的一个实例。Function对象拥有可编程访问的属性,和一系列不能通过代码访问仅供JavaScript引擎存取的内部属性。其中一个就是[[scope]]即作用域链。它决定了哪些属性能被函数访问。函数作用域中的每个对象都被称为一个可变对象,每个可变对象都以键值对的形式存在。每一个变量都会经历一次标识符解析过程,该过程搜索执行期的作用域链,这个搜索过程会影响性能。

标识符的解析是有代价的,在执行环境的作用域链中,一个标识符所在的位置越深,它的读写速度也就越慢。

因此函数中读写局部变量总是最快的。读写全局变量是最慢的。全局变量总是存在于执行环境作用域链的做末端,是最远的。

建议尽可能的使用局部变量。一个经验法则:如果某个跨作用域的值在函数中被引用了一次以上,那么就把它存储到局部变量里。

function initUI(){
    var bd = document.body,
        links = document.getElementsByTagName("a"),
        i = 0,
        len = links.length;
    ... ...
}

//在方法里多次用到document这个全局对象,可以把它的引用存放在一个局部变量中,让局部变量代替全局变量。访问全局变量的次数由2次变成了1次
function initUI(){
    var doc = document,
        bd = doc.body,
        links = doc.getElementsByTagName("a"),
        i = 0,
        len = links.length;
    ... ...
}

改变作用域链

有两个语句在执行时会改变作用域链。

一个是with语句:with语句避免了多次书写同一个全局变量,但是会产生性能问题。

function initUI(){
  with(document){
     var  bd = body,
         links = getElementsByTagName("a"),
         i = 0,
         len = links.length;
     ... ...
  }
}

当程序执行到with语句时,执行环境的作用域链临时被改变了。一个新的变量对象被创建,它包含了参数指定的所有属性。这个对象被推入作用域链的首位,这意味着函数的所有局部变量现在都位于第二个作用域链对象中,因此访问的代价更高了:访问document对象的属性非常快,而访问局部变量则变慢了,比如bd

另一个是try-catch语句中的catch子句:也具有with同样的效果

try{
  methodThatMightCauseAnError();
}catch(ex){
  alert(ex.message);//作用域链在此处改变
}

try代码块中发生错误,执行过程会自动跳转到catch子句,然后把异常对象推入一个变量对象并置于作用域的首位。在catch代码块内部,函数所有局部变量将会放在第二个作用域链对象中。一旦catch子句执行完毕,作用域链就会返回之前的状态。

try-catch语句是一个非常有用的语句 不建议完全弃用。可以尽量简化代码使catch对子句的影响最小化,可以将错误处理委托给一个函数来处理

try{
  methodThatMightCauseAnError();
}catch(ex){
  handleError(ex);
}

由于只执行一条语句,没有局部变量的访问,作用域链的临时改变就不会影响代码性能。

动态作用域

无论是with语句还是try-catch还是包含eval()的函数,都被认为是动态作用域。动态作用域只存在于代码执行过程中,因此无法通过静态分析(查看代码结构)检测出来。

经过优化的JavaScript引擎。比如safari的Nitro引擎,尝试通过分析代码来确定哪些变量可以在特定的时候被访问。这些引擎试图避开传统作用域链的查找,用标识符索引的方式进行快速查找来代替。但是当遇到动态作用域时就失效了,脚本引擎必须切换回较慢的基于哈希表的标识符识别方式,这更像是传统的作用域链查找。

推荐:只有在确实有必要时才使用动态作用域。

闭包、作用域和内存

由于闭包的[[scope]]属性包含了与执行环境作用域链相同的对象的引用,因此函数的激活对象不会随着执行环境一同销毁。这意味着脚本中的闭包和非闭包函数相比,需要更多的内存开销。在大型WEB应用中,这可能是个问题。尤其是IE浏览器中需要关注,由于IE使用非原生JavaScript对象来实现DOM对象,因此闭包会导致内存泄漏。

脚本编程中,最好小心的使用闭包,它同时关系到内存和执行速度。

对象成员

对象在原型链中存在的位置越深,找到它也就越慢。每深入一层原型链都会增加性能损失,搜索实例成员比从字面量或局部变量中读取数据代价更高,并且还有遍历原型链带来的开销,这些让性能问题更为严重。

嵌套成员,即点操作符:window.location.href。每次遇到点操作符,嵌套成员会导致JavaScript引擎搜索所有对象成员。

对象成员嵌套的越深,读取速度就会越慢。执行location.href总是比window.location.href要快,后者也比window.location.href.toString()要快。如果这些属性不是对象的实例属性,那么成员解析还需要搜索原型链,这会花更多的时间。

大部分浏览器中,通过点表示法(object.name)操作和通过括号表示法(object["name"])操作并没有明显的区别。只有在Safari中,点符号始终会更快,但这并不意味着不要用括号符号。

缓存对象成员值

所有类似的性能问题都与对象成员有关,因此应该尽可能避免使用他们,或者应该说,只在必要时使用对象成员。(在同一个函数中没有必要多次读取同一个对象成员)

通常来说,如果函数中要多次读取同一个对象的属性,最佳做法是讲=将属性值保存到局部变量中。局部变量能用来替代属性以避免多次查找带来的性能开销。特别是在处理嵌套对象成员时,这样做会明显提升执行速度。

不要再同一个函数中多次查找同一个对象成员,除非它的值改变了。

总结:在JavaScript中,数据存储的位置会对代码整体性能产生重大的影响。数据存储共有4种方式:字面量、变量、数组项、对象成员。

1、访问字面量和局部变量的速度最快,相反访问数组元素和对象成员相对较慢。

2、由于局部变量存在于作用链的起始位置,因此访问局部变量比访问跨作用域变量更快。变量在作用域链中的位置越深,访问所需要时间就越长。由于全局变量总处在作用域链的最末端,因此访问速度也是最慢的

3、避免使用with语句,因为它会改变执行环境作用域链。同样的try-catch语句中的catch子句也有同样的影响,因此要小心使用。

4、嵌套的对象成员会明显影响性能,尽量少用。

5、属性或方法在原型链中的位置越深,访问它的速度也越慢。

6、通常来说,你可以把常用的对象成员、数组元素、跨域变量保存在局部变量中来改善JavaScript性能,因为局部变量访问速度更快。

原文地址:https://www.cnblogs.com/liuyanan/p/10875661.html