Web前端性能优化——编写高效的JavaScript
前言
随着计算机的发展,Web富应用时代的到来,Web 2.0早已不再是用div+css高质量还原设计的时代。自Gmail网页版邮件服务的问世开始,Web前端开发也开启了新的纪元。用户需求不断提高,各种新的技术层出不穷,前端工程师的地位也越来越重要。然而任何事物都是有两面性的,随着前端技术的发展,前端业务越来越繁重,这大大增加了JS代码量。因此,要提高Web的性能,我们不仅需要关注页面加载的时间,还要注重在页面上操作的响应速度。那么,接下来我们讨论几种能够提高JavaScript效率的方法。
一、从JavaScript的作用域谈起
当JavaScript代码执行时,JavaScript引擎会创建一个执行环境,又叫执行上下文。执行环境定义了变量或函数有权访问的其他数据,决定了它们的行为,每个执行环境都有一个与它关联的变量对象,环境中定义的所有函数、变量都保存在这个对象中。在页面加载的时候,JavaScript引擎会创建一个全局的执行环境,所有全局变量和函数都是作为window对象(浏览器中)的属性和方法创建的。在此之后,每执行一个函数,JavaScript引擎都会创建一个对应的执行环境,并将该环境放入环境栈中,所以当前正在执行的函数的执行环境是在环境栈的最顶部的,当函数执行完毕之后,其执行环境会弹出栈,并被销毁,保存在其中的变量和函数定义也会被销毁。
当代码在一个执行环境中执行时,JavaScript引擎会创建变量对象的一个作用域链,它可以保证对执行环境有权访问的变量和函数的有序访问。作用域链的前端始终是当前执行的代码所在的环境的变量对象。全局环境的作用域链中只有一个变量对象,它定义了所有可用的全局变量和函数。当函数被创建时,JavaScript引擎会把创建时执行环境的作用域链赋给函数的内部属性[[scope]];当函数被执行时,JavaScript引擎会创建一个活动对象,最开始时这个活动对象只有一个变量,即arguments对象。该活动对象会出现在执行环境作用域链的顶端,接下来是函数[[scope]]属性中的对象。
当需要查找某个变量或函数时,JavaScript引擎会通过执行环境的作用域链来查找变量和函数,从作用域链的顶端开始,如果没找到,则向下寻找直至找到为止。若一直到全局作用域都没有找到,则该变量或函数为undefined。
举个栗子:
function add(a,b) { return a + b; } var result = add(2,3);
代码执行时,add函数有一个仅包含全局变量对象的[[scope]]属性,add函数执行时,JavaScript引擎创建新的执行环境以及一个包含this、arguments、a、b的活动对象,并将其添加到作用域链中。如下图所示:
二、使用局部变量
了解了作用域链的概念,我们应该知道在查找变量会从作用域链的顶端开始一层一层的向下找。显然,查找的层数越多,花费的时间越多。所以为了提高查找的速度,我们应该尽量使用 局部变量(到目前为止,局部变量是JavaScript中读写最快的标识符)。
例如:
function createEle() { document.createElement("div"); } function createEle() { var doc = document; doc.createElement("div"); }
当document使用次数比较少时,可能无所谓,可是如果在一个函数的循环中大量使用document,我们可以提前将document变成局部变量。
来看看jquery怎么写的:
(function(window, undefined) { var jQuery = function() {} // ... window.jQuery = window.$ = jQuery; })(window);
这样写的优势:
1、window和undefined都是为了减少变量查找所经过的scope作用域。当window通过传递给闭包内部之后,在闭包内部使用它的时候,可以把它当成一个局部变量,显然比原先在window scope下查找的时候要快一些。(原来的window处于作用域链的最顶端,查找速度慢)
2、在jquery压缩版本jquery.min.js中可以将局部变量window替换成单个字母,减小文件大小,提高加载速度。
3、undefined也是JavaScript中的全局属性。将undefined作为参数传递给闭包,因为没给它传递值,它的值就是undefined,这样闭包内部在使用它的时候就可以把它当做局部变量使用,从而提高查找速度。undefined并不是JavaScript的保留字或者关键字。
4、undefined在某些低版本的浏览器(例如IE8、IE7)中值是可以被修改的(在ECMAScript3中,undefined是可读/写的变量,可以给它赋任意值,这个错误在ECMAScript5中做了修正),将undefined作为参数并且不给它传值可以防止因undefined的值被修改而产生的错误。
三、避免增长作用域链
在JavaScript中,有两种语句可以临时增加作用域链:with、try-catch
with可以使对象的属性可以像全局变量来使用,它实际上是将一个新的变量对象添加到执行环境作用域的顶部,这个变量对象包含了指定对象的所有属性,因此可以直接访问。
这样看似很方便,但是增长了作用域链,原来函数中的局部变量不在处于作用域链的顶端,因此在访问这些变量的时候要查找到第二层才能找到它。当with语句块之行结束后,作用域链将回到原来的状态。鉴于with的这个缺点,所以不推荐使用。
try-catch中的catch从句和with类似,也是在作用域链的顶端增加了一个对象,该对象包含了由catch指定命名的异常对象。但是因为catch语句只有在放生错误的时候才执行,因此影响比较少。
四、字符串链接优化
由于字符串是不可变的,所以在进行字符串连接时,需要创建临时字符串。频繁创建、销毁临时字符串会导致性能低下。
当然,这个问题在新版本浏览器包括IE8+中都得到了优化,所以不需要担心
在低版本浏览器(IE6、IE7)中,我们可以种数组的join方法来代替。
var temp = []; var i = 0; temp[i++] = "Hello"; temp[i++] = " "; temp[i++] ="everyone"; var outcome = temp.join("");
五、条件判断
当出现条件判断时,我们采用什么样的结构才能使性能最优?
if(val == 0) { return v0; }else if(val == 1) { return v1; }else if(val == 2) { return v2; }else if(val == 3) { return v3; }else if(val == 4) { return v4; }
当条件分支比较多时,我们可以斟酌哪种条件出现的概率比较大,并将对应的语句放在最上面,这样可以减少判断次数。
使用switch语句,新版的浏览器基本上都对switch做了优化,这样层数比较深时,性能比if会更好
使用数组:
var v = [v0,v1,v2,v3,v4]; return v[valeue];
要求:对应的结果是单一值,而不是一系列操作
另外,其他方面的优化,譬如
if(condition1) { return v1; } else { return v2 } // 改成 if(condition1) { return v1; } return v2;
六、快速循环
1、循环总次数使用局部变量
for( var i = 0;i < arr.length;i++) { } // 改成 var len = arr.length; for( var i = 0;i < len;i++) { }
这样就避免了每次循环的属性查找。这点尤其重要,因为在进行dom操作时,很多人会这样写:
var divList = document.getElementsByTagName("div"); for( var i = 0;i < divList.length;i++) { }
查找DOM元素的属性是相对耗时的,所以应该避免这种写法。
2、如果可以,递减代替递增
for(var i = 0;i < arr.length;i++) { } // 改成 for(var i = arr.length - 1;i--;) { } var i = 0; while(i < arr.length) { i++; } // 改成 var i = arr.length - 1; while(i--) { }
i=0的时候会直接跳出,循环次数比较多时还是很有用的。
七、展开循环
var i = arr.length - 1; while(i--) { dosomething(arr[i]); }
遇到这样的情况时,执行一次循环的时候我们可以选择不止执行一次函数。
var interations = Math.floor(arr.length / 8); var left = arr.length % 8; var i = 0; if(left) { do { dosomething(arr[i++]); } while(--left); } do { dosomething(arr[i++]); dosomething(arr[i++]); dosomething(arr[i++]); dosomething(arr[i++]); dosomething(arr[i++]); dosomething(arr[i++]); dosomething(arr[i++]); dosomething(arr[i++]); } while(--interations);
当遇到大数组,减少循环的开销,性能不就提上去了嘛。(至于为什么是每次循环,调8次函数,大牛测出来的,这样达到最佳)
八、高效存取数据
JavaScript中4种地方可以存取数据:
字面量值;变量;数组元素;对象属性
字面量值和变量中存取数据是最快的,从数组元素和对象属性中存取数据相对较慢,并且随着深度增加,存取速度会越来越慢,譬如obj.item.value就比obj.item慢。
某些情况下我们可以将对象、数组属性存成局部变量来提高速度,譬如:
for( var i = 0;i < arr.length;i++) { } // 改成 var len = arr.length; for( var i = 0;i < len;i++) { }
var divList = document.getElementsByTagName("div"); for( var i = 0;i < divList.length;i++) { } // 改成 // var divList = document.getElementsByTagName("div"); for( var i = 0,len = divList.length;i < len;i++) { }
九、事件委托
事件委托就是利用冒泡的原理,将原本应该添加在某些元素身上的监听事件,添加到其父元素身上,来达到提高性能的效果。
举个栗子:
<div> <ul> <li>1</li> <li>2</li> <li>3</li> <li>4</li> <li>5</li> <li>6</li> <li>7</li> <li>8</li> <li>9</li> <li>10</li> </ul> </div> <script> window.onload = function() { var ul = document.getElementsByTagName('ul')[0]; var liList = document.getElementsByTagName('li'); for(var i = 0,len = liList.length;i < len;i++) { liList[i].onclick = function() { alert(this.innerHTML); } } } </script>
这样我们就为每个li添加了监听事件了。
显然,我们通过循环为每个li添加监听事件是不优化的。这样不仅浪费了内存,在新的li加入的时候我们还要重新为它添加监听事件。
我们可以这样写:
<div> <ul> <li>1</li> <li>2</li> <li>3</li> <li>4</li> <li>5</li> <li>6</li> <li>7</li> <li>8</li> <li>9</li> <li>10</li> </ul> </div> <script> window.onload = function() { var ul = document.getElementsByTagName('ul')[0]; var liList = document.getElementsByTagName('li'); ul.onclick = function(e) { var e = e || window.event; var target = e.target || e.srcElement; if(target.nodeName.toLowerCase() == "li") { alert(target.innerHTML); } } } </script>
这样写的好处:
只添加一个监听事件,节省了内存;新加入li的时候我们也不用为它单独添加监听事件;在页面中添加事件处理程序所需的时候更少,因为我们只需要为一个DOM元素添加事件处理程序。
如果是原创文章,转载请注明出处:http://www.cnblogs.com/MarcoHan/
最后,提一点不会提高性能的建议
if( 2 == value) { }
类似这种判断的时候推荐常量放在左边,这样就可以预防类似 if( value = 2){} 的错误了,因为如果少写了一个等号, if( value = 2) {} 是合法的语句,而且代码量变大的时候不容易检查出来。if( 2 = value) {} 这样少写了等号JavaScript引擎会直接报错,我们就可以愉快地改过来了。(只是建议)