【前端】JavaScript代码最佳实践方法

1 避免定义全局变量或函数
定义全局变量或函数,会影响代码的可维护性。如果在页面中运行的JavaScript代码是在相同的作用域里面,就意味着代码之间的定义存在互相影响的可能。如果另外一个开发者在不知道已定义这些变量和函数的情况下,也定义了相同名称的变量或函数,则后定义的函数或者方法会覆盖之前的定义。

解决这个问题最简单的手段是把变量和方法封装在一个变量对象上,使其变成对象的属性,例如:

var myCurrentAction = {
		length: 0,
		init: function(){...},
		action: function(){...}
}

但这个方案也有弊端,即所有的变量和函数的访问都要通过主对象来实现,比如访问length变量,需要通过myCurrentAction.length来访问。这就增加了代码的重复度和代码编写的复杂度。

另一种改进的方案是把全局的变量包含在一个局部作用域中,然后在这个作用域中完成这些变量的定义以及变量使用的逻辑。例如,可以通过一个匿名函数实现,如:

(function(){
		var length=0;
		function init(){...}
		function action(){...}
})();

所有的逻辑都包含在这个立即执行的匿名函数中,形成了一个独立的模块。
当然在实际业务中,比如要公开上述代码中的init函数,则上述代码应修改为如下形式;

var myCurrentAction =(function(){
		var length=0;
		function init(){...}
		function action(){...}
		return {
			init: init
}
})();

这样,外部代码访问init()方法时,就可以调用myCurrentAction.init了。
此方案既做到了代码逻辑的封装,又公开了外部需要访问的接口,是代码模块化的最佳实践方式之一。

另外一个避免定义全局变量的方式是:确保在定义变量时使用var关键字。如果定义变量时没有使用var,浏览器解析时并不会报错,而是自动把这一变量解析为全局变量,比如
下面length就是一个全局变量:

(function(){
		length=0;
		function init(){...}
		function action(){...}
})();

这种可以不通过var关键字定义而定义变量的方式也是JavaScript代码灵活的一种体现,但同时也是代码中潜在问题的根源之一。但是在JavaScript代码的严格模式中,变量定义必须添加var关键字,否则会报编译错误。

2 使用简化的编码方式
例如下面的代码:

//对象创建
var person = new Object();
person.age = 25;
person.name=’dang’;

//数组创建
var list = new Array();
list[0] = 12;
list[1] = 20;
list[2] = 24;

在JavaScript中,可以使用JSON方式创建对象和数组,如下:

person = {age: 25, name:’dang’};
list = [12,20,24];

3 使用比较运算符===而不是==
===严格相等 !==严格不相等 这两者会先进行基础类型值是否相等,或者两个复杂对象是否指向同一地址。
而==和!=则会先进行比较值的类型转换,在把两个比较值的类型转换为相同类型后才会进行比较运算,因此,只有在两个比较值的类型一致时,才与第一组相等元素运算符等同。

==!=在比较时转换规则很复杂,但是只要使用===和!==这两个严格相等运算符进行比较时就可以避免出现隐含的错误。

4 避免使用with语句
在JavaScript中,with语句可用来快捷地访问对象的属性,with语句的样式如下:

with(object){
	statement
}

with语句的使用原理是:JavaScript解析和运行时,会给with语句单独建立一个作用域,而和with语句结合的对象中的属性则成为了此作用域的局部变量,因此可以直接访问。

with(Math){
		a=PI*r*r;
		x=r*cos(PI);
		y=r*sin(PI/2);
}

上面的代码等同于

a=Math.PI*r*r;
x=r*Math.cos(PI);
y=r*Math.sin(PI/2);

但是with的缺陷是:
1> 使用with,代码难以阅读
2> with语句存在兼容问题
3> with语句的设计方面有缺陷,在with语句内部修改和with语句结合的对象后,并不能同步到with内部,即不能保证对象数据的一致性。

5 避免使用eval
eval会接受一个字符串参数,把字符串内容作为代码执行,并返回执行结果。如下:
eval(“x=1;y=2;x*y”)

但是使用eval的缺陷在于:
1> eval让代码难以阅读,影响代码的可维护性
2> 使用存在安全问题,因为它会执行任意传入的代码,而传入的代码可能不安全。
3> 此外,有几个和eval函数功能类型的也要避免使用,如:setTimeout,setInterval和Function构造器

6 不要编写检测浏览器的代码
最佳的做法是不要编写检测浏览器的代码,而是检测浏览器是否支持某个特定功能。可以借助Modernizr框架来检测浏览器的特性支持。

