js中有四种基本的数据存取位置。分别是:字面量、本地变量、数组元素、对象成员。
字面量:只代表自身,不存储在特定位置。js的字面量有:字符串、数字、布尔值、对象、数组、函数、正则表达式、以及特殊的null和undefined。
本地变量:开发人员使用关键字var定义的数据存储单元。
数组元素:存储在js数组对象内部,以数字作为索引。
对象成员:存储在js对象内部,以字符串作为索引。
总体来说:字面量和本地变量的访问速度快于数组项和对象成员的访问速度。
管理作用域:
- 作用域链和标示符解析
作用域链:内部属性[[scope]]包含了一个函数被创建的作用域中对象的集合。
函数的作用域链决定了哪些数据能被函数访问。函数作用域中的每个对象被称为一个可变对象,每个可变对象都以“键值对”的形式存在。当一个函数创建后,它的作用域链会被创建此函数的作用域中可访问的数据对象所填充。
例:function add(num1,num2){
var sum = num1+num2;return sum;
}
当函数add()创建时,它的作用域链中插入了一个对象变量,这个全局对象代表着所有在全局范围内定义的变量。
函数add()的作用域将会在执行时用到。假设执行:var total = add(5,10); 执行此函数时会创建一个称为执行环境的内部对象。一个执行环境定义了一个函数执行时的环境。函数执行时对应的执行函数都是独一无二的,所以多次调用同一个函数就会导致创建多个执行环境。当函数执行完毕后,执行环境就被销毁。
每个执行环境都有自己的作用域链,用于解析标示符。当执行环境被创建时,它的作用域链初始化为当前运行函数的[[scope]]属性的对象。这时会创建一个被称为“活动对象”。活动对象作为函数运行时的变量对象,包含了所有局部变量,命名参数,参数集合以及this。同时被推入作用域链的最前端。执行环境销毁后,活动对象也随之销毁。
在函数执行过程中,每遇到一个变量,都会经历一次标示符解析过程以决定从哪里获取或存储数据。而此搜索过程影响了性能。
标示符解析:
一个标示符所在的位置越深,它的读写速度也就越慢。
经验法则:如果某个跨作用域的值在函数中被引用一次以上,那么就把它存储到局部变量里。访问跨作用域的值,需要遍历作用域链往上找,会导致性能的下降,而通过局部变量存储一个跨作用域的值,使得查找次数减少,性能提升。
作用域链的改变:with语句、try-catch语句中的catch子句将临时改变作用域链,创建一个新的变量对象,并推入作用域链的首位,使得函数的所有局部变量处于第二作用域链的对象,导致访问代价提高。
动态作用域:with、try-catch的catch以及eval()被认为是动态作用域。动态作用域只存在于代码执行过程中,因此无法通过静态分析检测。某些优化的js引擎尝试通过分析代码来确定哪些变量可以在特定时候被访问。这些引擎试图避开传统作用域链的查找,取代以标示符索引的方式进行快速查找。而涉及动态作用域时,这种优化方式失效,脚本引擎切换回较慢的基于哈希表的标示符识别方式。
闭包:由于闭包的特性,将导致存在于闭包的[[scope]]属性中,活动对象无法被销毁,故引起更多的内存开销。因此在使用闭包时最需要关注的性能点:在频繁访问跨作用域的标示符时,每次访问都会带来性能损失。闭包的使用关系到内存和执行速度。
- 对象成员
js中的对象是基于原型的。原型是其他对象的基础,它定义并实行了一个新创建的对象所必须包含的成员列表。对象有两种成员类型:实例成员和原型成员。实例成员直接存在对象的实例中,原型成员则从对象的原型继承而来。
而在调用原型成员时,搜索过程会深入原型链中直到找到原型成员。而原型成员所在原型链的位置越深,访问所需时间就越多。
嵌套成员:js每次遇到点操作符时,嵌套成员会导致js引擎搜索所有对象成员。例如:location.href比window.location.href读取速度更快。
注:大部分游览器对于点表示法操作和括号表示法操作并没有明显区别,只有Safari中,点符号始终更快。
由于所有类似的性能问题都与对象成员有关,故因尽可能避免使用它们,或者说只在必要时使用对象成员。例如:多次读取同一个对象属性时,最佳做法是将属性值保存到局部变量以此来避免多次查找带来的性能开销。特别是处理嵌套对象成员时。
注:这种优化方式并不推荐用于对象的成员方法,因为许多对象方法使用this来判断执行环境,把一个对象方法保存在局部变量会导致this绑定到window,而this值的改变会使得js引擎无法正确解析它的对象成员,进而报错。