高性能JavaScript(2)

在JavaScript中,数据存储的位置对代码的整体性能会有重大影响。数据存储共有4种方式:字面量、变量、数组项、对象成员,它们有着各自特点,访问字面量和局部变量的速度最快,相反访问数组项和对象成员相对较慢。

作用域链

作用域确定了JavaScript中变量的访问范围。一个函数被创建,就创建了一个作用域。函数有一个内部属性[[Scopes]],其包含了函数被创建的作用域中的对象集合,这个集合就被称为作用域链。作用域链决定了哪些数据能被函数访问。

任意一个函数被创建,首先插入作用域链一个全局对象,包含windowdocument等全局变量。在函数执行时,又会创建一个执行环境的内部对象,它的作用链初始化为函数的[[Scopes]]中的对象,之后又会创建一个活动对象,该对象包含了所有局部变量,命名参数,以及this,然后此对象被压入作用链的最顶端,当执行结束,该对象也随之销毁。

函数执行过程中,每遇到一个变量,都会搜索执行环境的作用域链,也正是这个搜索过程影响到了性能。因为链式存储,随着变量在作用域链位置的加深,性能的消耗也就越大。因此不难理解,局部变量的访问速度是最快的,全局变量由于在作用域链的最末端,通常访问最慢。

因此,对于访问频繁的全局变量,我们可以通过将引用存储在一个局部变量中,使用这个局部变量代替全局变量。

改变作用域链

一般情况下,执行环境的作用链是不会改变的,但是有两种情况下可以临时改变。

第一个是with语句,with语句用来给对象的所有属性创建了一个变量。这时,会创建一个变量对象,包含了指定对象的所有属性。对象被插入作用链顶端,意味着之前的作用域被后移一位,局部变量处于第二个位置,访问的代价更高了。

第二个是try-catch语句,在发生异常的情况下,会自动跳转到catch子句,然后异常对象推入一个变量对象到作用域链首位。

不管是witch还是try-catch,还有不在此部分讨论的eval函数,都被认为是动态作用域链。特点就是,动态作用域只存在函数执行过程中,不能被浏览器的静态分析优化(浏览器引擎通过分析代码,确定哪些变量可以在特定时候被访问,希望用标识符索引访问的方式代替作用链查找的哈希表识别方式)。

综合上述两点,这也就是为什么不鼓励使用with语句的原因。

闭包

闭包是JavaScript的一个特性,允许函数访问局部作用域之外的数据。闭包会创建一个特定的作用域链,在全局对象之上,[[Scopes]]还包括一个外层函数的活动对象,也可以理解为,闭包的[[Scopes]]被初始化为外层函数的执行环境的作用域链。

带来的问题就是,由于[[Scopes]]属性包含了与执行环境作用域链相同的对象的引用,因此执行环境中局部变量不会被销毁。另外,当闭包执行时,创建的执行环境作用域链会继续增加一个活动对象到顶端,也就是说在访问外层函数的变量时,需要进行一次查找,带来性能消耗。

正如文章开头说的,局部变量的访问速度最快,因此我们在编写代码的时候,应该尽可能将对象成员、数组元素、跨域变量存储在一个局部变量中,实现性能的改善。