当然也存在某些特定情况需要判断浏览器的版本,尤其是判断IE浏览器。这时最好是把针对特定浏览器的代码逻辑放在单独的文件中,方便后期的维护和移除

如果要针对浏览器添加兼容代码,这时,可把兼容代码放在单独的文件中,页面中需要添加如下代码

<![if It IE 9]-->
		<script src=”javascript/html5.js”></script>
<![endif]>

后期如果不再支持IE8及其以下的浏览器,只需移除此代码引用。

使用更严格的编码格式
JavaScript的严格模式
严格模式对一些编码的格式进行了限制。有些在普通模式不会报错的代码,在严格模式下会出现运行错误。
除了IE8外,大多数浏览器都支持严格模式
使用严格模式,只需要在代码中添加:

“use strict”

即可
在不支持严格模式的浏览器中,代码会被当做普通代码运行,不会产生副作用

在严格模式中需要遵循的几条最佳实践:

  1. 不要在全局中启用严格模式
    因为不能保证其他人的代码也符合严格模式,所以在全局中启用严格模式会带来未知的风险。
    尽量把严格模式限定在函数作用域范围内。如果想给大量的代码设置严格模式,则可以把代码包含在一个立即执行的函数内,并在函数开头启用严格模式:
(function(){
		“use strict”;
		//其他代码
})();
  1. 在已有的代码内谨慎使用严格模式
    如果代码中含有不符合严格模式的代码,则启用严格模式存在风险。必须有足够的代码检查和测试,确保代码的运行。
    流行的代码检查工具是JSLint和JSHint

事件处理和业务逻辑分离
JavaScript中的事件处理非常重要,这部分代码在整个JavaScript中占很大的比重。
但很多人把事件处理和业务处理的代码混合在一起,导致代码的可维护性降低。如下:

var move_while_dnd = function(e){
	var lb=scheduler._get_lightbox();
	
	lb.style.top=e.clientY+”px”;
	lb.style.left=e.clientX+”px”;

}

本例中用到了event对象中的clientX和clientY这两个属性,但是这段代码耦合紧密,不利于复用。如果后期支持通过键盘的上下左右来移动元素,则需要重写编写代码。
所以要把事件处理和业务逻辑处理分离开,调整如下:

var move_while_dnd = function(top,left){
	var lb=scheduler._get_lightbox();
	
	lb.style.top=top+”px”;
	lb.style.left=left+”px”;

}

var move_while_dnd = function(e){
	setLightBoxPosition(e.clientY, e.clientX);
}

这里对业务逻辑进行分离,提高了代码的可维护性和可重用性。

配置数据和代码逻辑分离
有部分框架可以实现数据和代码逻辑的分离:
Bootstrap中所有的空间都有默认属性DEFAULTS用于保存默认的配置数据。
YUI框架中也有类似的做法

在JavaScript中,分离的配置数据一般以JSON格式保存。
如果配置数据过多或者期望客户端缓存配置数据,则推荐把配置数据放置在单独的JavaScript文件中,让配置数据的修改更方便,同时客户端可以缓存配置数据文件。

JavaScript和html&css分离
JavaScript可以修改或删除html的相关元素,但是滥用这一能力会对维护带来极大地困难,所以在编写JavaScript代码时,让JavaScript代码只关注逻辑行为,尽量不要越权做本来应该是HTML代码和CSS代码完成的工作,即要让逻辑与结构样式分离。

从JavaScript中,可以通过设置元素的style对象,来修改元素的样式。如:
list[i].style.borderColor=’#f00’;
也可以通过设置style上的cssText属性,如:
list[i].style.cssText += ‘border: 1px solid #f00;’;

但通过这两种方式修改元素的样式,都降低了代码的可维护性。如果元素的样式是通过这两种方式中的一种设置的,如果后期需要修改样式,那不光要修改JavaScript代码,还要修改CSS代码,这让页面样式维护变得很困难。
**最佳的做法是,在JavaScript代码中,仅仅是设置元素的class,**如:
list[i].className+=’empty’;

如果遇到元素样式的修改,则只修改元素上Class对应的CSS代码。

同时也应该避免在JavaScript代码中生成大量的HTML元素及内容。在JavaScript中,最常见的是用过元素的innerHtml属性设置元素内部子元素,如下:

var d2 = document.createElement(DIV);
d2.innerHtml =-<textarea class=’editor’>location</textarea>;
d.appendChild(d2);

在JavaScript中内嵌HTML代码的方式,有很大的缺点

  1. 容易写出结构不完整的html代码
  2. 维护困难,检查html代码需要花费很大的精力
  3. 不利于排错

