接上一篇,希望能写一个高性能Javascript专题。
经典计算机科学的一个问题是,数据应当存放在什么地方,以实现最佳的读写效率。数据存储是否得当,关系到代码运行期间数据被检索到的速度。在Javascript中,此问题相对简单,因为数据表现方式只有少量方式可供选择。在Javascript中,有四种基本的数据访问位置:
- Literal values 直接量
- 直接量仅仅代表自己,而不存储于特定的位置。
- Javascript的直接量包括:字符串(strings)、数字(numbers)、布尔值(booleans)、对象(objects)、数组(arrays)、函数(functions)、正则表达式(regular expressions),具有特殊意义的空值(null),以及未定义(undefined)。
- Variables 变量
- 开发人员用var关键字创建用于存储数据值。
- Array items 数组项
- 具有数字索引,存储一个Javascript数组对象。
- Object members 对象成员
- 具有字符串索引,存储一个Javascript对象。
每一种数据存储位置都具有特定的读写操作负担。在大多数情况下,对一个直接量和一个局部变量的数据访问的性能差异是微不足道的。具体而言,访问数组项和对象成员的代价要高一些,具体高多少,很大程度上取决于浏览器。一般的建议是,如果关心运行速度,那么尽量使用直接量和局部变量,限制数组项和对象成员的使用。为此,有如下几种模式,用于避免并优化我们的代码:
Managing Scope 管理作用域
作用域概念是理解Javascript的关键,无论是从性能还是功能的角度而言,作用域对Javascript有着巨大影响。要理解运行速度与作用域的关系,首先要理解作用域的工作原理。
Scope Chains and Identifier Resolution 作用域链和标识符解析
每一个Javascript函数都被表示为对象,它是一个函数实例。它包含我们编程定义的可访问属性,和一系列不能被程序访问,仅供Javascript引擎使用的内部属性,其中一个内部属性是[[Scope]],由ECMA-262标准第三版定义。
内部[[Scope]]属性包含一个函数被创建的作用域中对象的集合。此集合被称为函数的作用域链,它决定哪些数据可以由函数访问。此函数中作用域链中每个对象被称为一个可变对象,以“键值对”表示。当一个函数创建以后,它的作用域链被填充以对象,这些对象代表创建此函数的环境中可访问的数据:
1 2 3 4 |
function add(num1, num2){ var sum = num1 + num2; return sum; } |
当add()函数创建以后,它的作用域链中填入了一个单独可变对象,此全局对象代表了所有全局范围定义的变量。此全局对象包含诸如窗口、浏览器和文档之类的访问接口。如下图所示:(add()函数的作用域链,注意这里只画出全局变量中很少的一部分)
add函数的作用域链将会在运行时用到,假设运行了如下代码:
1 |
var total = add(5,10); |
运行此add函数时会建立一个内部对象,称作“运行期上下文”(execution context),一个运行期上下文定义了一个函数运行时的环境。且对于单独的每次运行而言,每个运行期上下文都是独立的,多次调用就会产生多此创建。而当函数执行完毕,运行期上下文被销毁。
一个运行期上下文有自己的作用域链,用于解析标识符。当运行期上下文被创建的时,它的作用域被初始化,连同运行函数的作用域链[[Scope]]属性所包含的对象。这些值按照它们出现在函数中的顺序,被复制到运行期上下文的作用域链中。这项工作一旦执行完毕,一个被称作“激活对象”的新对象就位运行期上下文创建好了。此激活对象作为函数执行期一个可变对象,包含了访问所有局部变量,命名参数,参数集合和this的接口。然后,此对象被推入到作用域链的最前端。当作用域链被销毁时,激活对象也一同被销毁。如下所示:(运行add()时的作用域链)
在函数运行的过程中,每遇到一个变量,就要进行标识符识别。标识符识别这个过程要决定从哪里获得数据或者存取数据。此过程搜索运行期上下文的作用域链,查找同名的标识符。搜索工作从运行函数的激活目标的作用域前端开始。如果找到了,就使用这个具有指定标识符的变量;如果没找到,搜索工作将进入作用域链的下一个对象,此过程持续运行,直到标识符被找到或者没有更多可用对象可用于搜索,这种情况视为标识符未定义。正是这种搜索过程影响了性能。
Identifier Resolution Performance 标识符识别性能
标识符识别是耗能的。
在运行期上下文的作用域链中,一个标识符所处的位置越深,它的读写速度就越慢。所以,函数中局部变量的访问速度总是最快的,而全局变量通常是最慢的(优化Javascript引擎,如Safari在某些情况下可用改变这种情况)。
请记住,全局变量总是处于运行期上下文作用域链的最后一个位置,所以总是最远才能被访问的。一个好的经验法则是:使用局部变量存储本地范围之外的变量值,如果它们在函数中的使用多于一次。考虑下面的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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"; } |
此函数包含三个对document的引用,而document是一个全局对象。搜索至document,必须遍历整个作用域链,直到最后才能找到它。使用下面的方法减轻重复的全局变量访问对性能的影响:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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"; } |
用doc代替document更快,因为它是一个局部变量。当然,这个简单的函数不会显示出巨大的性能改进,因为数量的原因,不过可以想象一下,如果几十个全部变量反复被访问,那么性能改进将显得多么出色。
Scope Chain Augmentation 改变作用域链
一个来说,一个运行期上下文的作用域链不会被改变。但是,有两种表达式可以在运行时临时改变运行期上下文。第一个是with表达式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
function initUI(){ with (document){ //avoid! var bd = body, links = getElementsByTagName("a"), links = getElementsByTagName("a"),Ԣ到的速度。在Javascript中,此问题相对简单,因为数据表现方式只有少量方式可供选择。在Javascript中,有四种基本的数据访问位置:
每一种数据存储位置都具有特定的读写操作负担。在大多数情况下,对一个直接量和一个局部变量的数据访问的性能差异是微不足道的。具体而言,访问数组项和对象成员的代价要高一些,具体高多少,很大程度上取决于浏览器。一般的建议是,如果关心运行速度,那么尽量使用直接量和局部变量,限制数组项和对象成员的使用。为此,有如下几种模式,用于避免并优化我们的代码:
Managing Scope 管理作用域 作用域概念是理解Javascript的关键,无论是从性能还是功能的角度而言,作用域对Javascript有着巨大影响。要理解运行速度与作用域的关系,首先要理解作用域的工作原理。
Scope Chains and Identifier Resolution 作用域链和标识符解析 每一个Javascript函数都被表示为对象,它是一个函数实例。它包含我们编程定义的可访问属性,和一系列不能被程序访问,仅供Javascript引擎使用的内部属性,其中一个内部属性是[[Scope]],由ECMA-262标准第三版定义。 内部[[Scope]]属性包含一个函数被创建的作用域中对象的集合。此集合被称为函数的作用域链,它决定哪些数据可以由函数访问。此函数中作用域链中每个对象被称为一个可变对象,以“键值对”表示。当一个函数创建以后,它的作用域链被填充以对象,这些对象代表创建此函数的环境中可访问的数据:
当add()函数创建以后,它的作用域链中填入了一个单独可变对象,此全局对象代表了所有全局范围定义的变量。此全局对象包含诸如窗口、浏览器和文档之类的访问接口。如下图所示:(add()函数的作用域链,注意这里只画出全局变量中很少的一部分) add函数的作用域链将会在运行时用到,假设运行了如下代码:
运行此add函数时会建立一个内部对象,称作“运行期上下文”(execution context),一个运行期上下文定义了一个函数运行时的环境。且对于单独的每次运行而言,每个运行期上下文都是独立的,多次调用就会产生多此创建。而当函数执行完毕,运行期上下文被销毁。 一个运行期上下文有自己的作用域链,用于解析标识符。当运行期上下文被创建的时,它的作用域被初始化,连同运行函数的作用域链[[Scope]]属性所包含的对象。这些值按照它们出现在函数中的顺序,被复制到运行期上下文的作用域链中。这项工作一旦执行完毕,一个被称作“激活对象”的新对象就位运行期上下文创建好了。此激活对象作为函数执行期一个可变对象,包含了访问所有局部变量,命名参数,参数集合和this的接口。然后,此对象被推入到作用域链的最前端。当作用域链被销毁时,激活对象也一同被销毁。如下所示:(运行add()时的作用域链) 在函数运行的过程中,每遇到一个变量,就要进行标识符识别。标识符识别这个过程要决定从哪里获得数据或者存取数据。此过程搜索运行期上下文的作用域链,查找同名的标识符。搜索工作从运行函数的激活目标的作用域前端开始。如果找到了,就使用这个具有指定标识符的变量;如果没找到,搜索工作将进入作用域链的下一个对象,此过程持续运行,直到标识符被找到或者没有更多可用对象可用于搜索,这种情况视为标识符未定义。正是这种搜索过程影响了性能。
Identifier Resolution Performance 标识符识别性能 标识符识别是耗能的。 在运行期上下文的作用域链中,一个标识符所处的位置越深,它的读写速度就越慢。所以,函数中局部变量的访问速度总是最快的,而全局变量通常是最慢的(优化Javascript引擎,如Safari在某些情况下可用改变这种情况)。 请记住,全局变量总是处于运行期上下文作用域链的最后一个位置,所以总是最远才能被访问的。一个好的经验法则是:使用局部变量存储本地范围之外的变量值,如果它们在函数中的使用多于一次。考虑下面的例子:
此函数包含三个对document的引用,而document是一个全局对象。搜索至document,必须遍历整个作用域链,直到最后才能找到它。使用下面的方法减轻重复的全局变量访问对性能的影响:
用doc代替document更快,因为它是一个局部变量。当然,这个简单的函数不会显示出巨大的性能改进,因为数量的原因,不过可以想象一下,如果几十个全部变量反复被访问,那么性能改进将显得多么出色。
Scope Chain Augmentation 改变作用域链 一个来说,一个运行期上下文的作用域链不会被改变。但是,有两种表达式可以在运行时临时改变运行期上下文。第一个是with表达式:
|