May22

Javascript的循环优化探究

分类: Javascript | 转载请注明: 出自 海玉的博客
本文地址: http://www.hicss.net/explore-optimization-of-javascript-loop/

前几天又温习了一番JS的基础知识,发现JS循环同其他语言的循环大有不同,随后再继续翻了俩本JS的权威教程,写下这篇文章。

平时我们书写循环大致是这个样子的:

function(){
	//一般循环的书写方式
	for(var i=0; i<values.length; i++){
		...
	}
}

优化变量声明

上面这个写法在一开始学习JS的时候是没有错的,甚至对于绝大多数的面向对象语言这么写都是正确的书写方式,但JS不同与其他面向对象的语言,他没有块及作用域,有的仅仅是函数作用域,所以说,上面的写法并不是很规范,在某些情况下还会发生Bug ,当你使用了一个变量,然后不久在函数中又重新声明的话,就可能产生逻辑错误。对于JavaScript,只要你的变量是在同一个作用域中(同一函数),它都被当做是声明的,即使是它在var声明前使用的时候。合理的写法即把变量声明在函数的开始,而不是在循环内部才开始定义变量。

function func(){
	//优化JS的变量定义,变量定义在开始位置,避免产生块级作用域的误区
	var i;
	for(i=0; i<values.length; i++){
		...
	}
}

优化循环中动态集合读取

优化了JS 循环的变量后,我们继续深入优化。日常开发最常打交道的就是DOM 了,常常会遇到循环NodeList 的情况(不知道什么是NodeList 对象的话请去复习基础知识)。总而言之一般类似var divs = document.getElementsByTagName(“div”) 的这个divs 所引用的就是NodeList 对象,其他类似的NodeList 近亲有NameNodeMap 和 HTMLCollection, 这三个集合每当文档结构发生变化时,它们都会得到更新(而且还是动态的更新)。这样就会导致俩个问题,第一是性能问题,每当你修改NodeList 在读取它时,你读取的不是先前的NodeList 而是修改后动态更新的NodeList。第二会产生无限循环的Bug。举个例子,下列代码会导致无限循环:

function func(){
	//导致NodeList无限循环的循环书写方式
	var divs = document.getElementsByTagName("div");
	for(var i=0; i<divs.length; i++){
		var div =document.createElement("div");
		document.body.appendChild(div);
		alert("Infinite loop");
	}
}
func()

上例所示的循环代码会导致一个严重的问题,每次循环都要对divs.length 求值,意味着会运行取得所有div元素的查询,之后创建一个新的div元素添加的文档中,因此div.length 的值每次循环后都会递增。既然i 和divs.length 每次都会同时递增,结果他们的值永远不会相等,从而导致了无限循环。

为了避免出现这样的低效率甚至于隐含Bug的循环书写方式,对策就是尽量减少访问NodeList 的次数。因为每次访问NodeList,都会运行一次基于文档的查询。所以可以考虑从NodeList 中取得的值缓存起来。下面是优化后的写法:

function func(){
	//避免NodeList无限循环的循环书写方式
	var divs = document.getElementsByTagName("div");
	for(var i=0, len = divs.length ; i<len; i++){
		var div =document.createElement("div");
		document.body.appendChild(div);
		alert("Infinite loop will not happen");
	}
}
func()

很简单吧,只要把divs.length 缓存起来放在一个变量len 里面,那么无论NodeList以后怎么变,都避免读取,避免无限循环发生,因为JavaScript的解释性,所以a.b.c.d.e,需要进行至少4次查询操作,先检查a再检查a中的b,再检查b中的c,如此往下。所以如果这样的表达式重复出现,只要可能,应该尽量少出现这样的表达式,利用局部变量,把它放入一个临时的地方进行查询。如果每次查询div.length,就要额外进行一个操作,而预先把var len=div.length,则就少了一次查询,性能的优化大大体现。 开始你会觉得这样书写很不适应,但多写几遍,你很快就会习惯这样“别扭”书写方式的。

优化继续优化!

用i+=1 代替i++

这个是从《Javascript语言精粹》里面看来的,大致意思是说用了i++ 会有潜在的安全问题,++这个运算符可以前置又可以后置使得自由度过大,如果出现结尾没有分号会导致些许的错误例如:a++b++c,你很难弄明白前面那个代码是什么意思。根据《Javascript语言精粹》上面提到,这个++ 或者–– 运算符怂恿过于诡异的写法而促使出现糟糕的代码。除了错误的架构外,它们是导致病毒和其他安全威胁的第二大元凶。所以JsLint有个选项plusplus 是检测是否禁止使用了这些运算符。还有个原因是i+=1 比起i++ 更加“原生”可以提升性能,但是个人认为可读性降低,根据需要各自取舍吧。那么我们的代码又要变成这个样子:

function func(){
	//用i+=1 代替i++
	for(var i=0; i<values.length; i+=1){
	...
	}
}

让优化来的更猛烈些吧!

减值迭代优化循环

大多数循环使用一个从0开始、增加到某个特定值的迭代器。在很多情况下,从最大值开始,在循环中不断减值的迭代器更加高效。如果值的处理顺序无关紧要,那么循环可以改为i 减值,优化如下:

function func(){
	//减值迭代优化循环
	for(var i=values.length-1; i>=0; i--){
	...
	}
}

循环优化大串联

如果我们把上面所有的循环优化方式集中在一块,写法会是这样子滴:

function func(){
	//循环优化大串联
	var i;  //优化变量声明
	var divs = document.getElementsByTagName("div");
	for(i=divs.length-1; i>=0; i-=1){  //优化循环中动态集合读取、减值迭代、用i-=1 代替 i--
	...
	}
}

老实说,上面这个循环优化大串联性能是提高了,但可读性也降低到了一个新的高度。你代码写的再风骚,也得让人有个接受的地步那,做为团队开发,上面这个串联似乎还是不是特别可取,个人还是提倡部分优化,以优化性能的前提下兼顾可读性:

function func(){
	//合理的循环优化
	var i,len;  //优化变量声明
	var divs = document.getElementsByTagName("div");
	for(i=0, len = divs.length; i<len; i++){  //优化循环中动态集合读取,必要时也可以用i+=1 代替 i++
	...
	}
}

个人比较倾向于上面的这段JS循环。合理优化循环提升了代码的性能,又兼顾了可读性,可谓是一箭双雕,万事大吉!

3 个评论

  1. [...] Javascript的循環優化探究 [...]

  2. 小小的循环也可以有这么多东西,赞!博主的博客风格我很喜欢,文章不多,但是质量很高啊!

  3. [...] 原文链接:Javascript的循环优化探究 [...]

发表评论