前端编码规范(3)JavaScript 开发规范
JavaScript规范
变量声明
总是使用var来声明变量。如不指定var,变量将被隐式地声明为全局变量,这将对变量难以控制。如果没有声明,变量处于什么定义域就变得不清(可以是在Document或Window中,也可以很容易地进入本地定义域)。所以,请总是使用var来声明变量。
采用严格模式带来的好处是,当你手误输入错误的变量名时,它可以通过报错信息来帮助你定位错误出处。
变量名
变量名推荐使用驼峰法来命名(camelCase)
全局变量为大写(UPPERCASE)
常量(如PI)为大写(UPPERCASE)
函数:构造函数始终以大写字母开头,非构造函数以一个小写字母开头
变量名不要以$作为开始标记,避免与很多JavaScript库冲突
全局命名空间污染与IIFE
总是将代码包裹成一个IIFE(Immediately-InvokedFunctionExpression),用以创建独立隔绝的定义域。这一举措可防止全局命名空间被污染。
IIFE还可确保你的代码不会轻易被其它全局命名空间里的代码所修改(i.e.第三方库,window引用,被覆盖的未定义的关键字等等)。
不推荐 varx=10, y=100; //Declaringvariablesintheglobalscopeisresultinginglobalscopepollution.Allvariablesdeclaredlikethis //willbestoredinthewindowobject.Thisisveryuncleanandneedstobeavoided. console.log(window.x+‘‘+window.y); 推荐 //WedeclareaIIFEandpassparametersintothefunctionthatwewillusefromtheglobalspace (function(log,w,undefined){ ‘usestrict'; varx=10, y=100; //Willoutput‘truetrue' log((w.x===undefined)+‘‘+(w.y===undefined)); }(window.console.log,window));
IIFE(立即执行的函数表达式)
无论何时,想要创建一个新的封闭的定义域,那就用IIFE。它不仅避免了干扰,也使得内存在执行完后立即释放。
所有脚本文件建议都从IIFE开始。
立即执行的函数表达式的执行括号应该写在外包括号内。虽然写在内还是写在外都是有效的,但写在内使得整个表达式看起来更像一个整体,因此推荐这么做。
不推荐 (function(){})(); 推荐 (function(){}()); so,用下列写法来格式化你的IIFE代码: (function(){ ‘usestrict'; //Codegoeshere }());
如果你想引用全局变量或者是外层IIFE的变量,可以通过下列方式传参:
(function($,w,d){ ‘usestrict'; $(function(){ w.alert(d.querySelectorAll(‘div').length); }); }(jQuery,window,document));
严格模式
ECMAScript5严格模式可在整个脚本或独个方法内被激活。它对应不同的javascript语境会做更加严格的错误检查。严格模式也确保了javascript代码更加的健壮,运行的也更加快速。
严格模式会阻止使用在未来很可能被引入的预留关键字。
你应该在你的脚本中启用严格模式,最好是在独立的IIFE中应用它。避免在你的脚本第一行使用它而导致你的所有脚本都启动了严格模式,这有可能会引发一些第三方类库的问题。
不推荐 //Scriptstartshere ‘usestrict'; (function(){ //Yourcodestartshere }()); 推荐 (function(){ ‘usestrict'; //Yourcodestartshere }());
变量声明
总是使用var来声明变量。如不指定var,变量将被隐式地声明为全局变量,这将对变量难以控制。如果没有声明,变量处于什么定义域就变得不清(可以是在Document或Window中,也可以很容易地进入本地定义域)。所以,请总是使用var来声明变量。
采用严格模式带来的好处是,当你手误输入错误的变量名时,它可以通过报错信息来帮助你定位错误出处。
不推荐 x=10; y=100; 推荐 varx=10, y=100;
理解JavaScript的定义域和定义域提升
在JavaScript中变量和方法定义会自动提升到执行之前。JavaScript只有function级的定义域,而无其他很多编程语言中的块定义域,所以使得你在某一function内的某语句和循环体中定义了一个变量,此变量可作用于整个function内,而不仅仅是在此语句或循环体中,因为它们的声明被JavaScript自动提升了。
我们通过例子来看清楚这到底是怎么一回事:
原function
(function(log){ ‘usestrict'; vara=10; for(vari=0;i<a;i++){ varb=i*i; log(b); } if(a===10){ varf=function(){ log(a); }; f(); } functionx(){ log(‘Mr.X!'); } x(); }(window.console.log));
被JS提升过后
(function(log){ ‘usestrict'; //Allvariablesusedintheclosurewillbehoistedtothetopofthefunction vara, i, b, f; //Allfunctionsintheclosurewillbehoistedtothetop functionx(){ log(‘Mr.X!'); } a=10; for(i=0;i<a;i++){ b=i*i; log(b); } if(a===10){ //Functionassignmentswillonlyresultinhoistedvariablesbutthefunctionbodywillnotbehoisted //Onlybyusingarealfunctiondeclarationthewholefunctionwillbehoistedwithitsbody f=function(){ log(a); }; f(); } x(); }(window.console.log));
根据以上提升过程,你是否可理解以下代码?
有效代码
(function(log){ ‘usestrict'; vara=10; i=5; x(); for(vari;i<a;i++){ log(b); varb=i*i; } if(a===10){ f=function(){ log(a); }; f(); varf; } functionx(){ log(‘Mr.X!'); } }(window.console.log));
正如你所看到的这段令人充满困惑与误解的代码导致了出人意料的结果。只有良好的声明习惯,也就是下一章节我们要提到的声明规则,才能尽可能的避免这类错误风险。
提升声明
为避免上一章节所述的变量和方法定义被自动提升造成误解,把风险降到最低,我们应该手动地显示地去声明变量与方法。也就是说,所有的变量以及方法,应当定义在function内的首行。
只用一个var关键字声明,多个变量用逗号隔开。
不推荐 (function(log){ ‘usestrict'; vara=10; varb=10; for(vari=0;i<10;i++){ varc=a*b*i; } functionf(){ } vard=100; varx=function(){ returnd*d; }; log(x()); }(window.console.log)); 推荐 (function(log){ ‘usestrict'; vara=10, b=10, i, c, d, x; functionf(){ } for(i=0;i<10;i++){ c=a*b*i; } d=100; x=function(){ returnd*d; }; log(x()); }(window.console.log));
把赋值尽量写在变量申明中。
不推荐 vara, b, c; a=10; b=10; c=100; 推荐 vara=10, b=10, c=100;
总是使用带类型判断的比较判断
总是使用===精确的比较操作符,避免在判断的过程中,由JavaScript的强制类型转换所造成的困扰。
如果你使用===操作符,那比较的双方必须是同一类型为前提的条件下才会有效。
如果你想了解更多关于强制类型转换的信息,你可以读一读DmitrySoshnikov的这篇文章。
在只使用==的情况下,JavaScript所带来的强制类型转换使得判断结果跟踪变得复杂,下面的例子可以看出这样的结果有多怪了:
(function(log){ ‘usestrict'; log(‘0'==0);//true log(”==false);//true log(‘1'==true);//true log(null==undefined);//true varx={ valueOf:function(){ return‘X'; } }; log(x==‘X'); }(window.console.log));
明智地使用真假判断
当我们在一个if条件语句中使用变量或表达式时,会做真假判断。if(a==true)是不同于if(a)的。后者的判断比较特殊,我们称其为真假判断。这种判断会通过特殊的操作将其转换为true或false,下列表达式统统返回false:false,0,undefined,null,NaN,”(空字符串).
这种真假判断在我们只求结果而不关心过程的情况下,非常的有帮助。
以下示例展示了真假判断是如何工作的:
(function(log){ ‘usestrict'; functionlogTruthyFalsy(expr){ if(expr){ log(‘truthy'); }else{ log(‘falsy'); } } logTruthyFalsy(true);//truthy logTruthyFalsy(1);//truthy logTruthyFalsy({});//truthy logTruthyFalsy([]);//truthy logTruthyFalsy(‘0');//truthy logTruthyFalsy(false);//falsy logTruthyFalsy(0);//falsy logTruthyFalsy(undefined);//falsy logTruthyFalsy(null);//falsy logTruthyFalsy(NaN);//falsy logTruthyFalsy(”);//falsy }(window.console.log));
变量赋值时的逻辑操作
逻辑操作符||和&&也可被用来返回布尔值。如果操作对象为非布尔对象,那每个表达式将会被自左向右地做真假判断。基于此操作,最终总有一个表达式被返回回来。这在变量赋值时,是可以用来简化你的代码的。
不推荐
if(!x){ if(!y){ x=1; }else{ x=y; } }
推荐
x=x||y||1;
这一小技巧经常用来给方法设定默认的参数。
(function(log){ 'usestrict'; functionmultiply(a,b){ a=a||1; b=b||1; log('Result'+a*b); } multiply();//Result1 multiply(10);//Result10 multiply(3,NaN);//Result3 multiply(9,5);//Result45 }(window.console.log));
分号
总是使用分号,因为隐式的代码嵌套会引发难以察觉的问题。当然我们更要从根本上来杜绝这些问题[1]。以下几个示例展示了缺少分号的危害:
//1. MyClass.prototype.myMethod=function(){ return42; }//Nosemicolonhere. (function(){ //Someinitializationcodewrappedinafunctiontocreateascopeforlocals. })(); varx={ 'i':1, 'j':2 }//Nosemicolonhere. //2.TryingtodoonethingonInternetExplorerandanotheronFirefox. //Iknowyou'dneverwritecodelikethis,butthrowmeabone. [ffVersion,ieVersion][isIE](); varTHINGS_TO_EAT=[apples,oysters,sprayOnCheese]//Nosemicolonhere. //3.conditionalexecutionalabash -1==resultOfOperation()||die();
Sowhathappens?
JavaScript错误——首先返回42的那个function被第二个function当中参数传入调用,接着数字42也被“调用”而导致出错。
八成你会得到‘nosuchpropertyinundefined'的错误提示,因为在真实环境中的调用是这个样子:x[ffVersion,ieVersion][isIE]().
die总是被调用。因为数组减1的结果是NaN,它不等于任何东西(无论resultOfOperation是否返回NaN)。所以最终的结果是die()执行完所获得值将赋给THINGS_TO_EAT.
Why?
JavaScript中语句要以分号结束,否则它将会继续执行下去,不管换不换行。以上的每一个示例中,函数声明或对象或数组,都变成了在一句语句体内。要知道闭合圆括号并不代表语句结束,JavaScript不会终结语句,除非它的下一个token是一个中缀符[2]或者是圆括号操作符。
这真是让人大吃一惊,所以乖乖地给语句末加上分号吧。
澄清:分号与函数
分号需要用在表达式的结尾,而并非函数声明的结尾。区分它们最好的例子是:
varfoo=function(){ returntrue; };//semicolonhere. functionfoo(){ returntrue; }//nosemicolonhere.
嵌套函数
嵌套函数是非常有用的,比如用在持续创建和隐藏辅助函数的任务中。你可以非常自由随意地使用它们。
语句块内的函数声明
切勿在语句块内声明函数,在ECMAScript5的严格模式下,这是不合法的。函数声明应该在定义域的顶层。但在语句块内可将函数申明转化为函数表达式赋值给变量。
不推荐
if(x){ functionfoo(){} }
推荐
if(x){ varfoo=function(){}; }
异常
基本上你无法避免出现异常,特别是在做大型开发时(使用应用开发框架等等)。
在没有自定义异常的情况下,从有返回值的函数中返回错误信息一定非常的棘手,更别提多不优雅了。不好的解决方案包括了传第一个引用类型来接纳错误信息,或总是返回一个对象列表,其中包含着可能的错误对象。以上方式基本上是比较简陋的异常处理方式。适时可做自定义异常处理。
在复杂的环境中,你可以考虑抛出对象而不仅仅是字符串(默认的抛出值)。
if(name===undefined){ throw{ name:'SystemError', message:'Anameshouldalwaysbespecified!' } }
标准特性
总是优先考虑使用标准特性。为了最大限度地保证扩展性与兼容性,总是首选标准的特性,而不是非标准的特性(例如:首选string.charAt(3)而不是string[3];首选DOM的操作方法来获得元素引用,而不是某一应用特定的快捷方法)。
简易的原型继承
如果你想在JavaScript中继承你的对象,请遵循一个简易的模式来创建此继承。如果你预计你会遇上复杂对象的继承,那可以考虑采用一个继承库,比如Proto.jsbyAxelRauschmayer.
简易继承请用以下方式:
(function(log){ 'usestrict'; //Constructorfunction functionApple(name){ this.name=name; } //Definingamethodofapple Apple.prototype.eat=function(){ log('Eating'+this.name); }; //Constructorfunction functionGrannySmithApple(){ //Invokingparentconstructor Apple.prototype.constructor.call(this,'GrannySmith'); } //SetparentprototypewhilecreatingacopywithObject.create GrannySmithApple.prototype=Object.create(Apple.prototype); //Setconstructortothesubtype,otherwisepointstoApple GrannySmithApple.prototype.constructor=GrannySmithApple; //Callingasupermethod GrannySmithApple.prototype.eat=function(){ //Besuretoapplyitontoourcurrentobjectwithcall(this) Apple.prototype.eat.call(this); log('PoorGranySmith'); }; //Instantiation varapple=newApple('TestApple'); vargrannyApple=newGrannySmithApple(); log(apple.name);//TestApple log(grannyApple.name);//GrannySmith //Instancechecks log(appleinstanceofApple);//true log(appleinstanceofGrannySmithApple);//false log(grannyAppleinstanceofApple);//true log(grannyAppleinstanceofGrannySmithApple);//true //Callingmethodthatcallssupermethod grannyApple.eat();//EatingGrannySmith\nPoorGranySmith }(window.console.log));
使用闭包
闭包的创建也许是JS最有用也是最易被忽略的能力了。关于闭包如何工作的合理解释。也可以搜索毛票票以前发布的文章
切勿在循环中创建函数
在简单的循环语句中加入函数是非常容易形成闭包而带来隐患的。下面的例子就是一个典型的陷阱:
不推荐
(function(log,w){ 'usestrict'; //numbersandiisdefinedinthecurrentfunctionclosure varnumbers=[1,2,3], i; for(i=0;i<numbers.length;i++){ w.setTimeout(function(){ //Atthemomentwhenthisgetsexecutedtheivariable,comingfromtheouterfunctionscope //issetto3andthecurrentprogramisalertingthemessage3times //'Index3withnumberundefined //Ifyouunderstandclosuresinjavascriptyouknowhowtodealwiththosecases //It'sbesttojustavoidfunctions/newclosuresinloopsasthispreventsthoseissues w.alert('Index'+i+'withnumber'+numbers[i]); },0); } }(window.console.log,window));
接下来的改进虽然已经解决了上述例子中的问题或bug,但还是违反了不在循环中创建函数或闭包的原则。
不推荐
(function(log,w){ 'usestrict'; //numbersandiisdefinedinthecurrentfunctionclosure varnumbers=[1,2,3], i; for(i=0;i<numbers.length;i++){//CreatinganewclosurescopewithanIIFEsolvestheproblem//Thedelayedfunctionwilluseindexandnumberwhichare//intheirownclosurescope(oneclosureperloopiteration).//---//Stillthisisnotrecommendedasweviolateourruletonot//createfunctionswithinloopsandwearecreatingtwo!(function(index,number){w.setTimeout(function(){//Willoutputasexpected0>1,1>2,2>3 w.alert('Index'+index+'withnumber'+number); },0); }(i,numbers[i])); } }(window.console.log,window));
接下来的改进已解决问题,而且也遵循了规范。可是,你会发现看上去似乎过于复杂繁冗了,应该会有更好的解决方案吧。
不完全推荐
(function(log,w){ 'usestrict'; //numbersandiisdefinedinthecurrentfunctionclosure varnumbers=[1,2,3], i; //Createafunctionoutsideoftheloopthatwillacceptargumentstocreatea //functionclosurescope.Thisfunctionwillreturnafunctionthatexecutesinthis //closureparentscope. functionalertIndexWithNumber(index,number){ returnfunction(){ w.alert('Index'+index+'withnumber'+number); }; } //Firstparameterisafunctioncallthatreturnsafunction. //--- //Thissolvesourproblemandwedon'tcreateafunctioninsideourloop for(i=0;i<numbers.length;i++){ w.setTimeout(alertIndexWithNumber(i,numbers[i]),0); } }(window.console.log,window));
将循环语句转换为函数执行的方式问题能得到立马解决,每一次循环都会对应地创建一次闭包。函数式的风格更加值得推荐,而且看上去也更加地自然和可预料。
推荐
(function(log,w){ 'usestrict'; //numbersandiisdefinedinthecurrentfunctionclosure varnumbers=[1,2,3], i; numbers.forEach(function(number,index){ w.setTimeout(function(){ w.alert('Index'+index+'withnumber'+number); },0); }); }(window.console.log,window));
eval函数(魔鬼)
eval()不但混淆语境还很危险,总会有比这更好、更清晰、更安全的另一种方案来写你的代码,因此尽量不要使用evil函数。
this关键字
只在对象构造器、方法和在设定的闭包中使用this关键字。this的语义在此有些误导。它时而指向全局对象(大多数时),时而指向调用者的定义域(在eval中),时而指向DOM树中的某一节点(当用事件处理绑定到HTML属性上时),时而指向一个新创建的对象(在构造器中),还时而指向其它的一些对象(如果函数被call()和apply()执行和调用时)。
正因为它是如此容易地被搞错,请限制它的使用场景:
在构造函数中
在对象的方法中(包括由此创建出的闭包内)
首选函数式风格
函数式编程让你可以简化代码并缩减维护成本,因为它容易复用,又适当地解耦和更少的依赖。
接下来的例子中,在一组数字求和的同一问题上,比较了两种解决方案。第一个例子是经典的程序处理,而第二个例子则是采用了函数式编程和ECMAScript5.1的数组方法。
例外:往往在重代码性能轻代码维护的情况之下,要选择最优性能的解决方案而非维护性高的方案(比如用简单的循环语句代替forEach)。
不推荐
(function(log){ 'usestrict'; vararr=[10,3,7,9,100,20], sum=0, i; for(i=0;i<arr.length;i++){ sum+=arr[i]; } log('Thesumofarray'+arr+'is:'+sum) }(window.console.log));
推荐
(function(log){ 'usestrict'; vararr=[10,3,7,9,100,20]; varsum=arr.reduce(function(prevValue,currentValue){ returnprevValue+currentValue; },0); log('Thesumofarray'+arr+'is:'+sum); }(window.console.log));
另一个例子通过某一规则对一个数组进行过滤匹配来创建一个新的数组。
不推荐
(function(log){ 'usestrict'; varnumbers=[11,3,7,9,100,20,14,10], numbersGreaterTen=[], i; for(i=0;i<numbers.length;i++){if(numbers[i]>10){ numbersGreaterTen.push(numbers[i]); } } log('Fromthelistofnumbers'+numbers+'only'+numbersGreaterTen+'aregreaterthanten'); }(window.console.log));
推荐
(function(log){ 'usestrict'; varnumbers=[11,3,7,9,100,20,14,10]; varnumbersGreaterTen=numbers.filter(function(element){ returnelement>10; }); log('Fromthelistofnumbers'+numbers+'only'+numbersGreaterTen+'aregreaterthanten'); }(window.console.log));
使用ECMAScript5
建议使用ECMAScript5中新增的语法糖和函数。这将简化你的程序,并让你的代码更加灵活和可复用。
数组和对象的属性迭代
用ECMA5的迭代方法来迭代数组。使用Array.forEach或者如果你要在特殊场合下中断迭代,那就用Array.every。
(function(log){ 'usestrict'; //Iterateoveranarrayandbreakatacertaincondition [1,2,3,4,5].every(function(element,index,arr){ log(element+'atindex'+index+'inarray'+arr); if(index!==5){ returntrue; } }); //Definingasimplejavascriptobject varobj={ a:'A', b:'B', 'c-d-e':'CDE' }; //Iteratingovertheobjectkeys Object.keys(obj).forEach(function(element,index,arr){ log('Key'+element+'hasvalue'+obj[element]); }); }(window.console.log));
不要使用switch
switch在所有的编程语言中都是个非常错误的难以控制的语句,建议用ifelse来替换它。
数组和对象字面量
用数组和对象字面量来代替数组和对象构造器。数组构造器很容易让人在它的参数上犯错。
不推荐
//Lengthis3. vara1=newArray(x1,x2,x3); //Lengthis2. vara2=newArray(x1,x2); //Ifx1isanumberanditisanaturalnumberthelengthwillbex1. //Ifx1isanumberbutnotanaturalnumberthiswillthrowanexception. //Otherwisethearraywillhaveoneelementwithx1asitsvalue. vara3=newArray(x1); //Lengthis0. vara4=newArray();
正因如此,如果将代码传参从两个变为一个,那数组很有可能发生意料不到的长度变化。为避免此类怪异状况,请总是采用更多可读的数组字面量。
推荐
vara=[x1,x2,x3]; vara2=[x1,x2]; vara3=[x1]; vara4=[];
对象构造器不会有类似的问题,但是为了可读性和统一性,我们应该使用对象字面量。
不推荐
varo=newObject(); varo2=newObject(); o2.a=0; o2.b=1; o2.c=2; o2['strangekey']=3;
应该写成这样:
推荐
varo={}; varo2={ a:0, b:1, c:2, 'strangekey':3 };
修改内建对象的原型链
修改内建的诸如Object.prototype和Array.prototype是被严厉禁止的。修改其它的内建对象比如 Function.prototype,虽危害没那么大,但始终还是会导致在开发过程中难以debug的问题,应当也要避免。
自定义toString()方法
你可以通过自定义toString()来控制对象字符串化。这很好,但你必须保证你的方法总是成功并不会有其它副作用。如果你的方法达不到这样的标准,那将会引发严重的问题。如果toString()调用了一个方法,这个方法做了一个断言[3],当断言失败,它可能会输出它所在对象的名称,当然对象也需要调用 toString()。
圆括号
一般在语法和语义上真正需要时才谨慎地使用圆括号。不要用在一元操作符上,例如delete,typeof和 void,或在关键字之后,例如return,throw,case,new等。
字符串
统一使用单引号(‘),不使用双引号(“)。这在创建HTML字符串非常有好处:
varmsg='ThisissomeHTML<divclass="makes-sense"></div>';
三元条件判断(if的快捷方法)
用三元操作符分配或返回语句。在比较简单的情况下使用,避免在复杂的情况下使用。没人愿意用10行三元操作符把自己的脑子绕晕。
不推荐
if(x===10){ return'valid'; }else{ return'invalid'; }
推荐
returnx===10?'valid':'invalid';
[1]:作者指的是采用严格规范的语句写法,从根本上杜绝由分号缺失而引起的代码歧义。
[2]:中缀符,指的是像x+y中的+。
[3]:断言一般指程序员在测试测序时的假设,一般是一些布尔表达式,当返回是true时,断言为真,代码运行会继续进行;如果条件判断为false,代码运行停止,你的应用被终止。