让JS和html分离的方案:
1. 从服务器端动态获取html代码
将页面加载初始不需要加载的HTML代码从页面中分离,放置在单独的文件中。需要时,再通过AJAX动态从服务器端获取,然后显示在页面上。
或者把JavaScript模板放置于单独的文件中,需要时才从服务器端加载。下面是从服务端取的HTML代码的示例:

var xhr = new XMLHttpRequest();
xhr.open(GET,”content/templates/store.html”,true);
xhr.onreadystatechange = function(){
		if(xhr.readyState === 4 && xhr.status ===200){
			document.getElementById(‘store_container’).innerHTML = xhr.responseText;
}
};
xhr.send(null);

如果是jQuery,则:

$(‘#store_container’).load(“content/templates/store.html”);

使用AJAX技术获取HTML代码的另一个优点是减少了页面初始的HTML代码量,加快了页面的传输速度,减少了页面的解析时间。

2. 通过客户端动态生成页面结构
可以把这部分代码或模板放置在页面的HTML中,如果纯粹是HTML代码,隐藏在页面中即可,然后通过JavaScript代码直接设置display样式就可以显示此区域。模板文件可以放在注释中,<textarea>标签中,但最好的方法是包含在<script>标签中。
<script>标签如果没有指定type属性,则默认值为”text/javascript”,即包含的内容为JavaScript代码,如果指定的属性值为其他值,则不会把内容指定为JavaScript代码。
可以利用这一点把JavaScript模板代码放在<script>标签中,更重要的是,<script>标签中的代码会保留代码的缩进格式。见下例:

<script id=”main_info” type=”text/x-tmpl”>
		<li><b>${name}</b>(${class})</li>
</script>

当需要取得模板代码时,通过innerHTML属性可得到:

var infoTemplate = document.getElementById(‘main.info’).innerHTML;

JavaScript模板
目前JS模板引擎可以分为两大类,一类是把JS逻辑写到模板中;另一类是无逻辑的,模板代码和逻辑分离。
流行的JS模板主要有:
1> Mustache
Mustache最大的特点是此模板是无逻辑的,模板中没有if或者for等结构,而是通过数据值实现这些逻辑。
2> Underscore中的模板引擎
该模板不是无逻辑的
3> Handlebars

使用模板时要注意:

  1. 尽量不要在模板中滥用逻辑块
  2. 不要构建太复杂的模板
  3. 使用预编译模板

JavaScript模块化开发
按照模块发开的思想,在JavaScript中利用立即执行函数,可以实现在不暴露私有数据的前提下公开一些公共的接口,如下是模块的具体实现示例:

var module1 = (function(){
	var length = 0;
	var init = function(){
		...
	}
	var action = function(){
		...
	}
	return{
		init: init;
		action: action;
	}
})

这种方式可以避免模块之间的代码“污染”,很巧妙地做到了模块的封装和接口的公开。
为了最好的保持模块之间的独立性,模块和模块之间最好通过各自的公开接口来通信。
如果模块之间存在很紧的依赖关系,则模块内部最好不要直接访问所依赖的外部模块,而是通过参数的方式传入模块,如下:

var module1 = (function($, module2)){
	//...
}(jQuery, module2);

目前通用的JS模块规范主要有两种CommonJS和AMD
CommonJS规范中以同步的方式加载模块,更多用再Node.js等服务端环境中,使用require()来加载模块,如下:

var add = require(‘math’).add;
add(val,1);

AMD规范中则是以异步方式加载模块,更多用在网络延迟存在的浏览器环境中,在实现上使用回调函数,将CommonJS的代码写成AMD规范,如下:

require(‘math’, function(math){
	var add = math.add;
	add(val,1);
})

目前实现AMD规范的JavaScript库主要有两个:requireJS和curl

使用高性能的变量或属性值读取方式
JavaScript的作用域和作用域链。
作用域就是变量或函数的作用范围,JS中最大的作用域是全局作用域。
JS中不存在块作用域,即大括号包含的区域不会成为一个单独的作用域,最小的作用域是函数。
在一个函数中定义的变量可以在这个函数的内部访问,包括函数内部定义的函数,这就意味着一个函数不仅可以访问自己内部定义的变量,还可以访问其外部函数或全局定义的变量。
多个函数嵌套使用时,就会形成作用域包含的关系,这个关系称为作用域链。
一个函数调用时,它的作用域就会随之初始化。
在一个函数内访问的变量,可能定义在从此函数的作用域起始的任何外部作用域上。
若函数在运行过程中遇到一个变量,就会判定从哪里取得数据值,在这个过程中,会顺着作用域链查找此名称的标识符,该搜索会从最近的作用域开始,如果找到了就使用这个变量,如果没有找到则会进入外层的作用域链中,如此反复,直到找到了此变量的定义,或者未找到而判定变量未定义为止。如下:

function update(){
	var imgs = document.getElementsByTagName(“img”);
	for(var i=0, len=img.length;i<len;i++){
		imgs[i].title = document.title + “ – image -+i;
	}
}

此函数运行时,要解析document变量,于是先在函数局部查找,然后在外部作用域查找,直到在全局作用域找到document定义为止。
这种变量定义的查找会影响到代码运行的性能,一个变量在作用域链上查找的层级越多则读取的速度越慢。
为了提高变量的读取性能,最佳的实践是尽量减少变量访问时在作用域链上查找的层级,最好是将变量定义为本作用域的局部变量,尽量不要使用全局变量。
如果需要频繁地访问一个外作用域的变量,最好是用一个局部变量保存外部变量,把多次的外部作用域变量访问变为一次外部作用域变量的访问。见下面的例子:

function update(){
	var doc = document
	var imgs = doc.getElementsByTagName(“img”);
	for(var i=0, len=img.length;i<len;i++){
		imgs[i].title = doc.title + “ – image -+i;
	}
}

上述代码使用局部变量doc保存了全局变量document,避免了多次的跨作用域查询。

try-catch表达式的catch块中也会产生一个作用域,访问外部变量时也会有性能的影响。最好将catch块中的处理交给一个函数,避免了在内部访问外部域的变量,如下:

try{
	//业务逻辑
}catch(ex){
	errorHandler(ex);
}

对象的属性值读取也存在和变量读取时类似的性能影响,读取对象上的属性值时也会搜索对象的原型链。
原型是JavaScript中的一个重要概念,是在JS中实现的一种特殊的继承机制,有别于传统的类继承机制, 因为在JS中没有类的概念。
在JS中,对象的构造函数中有一个prototype对象,即原型对象,这个对象上的属性或方法时共享给所有实例对象的。所以,实例对象上的属性和方法来自于两个地方:自身和对应的原型对象。
因为原型对象本身也可以是其他构造函数的示例对象,所以原型对象中的属性和方法也可能来自于其作为实例对象时对应的原型对象上。这就形成了一个由各原型对象组成的链条,称为原型链。
原型链的最顶端是构造函数Object中名为prototype的对象。
查找对象上的属性或方法时,首先会查找自身是否存在此属性或方法,如果未找到,则会继续在原型链上查找,直到找到或者未找到返回undefined值为止,如下示例:

function Person(name){
	this.name = name;
}

Person.prototype = { location: ‘China’}

var personA = new Person(‘name1’);
var personB = new Person(‘name2’);

alert(personA.location);
alert(personB.location);

在原型链上检索的层级越多,性能越差,即使是读取在对象上直接定义的属性也比读取局部变量慢。因此,如果在代码中频繁取得某个对象的属性值,尤其是此属性来自于对象的原型对象上,最佳的做法是把属性值缓存在局部变量中,提高读取对象属性的性能,例如下面的例子:

for(var i=0; i<number.length;i++){
	numbers[i] *=2;
}

在整个循环过程中,会反复读取numbers的length属性值,性能改进的方案就是使用一个局部变量缓存此属性值,修改代码如下

for(var i=0, len=numbers.length;i<len;i++){
	numbers[i] *=2;
}

总结如下,变量和对象属性或方法的读取会影响性能,尽量要将外作用域的变量或对象上的属性值缓存在局部变量中,以提高读取性能

高效的DOM操作
文档对象模型(DOM)是一个独立于特定寓言的应用程序接口。在富客户端网页应用中,界面上UI的更改都是通过DOM操作实现的。尽管DOM提供了丰富接口供外部调用,但DOM操作的代价很高,页面前端代码的性能瓶颈也大多集中在DOM操作上,前端性能优化的一个主要的关注点就是DOM操作的优化。
DOM操作优化的总体原则是尽量减少DOM操作

DOM操作对性能影响最大其实还是因为DOM导致了浏览器的重绘(repaint)和重排(reflow)
重绘指的是页面的某些部分要重新绘制,比如颜色或背景色,元素的位置和尺寸并没有改变。
重排则是元素的位置或尺寸发生了改变,浏览器需要重新计算渲染树,导致渲染树的一部分货全部发生变化。
重排的代价比重绘高很多,重绘影响部分元素,重排则有可能影响全部的元素。
虽然现代浏览器会针对重排或重绘做性能优化,但DOM操作带来的页面重绘或重排是不可避免的,但有办法可以降低其带来的影响。
1> 合并多次的DOM操作为单次的DOM操作
最常见的DOM操作是频繁修改DOM元素的样式,例如:

element.style.borderColor = ‘#f00’;
element.style.borderStyle = ‘solid’;
element.style.borderWidth =1px’;

这样会多次触发页面的重排或重绘。
推荐的方式是将DOM操作尽量合并
例如上面的代码可以优化为:

//方案1
element.style.cssText +=’border: 1px solid #f00’;

//方案2
element.className +=’empty’;

其中方案2比方案1稍微有一些性能上的损耗,但是方案2的维护性最好。
还有通过innerHTML接口修改DOM元素的内容,不要直接通过innerHTML拼接HTML代码,而是以字符串拼接好代码后,一次性赋值给DOM元素的innerHTML接口

2> 把DOM元素离线或隐藏后修改
这样处理后,只会在脱离和添加时,或者隐藏和显示时才会造成页面的重绘或重排。
这种方式适合那些需要大批量修改DOM元素的情况,具体方式有3中:

  1. 使用文档片段
    文档片段是轻量级的document对象,并不会和特定的页面关联。
    创建一个文档片段,并在此片段上进行必要的DOM操作,操作完后将它附加在页面中。对页面性能只存在最后把文件附加到页面的这一步操作上,如:
var fragment = document.createDocumentFragment();
document.getElementById(‘myElement’).appendChild(fragment);
  1. 通过设置DOM元素的display样式为none来隐藏元素
    经过大量的DOM操作后恢复元素原来的display样式。如:
var myElement = document.getElementById(‘myElement’);
myElement.style.display = ‘none’;
...
myElement.style.display = ‘block’;
  1. 克隆DOM元素到内存中
    把DOM元素克隆一份到内存中,然后在内存中操作,操作完成后使用此克隆元素替换页面中原来的DOM元素。代码如下:
var old = document.getElementById(‘myElement’);
var clone = old.cloneNode(true);
...
old.parentNode.replaceChild(clone, old);

3> 设置具有动画效果的DOM元素的position属性为fixed或absolute
把页面中具有动画效果的元素设置为绝对定位,使得元素脱离页面布局流,从而避免了页面频繁的重排。
4> 谨慎得获取DOM元素的布局信息
获取DOM的布局信息会有性能的损耗,如果存在重复调用,最佳的做法是尽量把这些值缓存在局部变量中,如下:

for(var i=0;i<len;i++){
	myElements[i].style.top = targetElement.offsetTop+i*5+’px’;
}

上面的代码中,会在一个循环中反复的取得一个元素的offsetTop值。优化的方案是在循环外部取得元素的offsetTop值,这样只调用了一遍元素的offsetTop值,更改后代码如下:

var targetTop = targetElement.offsetTop;
for(var i=0;i<len;i++){
	myElements[i].style.top = targetTop +i*5+’px’;
}

在有大批量DOM操作时,应避免获取DOM元素的布局信息。如果需要这些布局信息,最好是在DOM操作之前就获得,如下:

var newWidth = div1.offsetWidth + 10;
var newHeight = myElement.offsetHeight+10;

div1.style.width = newWidth+’px’;
myElement.style.height = newHeight+’px’;

5> 使用事件托管方式绑定事件
在DOM元素上绑定事件会影响页面的性能,在页面上绑定的时间越少越好,最好的方式是使用事件托管方式,即利用事件冒泡机制,只在父元素上绑定事件处理,用于处理所有子元素的事件,在事件处理函数中根据传入的参数判断事件源元素,针对不同的源元素做不同的处理。
这种方式也有很大的灵活性,可以很方便地添加或删除子元素,不要考虑因元素移除或改动而需要修改事件绑定。示例代码如下:

document.getElementById(‘list’).addEventListener(“click”,function(e){
	//检查事件源元素
	if(e.target && e.target.nodeName.toUpperCase ==LI){
		//针对子元素的处理
		...
}})

上述代码只在父元素上绑定了click事件,当单击子节点时,click事件会冒泡,父节点捕获事件后通过e.target检查事件源元素并做相应的处理。

在JS中事件绑定方式存在浏览器兼容问题,所以在很多框架中也提供了相似的接口方式用于事件托管。在jQuery中就有这样的方式实现事件托管。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值