经典计算机科学的一个问题是,数据应当存放在什么地方,以实现最佳的读写效率。数据存储是否得当,关系到代码运行期间数据被检索到的速度。在Javascript中,此问题相对简单,由于数据表现方式只有少许方式可供选择。在Javascript中,有四种基本的数据访问位置:前端
1.Literal values 直接量web
直接量仅仅表明本身,而不存储于特定的位置。正则表达式
Javascript的直接量包括:字符串(strings)、数字(numbers)、布尔值(booleans)、对象(objects)、数组(arrays)、函数(functions)、正则表达式(regular expressions),具备特殊意义的空值(null),以及未定义(undefined)。express
2.Variables 变量编程
开发人员用var关键字建立用于存储数据值。数组
3.Array items 数组项浏览器
具备数字索引,存储一个Javascript数组对象。闭包
4.Object members 对象成员函数
具备字符串索引,存储一个Javascript对象。性能
每一种数据存储位置都具备特定的读写操做负担。在大多数状况下,对一个直接量和一个局部变量的数据访问的性能差别是微不足道的。具体而言,访问数组项和对象成员的代价要高一些,具体高多少,很大程度上取决于浏览器。通常的建议是,若是关心运行速度,那么尽可能使用直接量和局部变量,限制数组项和对象成员的使用。为此,有以下几种模式,用于避免并优化咱们的代码:
Managing Scope 管理做用域
做用域概念是理解Javascript的关键,不管是从性能仍是功能的角度而言,做用域对Javascript有着巨大影响。要理解运行速度与做用域的关系,首先要理解做用域的工做原理。
Scope Chains and Identifier Resolution 做用域链和标识符解析
每个Javascript函数都被表示为对象,它是一个函数实例。它包含咱们编程定义的可访问属性,和一系列不能被程序访问,仅供Javascript引擎使用的内部属性,其中一个内部属性是[[Scope]],由ECMA-262标准第三版定义。
内部[[Scope]]属性包含一个函数被建立的做用域中对象的集合。此集合被称为函数的做用域链,它决定哪些数据能够由函数访问。此函数中做用域链中每一个对象被称为一个可变对象,以“键值对”表示。当一个函数建立之后,它的做用域链被填充以对象,这些对象表明建立此函数的环境中可访问的数据:
function add(num1, num2){ var sum = num1 + num2; return sum; } |
当add()函数建立之后,它的做用域链中填入了一个单独可变对象,此全局对象表明了全部全局范围定义的变量。此全局对象包含诸如窗口、浏览器和文档之类的访问接口。以下图所示:(add()函数的做用域链,注意这里只画出全局变量中不多的一部分)
add函数的做用域链将会在运行时用到,假设运行了以下代码:
var total = add(5,10); |
运行此add函数时会创建一个内部对象,称做“运行期上下文”(execution context),一个运行期上下文定义了一个函数运行时的环境。且对于单独的每次运行而言,每一个运行期上下文都是独立的,屡次调用就会产生多此建立。而当函数执行完毕,运行期上下文被销毁。
一个运行期上下文有本身的做用域链,用于解析标识符。当运行期上下文被建立的时,它的做用域被初始化,连同运行函数的做用域链[[Scope]]属性所包含的对象。这些值按照它们出如今函数中的顺序,被复制到运行期上下文的做用域链中。这项工做一旦执行完毕,一个被称做“激活对象”的新对象就位运行期上下文建立好了。此激活对象做为函数执行期一个可变对象,包含了访问全部局部变量,命名参数,参数集合和this的接口。而后,此对象被推入到做用域链的最前端。看成用域链被销毁时,激活对象也一同被销毁。以下所示:(运行add()时的做用域链)
在函数运行的过程当中,每遇到一个变量,就要进行标识符识别。标识符识别这个过程要决定从哪里得到数据或者存取数据。此过程搜索运行期上下文的做用域链,查找同名的标识符。搜索工做从运行函数的激活目标的做用域前端开始。若是找到了,就使用这个具备指定标识符的变量;若是没找到,搜索工做将进入做用域链的下一个对象,此过程持续运行,直到标识符被找到或者没有更多可用对象可用于搜索,这种状况视为标识符未定义。正是这种搜索过程影响了性能。
Identifier Resolution Performance 标识符识别性能
标识符识别是耗能的。
在运行期上下文的做用域链中,一个标识符所处的位置越深,它的读写速度就越慢。因此,函数中局部变量的访问速度老是最快的,而全局变量一般是最慢的(优化Javascript引擎,如Safari在某些状况下可用改变这种状况)。
请记住,全局变量老是处于运行期上下文做用域链的最后一个位置,因此老是最远才能被访问的。一个好的经验法则是:使用局部变量存储本地范围以外的变量值,若是它们在函数中的使用多于一次。考虑下面的例子:
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,必须遍历整个做用域链,直到最后才能找到它。使用下面的方法减轻重复的全局变量访问对性能的影响:
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表达式:
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"; } } |
此重写版本使用了一个with表达式,避免了屡次书写“document”。这看起来彷佛更有效率,实际否则,这里产生了一个性能问题。
当代码流执行到一个with表达式,运行期上下文的做用域被临时改变了。一个新的可变对象将被建立,它包含了指定对象(针对这个例题是document对象)的全部属性。此对象被插入到做用域链的最前端。意味着如今函数的全部局部变量都被推入到第二个做用域链对象中,因此局部变量的访问代价变的更高了。
正式由于这个缘由,最好不要使用with表达式。这样会得不偿失。正如前面提到的,只要简单的将document存储在一个局部变量中,就能够得到性能上的提高。
另外一个能改变运行期上下文的是try-catch语句的字句catch具备一样的效果。当try块发生错误的时,程序自动转入catch块,并将全部局部变量推入第二个做用域链对象中,只要catch之块执行完毕,做用域链就会返回到原来的状态。
try { methodThatMightCauseAnError(); } catch (ex){ alert(ex.message); //做用域链在这里发生改变 } |
若是使用得当,try-catch表达式是很是有用的语句,因此不建议彻底避免。可是一个try-catch语句不该该做为Javascript错误解决的办法,若是你知道一个错误会常常发生,那么说明应该修改代码自己。不是么?
Dynamic Scope 动态做用域
不管是with表达式仍是try-catch表达式的子句catch,以及包含()的函数,都被认为是动态做用域。一个动态做用域因代码运行而生成存在,所以没法经过静态分析(经过查看代码)来肯定是否存在动态做用域。例如:
function execute(code) { (code); function subroutine(){ return window; } var w = subroutine(); // w的值是什么? }; |
execute()函数看上去像一个动态做用域,由于它使用了()。w变量的值与传入的code代码有关。大多数状况下,w将等价于全局的window对象。可是若是传入的是:
execute("var window = {};"); |
这种状况下,()在execute()函数中建立了一个局部window变量。因此w将等价于这个局部window变量而不是全局window的那个。因此不运行这段代码是没法预知最后的具体状况,标识符window的确切含义没法预先知道。
所以,只有在绝对必要时刻才推荐使用动态做用域。
Closure,Scope,and Memory 闭包,做用域,和内存
闭包是Javascript最强大的一个方面,它容许函数访问局部范围以外的的数据。为了解与闭包有关的性能问题,考虑下面的例子:
function assignEvents(){ var id = "xdi9592"; document.getElementById("save-btn").onclick = function(event){ saveDocument(id); }; } |
assignEvents()函数为DOM元素指定了一个事件处理句柄。此事件处理是一个闭包,当函数执行建立时能够访问其范围内部的id变量。而这种方法封闭了对id变量的访问,必须建立一个特定的做用域链。
当assignEvents()函数执行时,一个激活对象被建立,而且包含了一些应有的内容,其中包含id变量。它将成为运行期上下文做用域链上的第一个对象,全局对象是第二个。当闭包建立的时,[[Scope]]属性与这些对象一块儿被初始化,以下图:
因为闭包的[[Scope]]属性包含与运行期上下文做用域链相同的对象引用,会产生反作用,一般,一个函数的激活对象与运行期上下文一同销毁。当涉及闭包时,激活对象就没法销毁了,由于仍然存在于闭包的[[Scope]]属性中。这意味着脚本中的闭包与非闭包函数相比,须要更多的内存开销。尤为在IE,使用非本地Javascript对象实现DOM对象,闭包可能致使内存泄露。
当闭包被执行,一个运行期上下文将被建立,它的做用域链与[[Scope]]中引用的两个相同的做用域链同时被初始化,而后一个新的激活对象为闭包自身建立。以下图:
能够看到,id和saveDocument两个标识符存在于做用域链第一个对象以后的位置。这是闭包最主要的性能关注点:你常常访问一些范围以外的标识符,每次访问都将致使一些性能损失。
在脚本中最好当心的使用闭包,内存和运行速度都值得被关注。可是,你能够经过上文谈到的,将经常使用的域外变量存入局部变量中,而后直接访问局部变量。
Object Members 对象成员
对象成员包括属性和方法,在Javascript中,两者差异甚微。对象的一个命名成员能够包含任何数据类型。既然函数也是一种对象,那么对象成员除了传统数据类型外,也能够包含函数。当一个命名成员引用了一个函数时,它被称做一个“方法”,而一个非函数类型的数据则被称做“属性”。
如前所言,对象成员的访问比直接量和局部变量访问速度慢,在某些浏览器上比访问数组还慢,这与Javascript中对象的性质有关。
Prototype 原型
Javascript中的对象是基于原型的,一个对象经过内部属性绑定到它的原型。Firefox,Safari和Chrome向开发人员开放这一属性,称做_proto_。其余浏览器不容许脚本访问这个属性。任什么时候候咱们建立一个内置类型的实现,如Object或Array,这些实例自动拥有一个Object做为它们的原型。而对象能够有两种类型的成员:实例成员和原型成员。实例成员直接存在于实例自身而原型成员则从对象继承。考虑以下例子:
var book = { title: "High Performance JavaScript", publisher: "Yahoo! Press" }; alert(book.toString()); //"[object Object]" |
此代码中book有title和publisher两个实例成员。注意它并无定义toString()接口,但这个接口却被调用且没有抛出错误。toString()函数就是一个book继承自原型对象的原型成员。下图表示了它们的关系:
处理对象成员的过程与处理变量十分类似。当book.toString()被调用时,对成员进行名为“toString”的搜索,首先从对象实例开始,若果没有名为toString的成员,那么就转向搜索原型对象,在那里发现了toString()方法并执行它。经过这种方法,book能够访问它的原型所拥有的每一个属性和方法。
咱们可使用hasOwnProperty()函数肯定一个对象是否具备特定名称的实例成员。实例略。
Prototype Chains 原型链
对象的原型决定了一个实例的类型。默认状况下,全部对象都是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", "Prototype Chains"); var book2 = new Book("JavaScript: The Good Parts", "Prototype Chains"); alert(book1 instanceof Book); //true alert(book1 instanceof Object); //true book1.sayTitle(); //"High Performance JavaScript" alert(book1.toString()); //"[object Object]" |
Book构造器用于建立一个新的book实例book1。book1的原型(_proto_)是Book.prototype,Book.prototype的原型是Object。这就建立了一条原型链。
注意,book1和book2共享了同一个原型链。每一个实例拥有本身的title和publisher属性,其余成员均继承自原型。而正如你所怀疑的那样,深刻原型链越深,搜索的速度就会越慢,特别是IE,每深刻原型链一层都会增长性能损失。记住,搜索实例成员的过程比访问直接量和局部变量负担更重,因此增长遍历原型链的开销正好放大了这种效果。
Nested Members 嵌套成员
因为对象成员可能包含其余成员。譬如window.location.href(获取当前页面的url)这种模式。每遇到一个点号(.),Javascript引擎就要在对象成员上执行一次解析过程,并且成员嵌套越深,访问速度越慢。location.href老是快于window.location.href,然后者比window.location.href.toString()更快。若是这些属性不是对象的实例成员,那么成员解析还要在每一个点上搜索原型链,这将须要更多的时间。
Summary 总结
1.在Javascript中,数据存储位置能够对代码总体性能产生重要影响。有四种数据访问类型:直接量,变量,数组项,对象成员。对它们咱们有不一样的性能考虑。
2.直接量和局部变量的访问速度很是快,而数组项和对象成员须要更长时间。
3.局部变量比外部变量快,是由于它位于做用域链的第一个对象中。变量在做用域链中的位置越深,访问所需的时间就越长。而全局变量老是最慢的,由于它处于做用域链的最后一环。
4.避免使用with表达式,由于它改变了运行期上下文的做用域链。并且应当特别当心对待try-catch语句的catch子句,它具备一样的效果。
5.嵌套对象成员会形成重大性能影响,尽可能少用。
6.通常而言,咱们经过将常用的对象成员,数组项,和域外变量存入局部变量中。而后,访问局部变量的速度会快于那些原始变量。
经过上述策略,能够极大提升那些使用Javascript代码的网页应用的实际性能。