本章主要帮助大家写出高质量的JS代码的方法,模式和习惯,例如:避免使用全局变量,使用单个的var变量声明,缓存for循环的长度变量length等
一、尽量避免使用全局变量
1 每一个js环境都有一个全局对象,通过this可以访问,创建的每一个全局变量都归这个全局对象所有,在浏览器中,这个全局对象this等于window(在其他环境中,this对象不一定是window对象)
var sss="sss"; this.sss;//"sss" window.sss;//"sss"
1.1全局变量导致的问题
1,与第三方JS库发生命名冲突;2,与广告合作伙伴的脚本发生命名冲突;3,与来自第三方的统计脚本或者分析脚本发生命名冲突;4,代码移植,如果你的代码换个环境执行,可能与另外一个环境的代码相冲突
2, 全局变量总是出现的原因:1,JS有暗示全局变量的概念,即任何变量,如果没有声明过,那么就是全局变量;2,隐式的创建了全局变量反模式--使用var声明的链式赋值
var a=b=0; //这一切的原因是操作符的优先级,=操作符的优先级是从右向左,所以上面的例子,实际上相当于 //var a=(b=0);因此相当于隐式地创建了全局变量b,
/*正确的写法:
*var a,b,c; *a=b=c=0;
*/
1.2 变量释放(即delete删除变量)时的副作用
1,隐含的全局变量(即不声明而直接使用的变量)与明确定义的全局变量(即使用var定义的全局变量)的不同之处在于,能否使用delete操作符删除该变量
首先我们理解一下delete操作符,delete操作符是用来删除对象的属性的,使用var定义的全局变量,不能被删除,而隐含的全局变量是可以删除的,因此隐含的全局变量其实并不是真正意义上的变量,而是作为全局对象的属性存在的
var m="111"; delete m;//false console.log(m);//111 fff="d"; delete fff;//true console.log(fff);//Uncaught ReferenceError: fff is not defined
1.3 访问全局对象
按照以下方式获取全局对象,因为函数的调用(这里不包括使用new操作符执行的函数)一般都指向全局对象
但是在ECMAScript5的严格模式下不能如此使用,严格模式下对全局对象的访问:将库代码打包到一个直接函数,然后传递一个引用给this,即把this看成传递到直接函数的一个参数。
var global=(function(){
return this;
})();
1.4 单一var模式:只使用一个var,在函数的顶部对该函数中所有的变量进行声明
优点:
1、在一个位置可以查找到函数所需要的所有局部变量;
2、防止变量未声明就使用;
3、更少编码;
4、声明的时候进行初始化,防止在后期使用时出现逻辑错误(数字变量当成字符串这样低级的错误)
5、将DOM引用赋值给局部变量,不需要每次都去重新进行DOM搜索,可大量节约时间
function fun(){
var a=1,
b=2,
my={},
i,
j;
//函数体
}
function fun(){
var element=document.getElementById("result"),
style=element.style;
}
1.5、提升:无论在js函数内的任意位置声明的变量,效果都等同于在函数顶部进行声明
JS允许在函数中的任意位置声明变量,但无论在哪里声明最终都会被提升到函数的顶部,因此先使用后声明可能会导致逻辑问题
js分为预编译阶段与执行阶段,
预编译阶段:
1 对使用function语句声明的函数进行处理,不仅按照函数名按照变量标识符进行索引,对函数体也进行处理(即对函数名(也可以认为是变量名)进行赋值),如果出现同名函数,则会用后者覆盖前者(即后者的赋值覆盖前者)
2 对匿名函数在此阶段视而不见
3 对使用var声明的变量进行索引,但是对变量的初始化忽略掉,
在预编译阶段遇到var声明的变量,如果前面没有做过初始化,就是undefined,如果该变量在前面是函数名,那么其值就是函数体,这里不做覆盖
function fun(){} /*变量名fun在预编译阶段,不仅对fun这个变量名进行索引,对其函数体也进行了处理,即fun的值为函数体*/ console.log(fun);//function() var fun="123456"; /*使用var声明的变量名fun在预编译阶段,只对其变量名进行索引,不对其进行赋值,所以这fun的初始化值不会覆盖上面的函数体,但是在执行阶段会覆盖*/ console.log(fun);//123456
执行阶段:
1 对匿名函数按表达式逐行进行解释执行
2 为预编译阶段索引的变量读取初始值,由于执行阶段是逐行解释执行的,所以如果在赋值语句前面进行调用的话,值应该为预编译阶段的
function fun(){
alert(myName);//undefined
var myName="local";
alert(myName);//local
}
fun();
二、for循环
for循环一般用于遍历数组或者类数组对象(arguments,html容器等)
优化点:1 对数组的长度进行缓存,var len=arr.length
2 使用单一的var模式,把所有的变量都提到函数体的开始,使用一个var进行声明
3 i++替换掉i=i+1与i+=1
4 在没有要求的时候,递减到0,即i--
1 一般情况下的for循环
for(var i=0;i<arr.length;i++){
//对数组或者类数组对象中的元素的操作
}
缺点:显然每次循环都会计算一下要访问数据的长度,这样效率就会降低,尤其是当访问的是HTML容器对象时
改进:将要访问数据的长度缓存起来,对访问速度的提升相当显著,ie可提高170倍
for(var i=0,max=ayy.length;i<max;i++){
}
2 进一步改进:结合我们前面提到单var变量模式
function fun(){ var i=0,ayy=[],max;//将所有该函数中用到的变量都声明在函数的顶部 for(i=0,max=ayy.length;i<max;i++){ } }
该模式的缺点:复制粘贴的时候,要保证将所需变量的声明全部复制进去
3 JSLint推荐使用++与--,即逐步递增或者递减(这个有不同的意见,可以保留)
4 更进一步改进:从最后一个元素,逐个遍历到第一个元素,将i与0比较,比i与非0的max比较效率要高
var a,b,c,d,e,f; a=+new Date(); for(i=10000000;i>=0;i--){ } b=+new Date(); console.log(b-a); //4508 c=+new Date(); for(j=0;j<=10000000;j++){ } d=+new Date(); console.log(d-c); //4310 e=+new Date(); for(k=10000000;k--;){ } f=+new Date(); console.log(f-e);//3151
三、for-in循环
for-in主要用于遍历非数组对象,称之为枚举
1 当要遍历对象属性,并过滤掉原型中的属性和方法时,使用hasOwnProperty()方法
var man={
a:1,
b:2,
c:3
},i;
for(i in man){
if(man.hasOwnProperty(i)){//当确定不了对象属性和原型中的内容时,使用hasOwnProperty方法加以判断,如果可以确定则可以省略该判断,提高效率
console.log(i+":"+man[i]);
}
}
2 在上面我们提到对原型链中的属性和方法进行过滤时,使用hasOwnProperty方法,由于hasOwnProperty方法是属于Object原型的方法,所以我们可以这样使用,这样避免命名冲突(即man对象也有一个hasOwnProperty方法,那么就与我们想要过滤使用的hasOwnProperty方法冲突了)
var i,hasOwn=Object.prototype.hasOwnProperty,man={ "name":"jim", "age":12 }; for(i in man){ if(hasOwn.call(man,i)){ console.log(i+":"+man[i]); } }
var i,hasOwn=Object.prototype.hasOwnProperty,man={
"name":"jim",
"age":12,
hasOwnProperty:function(){console.log("man's hasownproperty")}
};
for(i in man){
if(man.hasOwnProperty(i)){
console.log(i+":"+man[i]);
}
}
四、不要给内置对象的原型增加方法(这里的内置对象指的是Date,Math,Array,String,Event,Object等)
我们经常给构造函数的原型增一些方法,但是给js的内置对象的构造函数的原型增加方法会严重影响可维护性,因此这里不推荐
下面的几种情况例外
1,可以添加ECMAScript5中描述的确尚未实现的方法,等待ECMAScript加以实现
2,某些浏览器的JS引擎已经实现了该方法
3,写成文档形式,与团队充分沟通
给内置对象添加自定义方法
if(typeof Object.prototype.MyMethod!=="function"){
Object.prototype.MyMethod=function(){
};
}
五、switch模式
1 每个case语句结尾都有一个break
2 使用default来作为switch的结束
六、避免使用隐式类型转换
1 什么是隐式类型转换:false==0、""==false
2 为了避免出现上面的情况,我们在使用比较语句的时候尽量使用===和!==
6.1 避免使用eval
1 eval可以将任何的字符串当作js代码执行
2 将ajax返回的数据字符串转换成对象时,不推荐使用eval,推荐JSON.parse或者JSON.org网站的类库,因为eval执行的字符串可能是一家被篡改过的代码
3 setInterval/setTimeout传递参数时,也会导致类似于eval的隐患,因此尽量避免使用
4 new Function()与eval和相似,使用时要小心
如果一定要使用eval,可以使用new Function来替代,因为new Function将在局部函数空间运行,因此代码中var定义的变量不会成为全局变量
另外一个避免eval中使用var定义的变量成为全局变量,将eval放到一个即时函数中
new Function(或者Function),这个方法是只看到全局变量,对局部变量影响较小
setTimeout("fun(1,2,3)",100);//反模式 //推荐模式 setTimeout(function(){ fun(1,2,3); },100);
var jsString1='var aaaaaa=1;console.log(aaaaaa);'; var jsString2='var bbbbbbb=2;console.log(bbbbbbb);'; var jsString3='var ccccccc=3;console.log(ccccccc);'; eval(jsString1);//1 new Function(jsString2)();//2new Function将在局部函数空间运行
/*另外一个避免eval中使用var定义的变量成为全局变量,将eval放到一个即时函数中*/ (function(){ eval(jsString3); })();//3 console.log(aaaaaa);//1eval代码里面var定义的变量会成为全局变量 console.log(bbbbbbb);//ReferenceError: ccccccc is not defined console.log(ccccccc);//ReferenceError: ccccccc is not defined
new Function(或者Function),这个方法是只看到全局变量,对局部变量影响较小
function fff(){ var bbb='bbb'; var jsString='console.log(bbb);' new Function(jsString)();//或者Function(jsString)() } fff();//ReferenceError: bbb is not defined
var a="global a"; function fff(){ var a='local a'; var jsString='console.log(a);' new Function(jsString)();//或者Function(jsString)() } fff();//global a
七、使用parseInt方法时,第二个参数尽量不要省略
八、编码约定
8.1 缩进:使用tab键进行缩进
8.2 大括号:for与if语句最好都使用大括号
8.3 开放大括号的位置:和语句放在同一行
/*根据分号插入机制,也就是一行结束如果后面没有分号,会自动插入分号*/ return { "name":"Amy", "age":18 }; /*相当于 return ; { "name":"Amy", "age":18 };
*因此下面这种方式更合适 */
return { "name":"Amy", "age":18 };
九、命名约定
9.1 构造函数的首字母大写
9.2 分隔单词:函数/方法名:小驼峰式;变量:小写字母,下划线分隔;常量:全部大写;私有变量/方法:下划线做前缀
十、编写注释
十一、编写API文档
十二、编写可读性强的代码
十三、同行互查
十四、在正式发布时精简代码
十五、运行JSLint