|
良好的編碼習(xí)慣,這是每個(gè)程序員應(yīng)具備的最基本素質(zhì)。無(wú)論是前端程序員還是后端程序員,都要遵循基本的規(guī)范,減少因代碼混亂而造成難以維護(hù)的局面。要做到不管有多少人共同參與同一個(gè)項(xiàng)目,一定要確保每一行代碼都像是同一個(gè)人編寫(xiě)的。 提高代碼的可讀性和可維護(hù)性,有一些共同的方法,比如注意代碼格式整齊,縮進(jìn)合理,規(guī)范的命名等等。但有一些方式還是和所使用語(yǔ)言本書(shū)的特性有關(guān)。JavaScript是一種弱類型語(yǔ)言,有著相對(duì)松散的限制,這種特點(diǎn)使得開(kāi)發(fā)者可以更靈活更高效地編寫(xiě)JavaScript代碼。但同時(shí)也存在著一些設(shè)計(jì)上的缺陷,使得開(kāi)發(fā)者很容易編寫(xiě)帶有潛在問(wèn)題的代碼。JavaScript引擎在運(yùn)行這些有潛在問(wèn)題的代碼時(shí)可能并不會(huì)報(bào)錯(cuò)或者警告,所以發(fā)現(xiàn)這些問(wèn)題就變的很困難。這些形式各異、隱含有邏輯錯(cuò)誤的代碼,影響著代碼整體的可讀性和可維護(hù)性。因而,需要使用更嚴(yán)格的編碼規(guī)范,避免出現(xiàn)這些不合規(guī)范的代碼帶來(lái)錯(cuò)誤。如下是提高JavaScript代碼可維護(hù)性的一些最佳實(shí)踐方法 1. 避免定義全局變量或函數(shù) 定義全局的變量和函數(shù),會(huì)影響代碼的可維護(hù)性。如果在頁(yè)面中運(yùn)行的JavaScript代碼是在相同的作用域里面,那這就意味著代碼之間的定義存在互相影響的可能。如果在其中一段代碼中定義了全局的變量或函數(shù),則這些全局的變量或函數(shù)在另一段代碼中將會(huì)是透明的,意味著在另一段代碼中可以操作或者覆蓋這些變量或函數(shù)。但很多時(shí)候,這樣的情形并不是設(shè)計(jì)需要,而是誤操作。例如,在項(xiàng)目中的一位開(kāi)發(fā)者定義了如下的全局變量和函數(shù): var length = 0; function init(){…} function action() {…} 如果另外一個(gè)開(kāi)發(fā)者在不知道已定義這些變量和函數(shù)的情況下,也定義了相同名稱的變量或函數(shù),則后定義的函數(shù)或者方法會(huì)覆蓋之前的定義。在代碼中出現(xiàn)這樣的情形是非常嚴(yán)重的,導(dǎo)致了變量值被重置或者函數(shù)邏輯改變,從而發(fā)生不可預(yù)知的錯(cuò)誤。 有很多的手段可以解決因?yàn)槎x了全局變量而導(dǎo)致代碼污染的情況。最簡(jiǎn)單的方法是把變量和方法封裝在一個(gè)變量對(duì)象上,使其變成對(duì)象的屬性。例如: var myCurrentAction = { length: 0, init: function(){…}, action: function(){…} } 這樣基本上可以避免全局變量或方法被覆蓋的情況。但這種方案也有弊端,所有變量和函數(shù)的訪問(wèn)都需要通過(guò)主對(duì)象來(lái)實(shí)現(xiàn)了,比如訪問(wèn)如上的length變量,就需要通過(guò)myCurrentAction.length來(lái)訪問(wèn)。這就增加了代碼的重復(fù)度和代碼編寫(xiě)的繁瑣性。另一種改進(jìn)的方案是把全局的變量包含在一個(gè)局部作用域中,然后在這個(gè)作用域中完成這些變量的定義以及變量使用的邏輯。比如,可以通過(guò)定義一個(gè)匿名函數(shù)實(shí)現(xiàn): (function () { var length = 0; function init(){…} function action() {…} })(); 所有的邏輯都包含在了這個(gè)立即執(zhí)行的匿名函數(shù)中,形成了一個(gè)獨(dú)立的模塊,最大限度地防止了代碼之間的污染。當(dāng)然,在實(shí)際的業(yè)務(wù)中,模塊之間會(huì)有交互,這時(shí)則可以使用return語(yǔ)句,返回需要公開(kāi)的接口,比如要公開(kāi)上述代碼中的init函數(shù),則如上代碼應(yīng)修改為如下形式: var myCurrentAction = (function () { var length = 0; function init(){…} function action() {…} return { init: init } })(); 經(jīng)過(guò)如此調(diào)整,外部代碼訪問(wèn)init方法時(shí),就可以調(diào)用myCurrentAction.init了。此方案既巧妙地做到了代碼邏輯的封裝,又公開(kāi)了外部需要訪問(wèn)的接口,是代碼模塊化的最佳實(shí)踐方式之一。 另外一個(gè)避免定義全局變量的方式是:確保在定義變量時(shí)使用var關(guān)鍵字。如果定義變量時(shí)沒(méi)有使用var,瀏覽器解析時(shí)并不會(huì)報(bào)錯(cuò),而是自動(dòng)把這一變量解析為全局變量,比如如下的代碼就定義了一個(gè)全局的變量length: (function () { length = 0; function init(){…} function action() {…} })(); 這種可以不通過(guò)var關(guān)鍵字而定義變量的方式也是JavaScript代碼靈活的一種體現(xiàn),但同時(shí)也是代碼中潛在問(wèn)題的根源之一。很多時(shí)候,開(kāi)發(fā)者并非想通過(guò)這種方式定義一個(gè)全局變量,只是錯(cuò)誤地遺漏了var關(guān)鍵字。規(guī)范的制定者也意識(shí)到了這種靈活性帶來(lái)的問(wèn)題,所以在JavaScript代碼的嚴(yán)格模式中,變量定義必須添加var關(guān)鍵字,否則會(huì)報(bào)編譯錯(cuò)誤。 2. 使用簡(jiǎn)化的編碼方式 在JavaScript中,提供了很多種簡(jiǎn)化的編碼方式,這些方式保持了代碼的簡(jiǎn)潔性,但同時(shí)也提高了可讀性。如下示例將使用復(fù)雜的方式創(chuàng)建對(duì)象和數(shù)組,這種方式是在后端語(yǔ)言中慣用的方式: // 對(duì)象創(chuàng)建 var person = new Object(); person.age = 25; person.name = 'dang'; // 數(shù)組創(chuàng)建 var list = new Array(); list[0] = 12; list[1] = 20; list[2] = 24; 在JavaScript中,可以使用JSON方式創(chuàng)建對(duì)象和數(shù)組。如果開(kāi)發(fā)者熟悉JavaScript,則使用這種方式更簡(jiǎn)潔易讀,代碼如下: // 對(duì)象創(chuàng)建 person = {age: 25, name: 'dang'}; // 數(shù)組創(chuàng)建 list = [12, 20, 24]; 3. 使用比較運(yùn)算符===而不是== JavaScript有兩組相等的運(yùn)算符:===(嚴(yán)格相等)和!==(嚴(yán)格不等)及==(相等)和!=(不等)。===和!==會(huì)比較兩個(gè)基礎(chǔ)類型值是否相等,或者兩個(gè)復(fù)雜對(duì)象是否指向同一個(gè)地址,而==和!=則會(huì)先進(jìn)行比較值的類型轉(zhuǎn)換,在把兩個(gè)比較值的類型轉(zhuǎn)換為相同類型后才會(huì)進(jìn)行比較運(yùn)算,所以只有在兩個(gè)比較值的類型一致時(shí),它才與第一組相等運(yùn)算符等同。==和!=在比較時(shí)的類型轉(zhuǎn)換規(guī)則也很復(fù)雜,具體如下: undefined和null與自己比較時(shí)結(jié)果為true;它們相互比較時(shí)結(jié)果也為true;但與其它類型比較時(shí),結(jié)果為false;原始類型(數(shù)值、布爾和字符類型)進(jìn)行比較時(shí),會(huì)先轉(zhuǎn)換為數(shù)值類型再比較;對(duì)象和原始類型比較時(shí),會(huì)先將對(duì)象轉(zhuǎn)換為原始類型,然后再比較。來(lái)看看相應(yīng)的示例: null == undefined; // true 0 == null; // false false == '0' // true false == 'false' // false '\n 123 \t' == 123 // true var p = {toString: function(){ return '1'}} p == 1; // true 要記住如上的這些規(guī)則很難,而使用===和!==這兩個(gè)嚴(yán)格相等運(yùn)算符進(jìn)行比較時(shí)并不存在類型轉(zhuǎn)換的過(guò)程,因此會(huì)返回正確的結(jié)果。為了避免出現(xiàn)隱含的錯(cuò)誤,推薦使用===和!==運(yùn)算符,不要使用==和!=運(yùn)算符。 4. 避免使用with語(yǔ)句 在JavaScript中,with語(yǔ)句可用來(lái)快捷地訪問(wèn)對(duì)象的屬性。with語(yǔ)句的格式如下: with (object) { statement } with語(yǔ)句的使用原理是:JavaScript解析和運(yùn)行時(shí),會(huì)給with語(yǔ)句單獨(dú)建立了一個(gè)作用域,而和with語(yǔ)句結(jié)合的對(duì)象上的屬性則成為了此作用域的局部變量,因此可以直接訪問(wèn)。比如: with (Math) { a = PI * r * r; x = r * cos(PI); y = r * sin(PI / 2); } 上面代碼和如下的代碼會(huì)完成同樣的事情: a = Math.PI * r * r; x = r * Math.cos(PI); y = r * Math.sin(PI / 2); 從代碼量上看,使用with語(yǔ)句的確簡(jiǎn)化了代碼,但不幸的是,使用with語(yǔ)句可能也會(huì)帶來(lái)不可思議的bug以及兼容問(wèn)題: 首先,使用with語(yǔ)句,使得代碼難以閱讀,對(duì)于with語(yǔ)句內(nèi)部的變量引用,只有在運(yùn)行時(shí)才能知道變量屬于哪個(gè)對(duì)象。比如: function f(x, o) { with (o) { print(x); } } 來(lái)看一下with語(yǔ)句中的x變量,但從代碼分析,x可能是參數(shù)上傳入的x,也可能是o對(duì)象上的屬性o.x,這取決于實(shí)際運(yùn)行時(shí)的上下文。 當(dāng)從代碼無(wú)法確認(rèn)實(shí)際邏輯時(shí),這段代碼就可能會(huì)有潛在的bug。如上的代碼中,可能開(kāi)發(fā)者在代碼中使用x的期望是從參數(shù)傳入x,但如果實(shí)際運(yùn)行時(shí)o對(duì)象上有x屬性,則with語(yǔ)句內(nèi)部的x會(huì)成為o對(duì)象上的x屬性,這就和開(kāi)發(fā)者預(yù)期不同了。 其次,with語(yǔ)句存在兼容問(wèn)題,如下的示例來(lái)自mozilla開(kāi)發(fā)網(wǎng)站: function f(foo, values) { with (foo) { console.log(values) } } 如果在ECMAScript 5環(huán)境中調(diào)用f([1,2,3], obj),則with語(yǔ)句中的values引用的是obj對(duì)象。如果在ECMAScript 6環(huán)境中調(diào)用 f([1,2,3], obj),由于Array.prototype引入了values屬性,因此with語(yǔ)句中的values引用的是[1,2,3].values。 此外, with語(yǔ)句的設(shè)計(jì)方面也有缺陷,在with語(yǔ)句內(nèi)部修改和with語(yǔ)句結(jié)合的對(duì)象后,并不能同步到with內(nèi)部,即不能保證對(duì)象數(shù)據(jù)的一致性。舉個(gè)例子: var group = { value: { node: 1 } }; with(group.value) { group.value = { node: 2 }; // 顯示錯(cuò)誤: 1 console.log(node); } // 顯示正確: 2 console.log(group.value.node); 如上的例子中,在with內(nèi)部修改了group.value對(duì)象,設(shè)置了group.value.node值為2,但在with語(yǔ)句內(nèi)部的node值并沒(méi)用同步修改為2。 基于以上的分析,在使用with語(yǔ)句的過(guò)程中,開(kāi)發(fā)者通過(guò)閱讀代碼不能知道它將會(huì)做什么,即無(wú)法確定代碼是否會(huì)正確地做期望的事情,并且with語(yǔ)句也存在設(shè)計(jì)上的缺陷,所以應(yīng)該在代碼中避免使用with語(yǔ)句。 5. 避免使用eval 在JavaScript中,eval函數(shù)的用法很簡(jiǎn)單,它會(huì)接受一個(gè)字符串參數(shù),把字符串內(nèi)容作為代碼執(zhí)行,并返回執(zhí)行結(jié)果。典型的用法如下: eval("x=1;y=2; x*y") 但這個(gè)函數(shù)存在被濫用的情況。很多新手因?yàn)椴涣私釰avaScript語(yǔ)法,所以會(huì)在某些不恰當(dāng)?shù)膱?chǎng)合使用eval函數(shù)。比如想得到對(duì)象上的屬性值,但由于屬性名是通過(guò)變量傳入的,所以無(wú)法用點(diǎn)操作符,這個(gè)時(shí)候就可能會(huì)想要使用eval,代碼類似如下形式: eval('obj.' + key); 其實(shí)可以使用下標(biāo)法取得屬性值: obj[key] 從eval的功能上看,使用eval函數(shù)會(huì)讓代碼難以閱讀,影響代碼的可維護(hù)性。除此之外,eval的使用也存在安全性問(wèn)題,因?yàn)樗鼤?huì)執(zhí)行任意傳入的代碼,而傳入的代碼有可能是未知的或者來(lái)自不受控制的源,所以盡量避免使用eval。其實(shí)在大多數(shù)的情況下,都是可以使用其它方案來(lái)代替eval的功能。上例便是其中一個(gè)典型的例子,使用下標(biāo)法代替使用eval函數(shù)取得了對(duì)象的屬性。 和eval函數(shù)類似的還有setTimeout和setInterval函數(shù),這兩個(gè)函數(shù)也可以接受字符串參數(shù),當(dāng)傳入的參數(shù)為字符串時(shí),它們會(huì)做類似eval函數(shù)的處理,把字符串當(dāng)作代碼執(zhí)行。所以使用這兩個(gè)函數(shù)時(shí),應(yīng)該避免使用字符串類型參數(shù)。此外,F(xiàn)unction構(gòu)造器也和eval函數(shù)的功能類似,所以也應(yīng)該避免使用。 6. 不要編寫(xiě)檢測(cè)瀏覽器的代碼 經(jīng)常在一些老舊的JavaScript代碼中存在瀏覽器判斷的邏輯,即根據(jù)瀏覽器的不同做不同的處理。這種判斷瀏覽器的做法在五六年以前還算是有一定合理性的,因?yàn)楫?dāng)時(shí)瀏覽器的發(fā)展很緩慢,瀏覽器的功能變化不大。但隨著瀏覽器更新的速度越來(lái)越快,并且瀏覽器之間的差異也越來(lái)越小,原來(lái)這些判斷瀏覽器版本的代碼邏輯就不適時(shí)宜了,甚至有可能導(dǎo)致邏輯上的錯(cuò)誤。因?yàn)闉g覽器之前不支持的功能有可能在新版本中得到了支持,瀏覽器的bug也可能在新版本中得到了修正。這樣一來(lái),之前那些通過(guò)判斷瀏覽器而修正的bug就可能完全沒(méi)有作用了,開(kāi)發(fā)者不得不重新修改代碼來(lái)適應(yīng)新的瀏覽器。所以,最佳的做法是不要編寫(xiě)檢測(cè)瀏覽器的代碼,取而代之的是檢測(cè)瀏覽器是否支持某個(gè)特定功能。開(kāi)發(fā)者可以借助目前流行的Modernizr框架來(lái)檢測(cè)瀏覽器的特性支持。 當(dāng)然,也存在某些特定情況需要判斷瀏覽器的版本,尤其是判斷IE瀏覽器。這個(gè)時(shí)候,最好是把針對(duì)特定瀏覽器的代碼邏輯放置在單獨(dú)的文件中,方便后期的維護(hù)和移除。比如,已經(jīng)知道IE8及以下版本瀏覽器不支持HTML5的新標(biāo)簽,所以如果要在頁(yè)面上使用HTML5新標(biāo)簽,則需要針對(duì)這些瀏覽器加入兼容代碼。這時(shí),可把兼容代碼放在單獨(dú)的文件中,頁(yè)面中添加如下代碼: 后期如果頁(yè)面不再支持IE8及以下版本瀏覽器,則只需移除此代碼引用即可。 jQuery 從 1.9 版開(kāi)始,移除了 $.browser 和$.browser.version ,取而代之的是$.support。jQuery的做法正是為了讓開(kāi)發(fā)者不再借助$.browser和$.browser.version來(lái)判斷瀏覽器版本,而是使用$.support來(lái)判斷瀏覽器的特性支持。 |
|
|