ECMAScript
避免使用eval或Function构造器
首先废话一句,根本没用过并且也不知道eval和Function是什么鬼……借由此契机,我终于给js权威指南开封了,2333……
- eval(code)
执行一段字符串中的JavaScript代码。code可包含一条或多条js语句,最后一条语句成为eval的返回值;若无返回值(console.log(…)),则返回undefined。- Function() - 构造函数
1
2
3
4 > var f = new Function("x", "y", "return x*y;");
> // 等同于
> var f = function(x, y) { return x*y; };
>
可以传入大于等于1个字符串,最后一个字符串为函数体,前面的字符串则为函数形参名称。
每次调用Function()都会解析函数体,创建新的匿名函数对象。
Function()在全局作用域执行,读取全局变量。
先大致翻阅了解了一下,可以哪天再开个文研究研究~回归正题:
从以上了解就能感受到,性能的影响在于每次调用时的解析。
eval被调用时直接在上下文档中进行解释,因此就无法优化相关上下文,使浏览器在运行时需要解释得内容变多。Function稍微好些,它不影响周围代码的使用,但仍运行缓慢,如果在循环中使用Function就是灾难。
解决建议:
- 改写eval
简而言之,eval几乎不用存在,想用前先研究一下有没有其他方法效果相同? - 使用function替代Function
function(){...}
的使用效果与new Function("...")
完全一致
可见,本宝宝发现自己根本没见识过这两个东东也是有道理的,呵呵…
不要使用with
666,这个也没用过。
with会在引用变量时为脚本引擎构造一个额外的作用域,这个作用域不能被编译期获知,所以编译器不会像优化普通的作用域(比如由函数创建的作用域)那样优化它,从而影响性能。
with的使用举例:
1 | with(test.information.settings.files) { |
更有效率的做法 —— 使用普通变量来引用对象,然后通过这个变量来访问其属性:
1 | var testObject = test.information.settings.files; |
显然这么搞我更熟悉,2333……
不要在要求性能的函数中使用 try-catch-finally
由于还没在js中用过这个,但前一阵子在python中有了体验:写在一个循环里,销魂啊,循环了二十次左右后pythonw.exe直接崩了……后来查了资料,改用if判断提前规避错误了……
try-catch-finally在运行时会在当前作用域创建一个新变量,每次catch子句运行的时候,这个变量会引用捕捉到的异常对象。这个变量在catch子句开始的时候创建,并在这个子句结束的时候销毁,即在脚本运行时创建和销毁,所以带来了性能问题。
异常处理应尽可能地放在更高层次的脚本中,使异常可能不会频繁发生;或者可以先检查操作是否可行,以避免异常发生。(啊,这不就是本宝宝自己想出来的if判断,本宝宝真机智~)
虽然不够应景,但耐不住感触深啊。pia一下我当时老崩的python代码:
1 | import arcpy |
这个是改进后的代码:
1 | import os, re |
隔离eval和with的使用
前文说了尽可能的少用或不用eval和with,但在必需使用它们时,就要注意了:
- 不要在循环中重复执行它们,或者其他的反复调用它们
- 它们只适合在执行一次或很少几次的代码中使用
- 把它们与其它代码隔离开来,这样就不会影响到其它代码的性能
- 可以把它们放在一个顶层函数中,或者只运行一次并把结果保存下来,以便稍后可以使用其结果而不必再运行这些代码
另外某些浏览器解析try-catch-finally结构时会对性能产生影响,包括 Opera,所以最好以同样的方式对其进行隔离。
尽量不用全局变量
我就是走全局变量的懒人,要改!
全局变量对性能的影响:
- 如果代码在函数或另一个作用域中引用全局变量,脚本引擎会依次通过每个作用域直到全局作用域进行查询,而局部变量找起来就会快得多。
- 全局作用域中的变量存在于脚本的整个生命周期,而局部变量会在离开局部作用域的时候被销毁,它们占用的内存可以被垃圾收集器回收。
- 全局作用域由window对象共享,也就是说它本质上是两个作用域而不是一个。在全局作用域中,变量总是通过其名称来定位,而不是像局部变量那样经过优化,通过预定义的索引来定位。这最终导致脚本引擎需要花更多时间来找到全局变量。
*函数通常也在全局作用域中创建。因此一个函数调另一个函数,另一个函数再接着调其它函数,也会增加脚本引擎的运行时间。
低效率版:
1 | var i, s = ''; |
高效率版(快30%左右):
1 | function testfunction() { |
注意对象的隐式转换
额,这点也是我的恶习……从没注意过,甚至一度不解过为什么要有new的形式
字面量(如字符串、数、布尔值等),在ECMAScript中有两种表现形式:作为值创建、作为对象创建。例如:var oString = 'some content';
:创建了一个字符串值;var oString = new String('some content');
:创建了等价的字符串对象。
所有属性和方法都是在字符串对象而不是值上定义的。
如果对字符串值调用属性和方法,ECMAScript引擎会用相同的字符串值隐式地创建一个新的字符串对象,然后才调用属性或方法。并且,这个对象仅用于这一次需求,如果下次再对字符串值调用某个方法,会再次类似地创建一个新的字符串对象。
低效率版。每次访问length属性和调用charAt方法的时候都会创建对象,脚本引擎总共会创建21个新的字符串对象(terrible…):
1 | var s = '0123456789'; |
高效率版。只创建了一个对象:
1 | var s = new String('0123456789'); |
在要求性能的函数中避免使用 for-in
不太习惯for-in循环,但一直以为这是一种高级的写法,如今看来,哈哈~
for-in循环需要脚本引擎为所有可枚举的属性创建一个列表,然后检查其中的重复项,之后才开始遍历。所以当脚本本身已经知道需要遍历哪些属性的时候(如所需遍历属性名称为有序数字的情况),使用简单的for循环更为合适。比如数组、伪数组(如由DOM创建的NodeList)
低效率版:
1 | var oSum = 0; |
高效率版:
1 | var oSum = 0; |
使用累加形式连接字符串
再次,膝盖中一箭……
字符串连接非常消耗性能。使用+运算符不会直接把结果赋值给变量,而会在内存中创建一个新的字符串用于保存结果,再将这个新的字符串赋值给变量。
低效率版:a += 'x' + 'y';
。这段代码首先在内存中创建一个临时的字符串保存连接的结果’xy’,然后将它连接到a的当前值,再将最终的连接结果赋值给a。
高效率版。因为每次都是直接赋值,所以不会使用临时字符串,运行速度会快20%,并且消耗内存更少:
1 | a += 'x'; |
基本运算比调用函数更快
比如:直接通过数组的尾部索引添加元素 比 对数组调用push方法 更佳;简单的数学计算 比 调用Math对象的方法 更佳。
低效率版:
1 | arr.push(v); |
高效率版:
1 | arr[arr.length] = v; |
为setTimeout()和setInterval()传入函数而不是字符串
即,传入定义好的函数的函数名:setInterval(f1, 1000);
或者匿名函数:setInterval(function(){...}, 1000);
不要用setInterval('f1()', 1000);
这类的字符串形式。
DOM
总的来说,有三个主要因素会导致DOM的性能不佳。
- 脚本进行了大量的DOM操作,比如通过收到的数据创建一棵树。
- 脚本触发了太多重排或者重绘。
- 脚本使用了低性能的方法来定位DOM树中的节点。
重绘和重排
重绘:
某元素从可见变为不可见,或者反之,但没有改变文档布局。例如:为某个元素添加轮廓线,改变背景色或者改变visibility样式等。
重绘对性能的消耗在于:它需要引擎搜索所有元素来决定什么是可见的,什么应该显示出来。
重排(更耗性能):
对DOM树进行了改动操作,或者某个元素样式变动时改变了文档布局。例如:改变元素的className属性(这个为什么会重排?),改变浏览器窗口的大小。
重排对性能的消耗在于:它相当于重新布局整个页面(父元素的重排会引起子元素的重排,某个重排元素之后的元素也需要重新计算新的布局位置,子孙元素大小的改变也会导致祖先元素的重排……),即:牵一发而动全身。
以下是对一些重排或重绘操作的优化建议:
1. 将重排数量降到最低
首先必须承认,重排是不能完全避免的,比如动画。所以要保证脚本跑得飞快,就必须在保证相同整体效果的前提下将重排保持在最低限度。
浏览器可以选择在脚本线程完成后进行重排,显示变化。Opera会等到发生了足够多的变化,经过了一定的时间,或者脚本线程结束,再重排。也就是说,如果在同一个线程中发生的变化足够快,它们就只会触发一次重排。然而,Opera运行在不同速度的设备上,这种现象并不保证一定会发生。
有些元素在重排时,显示速度慢于其它元素,要注意规避。比如,重排一个table需要3倍于等效块元素显示的时间。
2. 最小重排
一般的重排会影响到整个文档,文档中需要重排的东西越多,重排花的时间就越长。
然而,绝对定位(absolute)和固定定位(fixed)的元素不会影响主文档的布局,所以对它们的重排不会引起其它部分的连锁反应。文档中在它们之后的内容可能需要重绘来呈现变化,但这也远比一个完整的重排好得多。
因此,动画不需要应用于整个文档,它最好只应用在一个固定位置的元素上。
3. 关于修改文档树
修改DOM树(添加新的节点、改变文本节点的值或者修改各种属性)会导致重排,而多次连续地改变可能导致多次重排。
因此,最好在一段未显示出来的DOM树片段上进行多次改变,然后用一个单一的操作把改变应用在文档的DOM中:
1 | var docFragm = document.createDocumentFragment(); |
修改文档树也可以通过克隆实现(注意如果元素中包含任何形式的控制,或者其本身或子元素存在事件响应,则不能使用这个方法,因为这些附着关系不会被克隆):
1 | var original = document.getElementById('container'); |
4. 修改不可见的元素
如果某个元素的display样式设置为none,就不会对其进行重绘,哪怕它的内容发生改变 —— 这是一种优势。
因此,如果需要对某个元素或者它的子元素进行改变,而且这些改变又不能合并在一个单独的重绘中,那就可以先设置这个元素的样式为display:none
,然后改变它,再把它设置为普通的显示状态。
1 | var posElem = document.getElementById('animation'); |
不过这会造成两次额外的重排,一次是在隐藏元素的时候,另一次是它再次显示出来的时候,所以需要权衡什么时候需要使用这种方法,什么时候不用。
另外,这样做也可能意外导致滚动条跳跃,不过把这种方式应用于固定位置的元素就不会导致难看的效果。
5. 关于测量元素
一般而言,浏览器会缓存一些变化,然后在这些变化都完成之后只进行一次重排。
但是,测量元素会导致浏览器强制重排(如,使用offsetWidth这样的属性,或者getComputedStyle这样的方法)。一旦调用,就改变了浏览器的缓存,从而触发重排,即使这不会引起明显的重绘。
因此,如果这些测量数据需要反复使用,建议仅测量一次,然后将结果保存起来以备后用。
6. 多项样式的一起改变
如果需要对某个元素一次性改变多个CSS样式,不宜一个个地去指定样式,可以使用以下两种方式:
1.如果需要变化的样式是已知的,则可以定义一个包含这些样式变化的class,通过修改元素的class实现。
2.如果变化的样式是未知的,例如动画,则可以为元素定义一个新的样式属性,通过style对象的cssText属性实现,或者通过setAttribute实现。
1 | var posElem = document.getElementById('animation'); |
平滑度换速度
开发者总是希望通过使用更小的间隔时间和更小的变化,让动画尽可能平滑。
但是,10ms几乎已经是浏览器能在不100%占用大多数台式机CPU的情况能实现的最小时间间隔。对于多数浏览器来说,每秒进行100次重排实在太多了。对于低功耗计算机或低功耗设备上的浏览器,这样的动画只会给人以缓慢和卡顿的感觉。
所以有必要权衡平滑度与速度的关系,适当地使用动画的平滑度来换取速度。比如将时间间隔改变为50ms,动画每次移动5个像素,这样需要的处理能力更少,也会让动画在低功耗处理器上运行起来快得多。
关于遍历检索特定节点
这里涉及到了以前不曾关注的DOM遍历问题,有必要完整学习一下。(一些值得注意的方法:DOM2 Traversal TreeWalker、XPath)
1. 避免检索大量节点
在试图找到某个特定节点,或者某个节点的子集时,应该使用内置的方法和DOM集合来缩小搜索范围,使之在尽可能少的节点内进行搜索。
这里讨论的是js的检索,但是一般实践中我用jquery更多,不知道$选择器是怎样操作的,写全完整路径更优还是模糊范围更优?
2. 通过XPath提升检索速度
假设需要在一个包含了上千元素的文档中获取h2-h4元素,它们散落在文档各处,没有任何适当的结构,所以不能用递归来获得正确的顺序。
传统的DOM遍历方法,因为文档中元素过多,一个一个遍历会导致显著的延迟:
1 | var allElements = document.getElementsByTagName('*'); |
使用XPath可以优化查询引擎,查询速度甚至可以提升高达两个数量级:
1 | var headings = document.evaluate('//h2|//h3|//h4', document, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null); |
*可以通过if( document.evaluate )
来判断浏览器是否支持XPath。
3. 避免在遍历DOM的时候进行修改
对于某些类型的DOM集合,如果你的脚本在检索它的时候改变了相关元素,集合会立即发生变化而不会等你的脚本运行结束。如:childNodes集合,以及getElementsByTagName返回的节点列表。
对于这样的集合,如果在检索时又向里添加元素,那可能会导致一个无限循环,因为在到达终点前不断的往集合内添加项。
另外,更重要的是:这些集合原可以被优化以提升性能,它们能记住长度和脚本引用的最后一个索引,以便在增加索引的时候,能迅速引用下一个节点。但是如果你修改了DOM树的任意部分(哪怕它不在集合中),集合就必须重新寻找新的条目。这样做的话,它就不能记住最后的索引或长度,因为这些可能已经变化,之前所做的优化也就失效了。
解决方案:
先建立一个静态元素列表用于修改,然后遍历这个静态列表来进行修改,以此避免对getElementsByTagName返回的列表进行修改:
1 | var allPara = document.getElementsByTagName('p'); |
4. 在脚本中用变量缓存检索到的DOM值
DOM返回的某些值是不缓存的,它们会在再次调用的时候重新计算(如getElementById方法)。因此建议用变量保存返回值,以便后续的多次使用(命令运行的速度会快五到十倍)。
1 | var sample = document.getElementById('test'); |
文档加载
避免在多个文档间保持同一个引用
如果一个文档访问了另一个文档的节点或者对象,应该避免在脚本使用完它们之后仍然保留它们的引用。如果某个引用保存在当前文档的全局变量中,或者保存在某个长期存在的对象的属性中,通过将其设置为null,或者通过delete来清除它。
1 | var remoteDoc = parent.frames['sideframe'].document; |
原因:
如果另一个文档已经销毁(比如原来显示在弹出窗中而现在这个窗口关闭了),当前文档中保存的引用通常仍然会使其DOM树或者脚本环境在RAM中存在,哪怕文档本身已经不在加载状态了。
在框架页面,内联框架页面或object元素中同样存在这个问题(额,啥框架,比如说?还是不太懂这方面的啊)。
关于浏览器的快速历史导航功能
Opera(以及很多其它浏览器)默认使用快速历史导航,即:当用户在浏览器历史上前进或回退的时候,页面的状态及其中的脚本其实都被保存了。当用户回到某个页面的时候,它会像从未离开过一样继续运行,文档不会再次加载和初始化。
快速历史导航有助于浏览器实现对用户的快速响应,使加载缓慢的Web应用在导航过程中表现得更好。因此,我们要做的就是使脚本尽量避免做出会导致这种行为失败的事情。例如:在表单提交时禁用表单控件、菜单项被点击之后就不再有效、离开页面时的淡出效果使内容模糊不清或不可见。
使用onunload监听器是比较简单的解决办法,可以通过它重置淡出效果,或者使表单控件变为可用:
1 | window.onunload = function () { |
但是,某些浏览器(Firefox、Safari)使用unload监听器会导致快速历史导航失效。此外,禁用提交按钮在Opera中也会导致快速历史导航失效。
使用XMLHttpRequest
该方法能有效减少从服务器接收的内容,同时避免页面加载带来的脚本环境的破坏和再造。最初,页面以正常的方式加载,之后再通过XMLHttpRequest来加载最小需求的新内容。这会让JavaScript环境保持下来。
这里的js环境指执行环境(Execution context,EC),或称执行上下文,可以深扒一下
不过需要注意的是,这并非对所有项目都适用,而且这种访求可能会导致问题——它完全打破了历史导航,虽然可以通过将信息保存在内联框架中来伪造历史,但这违背了使用XMLHttpReqest的首要目的。因此,请谨慎地,只在它所造成的变化不需要回退的时候使用它。
这种方法也有可能对辅助设备(什么是辅助设备?)造成混乱,因为辅助设备感受不到页面上的DOM的变化。所以最好在确保不会出现问题的情况下使用这个方法。
对于不允许JavaScript,或者浏览器不支持XMLHttpReqeust的情况,可以使用一个正常的链接,指向新页面;然后为这个链接添加事件处理函数,在链接被点击的时候检查是否支持XMLHttpReqest;如果支持,则加载新数据并阻止链接的默认行为。
XMLHttpReqest获得的数据加载完成,替换了页面的某些内容后,就可以销毁请求对象,以允许垃圾回收释放内存。
1 | document.getElementById('nextlink').onclick = function() { |
动态创建script元素
最好不要加载当前页面不使用的脚本,可以通过动态加载脚本的方式,在实际用到的时候才创建脚本元素。
理论上,在页面加载完成之后,可以通过script元素来加载额外的脚本并通过DOM添加到文档中,但是实际上可能是在浏览器上请求而不是立即加载脚本。
另外,记得用转义斜杠以免过早结果当前脚本:<\/script>
具体动态加载方式再开坑
location.replace()控制历史记录
有时我们需要使用脚本来改变页面地址。最典型的做法是给location.href赋予一个新地地址。但这样做会添加一个历史记录,同时加载一个新的页面,这和激活一个普通的链接一样。
在某些情况下,并不希望出现一条额外的历史记录,因为用户不需要回到之前的页面。如果在内存特别重要的环境下,这样做就非常有用。
当前页面使用的内存可以通过替换历史记录来得到重新利用,使用location.replace('newpage.html')
方法就可以做到。
但请注意,该页可能仍然保留在缓存中,并可能在那里使用内存,但不会用到像保存在历史记录里那么多。
最后的感慨:当初简单学习了一下js,就用jquery库了,文中很多关于DOM的操作都是没有接触过的,看的时候往往理解不能很深;推及到自己实际使用吧,又因为jquery的集成不能得知其中详细,感觉很是不痛快,要补!
致谢:
(高效的JavaScript)[http://www.zcfy.cc/article/dev-opera-efficient-javascript-2320.html]