天天看點

編寫高品質JavaScript代碼的基本要點

編寫高品質JavaScript代碼的基本要點

英文 | http://net.tutsplus.com/tutorials/javascript-ajax/the-essentials-of-writing-high-quality-javascript/

譯文 | http://www.zhangxinxu.com/wordpress/?p=1173

才華橫溢的Stoyan Stefanov,在他寫的由O’Reilly初版的新書《JavaScript Patterns》(JavaScript模式)中,我想要是為我們的讀者貢獻其摘要,那會是件很美妙的事情。具體一點就是編寫高品質JavaScript的一些要素,例如避免全局變量,使用單變量聲明,在循環中預緩存length(長度),遵循代碼閱讀,以及更多。

此摘要也包括一些與代碼不太相關的習慣,但對整體代碼的建立息息相關,包括撰寫API文檔、執行同行評審以及運作JSLint。這些習慣和最佳做法可以幫助你寫出更好的,更易于了解和維護的代碼,這些代碼在幾個月或是幾年之後再回過頭看看也是會覺得很自豪的。

書寫可維護的代碼(Writing Maintainable Code )

軟體bug的修複是昂貴的,并且随着時間的推移,這些bug的成本也會增加,尤其當這些bug潛伏并慢慢出現在已經釋出的軟體中時。當你發現bug 的時候就立即修複它是最好的,此時你代碼要解決的問題在你腦中還是很清晰的。否則,你轉移到其他任務,忘了那個特定的代碼,一段時間後再去檢視這些代碼就 需要:

  • 花時間學習和了解這個問題
  • 化時間是了解應該解決的問題代碼

還有問題,特别對于大的項目或是公司,修複bug的這位夥計不是寫代碼的那個人(且發現bug和修複bug的不是同一個人)。是以,必須降低了解代 碼花費的時間,無論是一段時間前你自己寫的代碼還是團隊中的其他成員寫的代碼。這關系到底線(營業收入)和開發人員的幸福,因為我們更應該去開發新的激動 人心的事物而不是花幾小時幾天的時間去維護遺留代碼。

另一個相關軟體開發生命的事實是,讀代碼花費的時間要比寫來得多。有時候,當你專注并深入思考某個問題的時候,你可以坐下來,一個下午寫大量的代碼。

你的代碼很能很快就工作了,但是,随着應用的成熟,還會有很多其他的事情發生,這就要求你的進行進行審查,修改,和調整。例如:

  • bug是暴露的
  • 新功能被添加到應用程式
  • 程式在新的環境下工作(例如,市場上出現新想浏覽器)
  • 代碼改變用途
  • 代碼得完全從頭重新,或移植到另一個架構上或者甚至使用另一種語言

由于這些變化,很少人力數小時寫的代碼最終演變成花數周來閱讀這些代碼。這就是為什麼建立可維護的代碼對應用程式的成功至關重要。

可維護的代碼意味着:

  • 可讀的
  • 一緻的
  • 可預測的
  • 看上去就像是同一個人寫的
  • 已記錄

最小全局變量(Minimizing Globals)

JavaScript通過函數管理作用域。在函數内部聲明的變量隻在這個函數内部,函數外面不可用。另一方面,全局變量就是在任何函數外面聲明的或是未聲明直接簡單使用的。

每個JavaScript環境有一個全局對象,當你在任意的函數外面使用this的時候可以通路到。你建立的每一個全部變量都成了這個全局對象的屬 性。在浏覽器中,友善起見,該全局對象有個附加屬性叫做window,此window(通常)指向該全局對象本身。下面的代碼片段顯示了如何在浏覽器環境 中建立和通路的全局變量:

myglobal = "hello"; // 不推薦寫法
console.log(myglobal); // "hello"
console.log(window.myglobal); // "hello"
console.log(window["myglobal"]); // "hello"
console.log(this.myglobal); // "hello"      

全局變量的問題

全局變量的問題在于,你的JavaScript應用程式和web頁面上的所有代碼都共享了這些全局變量,他們住在同一個全局命名空間,是以當程式的兩個不同部分定義同名但不同作用的全局變量的時候,命名沖突在所難免。

web頁面包含不是該頁面開發者所寫的代碼也是比較常見的,例如:

  • 第三方的JavaScript庫
  • 廣告方的腳本代碼
  • 第三方使用者跟蹤和分析腳本代碼
  • 不同類型的小元件,标志和按鈕

比方說,該第三方腳本定義了一個全局變量,叫做result;接着,在你的函數中也定義一個名為result的全局變量。其結果就是後面的變量覆寫前面的,第三方腳本就一下子嗝屁啦!

是以,要想和其他腳本成為好鄰居的話,盡可能少的使用全局變量是很重要的。在書中後面提到的一些減少全局變量的政策,例如命名空間模式或是函數立即自動執行,但是要想讓全局變量少最重要的還是始終使用var來聲明變量。

由于JavaScript的兩個特征,不自覺地建立出全局變量是出乎意料的容易。首先,你可以甚至不需要聲明就可以使用變量;第二,JavaScript有隐含的全局概念,意味着你不聲明的任何變量都會成為一個全局對象屬性。參考下面的代碼:

function sum(x, y) {
   // 不推薦寫法: 隐式全局變量    
   result = x + y;
   return result;
}      

此段代碼中的result沒有聲明。代碼照樣運作正常,但在調用函數後你最後的結果就多一個全局命名空間,這可以是一個問題的根源。

經驗法則是始終使用var聲明變量,正如改進版的sum()函數所示範的:

function sum(x, y) {
   var result = x + y;
   return result;
}      

另一個建立隐式全局變量的反例就是使用任務鍊進行部分var聲明。下面的片段中,a是本地變量但是b确實全局變量,這可能不是你希望發生的:

// 反例,勿使用 
function foo() {
   var a = b = 0;
   // ...}      

此現象發生的原因在于這個從右到左的指派,首先,是指派表達式b = 0,此情況下b是未聲明的。這個表達式的傳回值是0,然後這個0就配置設定給了通過var定義的這個局部變量a。換句話說,就好比你輸入了:

var a = (b = 0);      

如果你已經準備好聲明變量,使用鍊配置設定是比較好的做法,不會産生任何意料之外的全局變量,如:

function foo() {
   var a, b;
   // ... a = b = 0; // 兩個均局部變量}      
然而,另外一個避免全局變量的原因是可移植性。如果你想你的代碼在不同的環境下(主機下)運作,使用全局變量如履薄冰,因為你會無意中覆寫你最初環境下不存在的主機對象(是以你原以為名稱可以放心大膽地使用,實際上對于有些情況并不适用)。

忘記var的副作用(Side Effects When Forgetting var)

隐式全局變量和明确定義的全局變量間有些小的差異,就是通過delete操作符讓變量未定義的能力。

  • 通過var建立的全局變量(任何函數之外的程式中建立)是不能被删除的。
  • 無var建立的隐式全局變量(無視是否在函數中建立)是能被删除的。

這表明,在技術上,隐式全局變量并不是真正的全局變量,但它們是全局對象的屬性。屬性是可以通過delete操作符删除的,而變量是不能的:

// 定義三個全局變量
var global_var = 1;
global_novar = 2; // 反面教材
(function () {
   global_fromfunc = 3; // 反面教材
}());


// 試圖删除
delete global_var; // false
delete global_novar; // true
delete global_fromfunc; // true


// 測試該删除
typeof global_var; // "number"
typeof global_novar; // "undefined"
typeof global_fromfunc; // "undefined"      

在ES5嚴格模式下,未聲明的變量(如在前面的代碼片段中的兩個反面教材)工作時會抛出一個錯誤。

通路全局對象(Access to the Global Object)

在浏覽器中,全局對象可以通過window屬性在代碼的任何位置通路(除非你做了些比較出格的事情,像是聲明了一個名為window的局部變量)。但是在其他環境下,這個友善的屬性可能被叫做其他什麼東西(甚至在程式中不可用)。如果你需要在沒有寫死的window辨別符下通路全局對象,你可以在任何層級的函數作用域中做如下操作:

var global = (function () {
   return this;
}());      

這種方法可以随時獲得全局對象,因為其在函數中被當做函數調用了(不是通過new構造),this總 是指向全局對象。實際上這個病不适用于ECMAScript 5嚴格模式,是以,在嚴格模式下時,你必須采取不同的形式。例如,你正在開發一個JavaScript庫,你可以将你的代碼包裹在一個即時函數中,然後從 全局作用域中,傳遞一個引用指向this作為你即時函數的參數。

單var形式(Single var Pattern)

在函數頂部使用單var語句是比較有用的一種形式,其好處在于:

  • 提供了一個單一的地方去尋找功能所需要的所有局部變量
  • 防止變量在定義之前使用的邏輯錯誤
  • 幫助你記住聲明的全局變量,是以較少了全局變量//zxx:此處我自己是有點暈乎的…
  • 少代碼(類型啊傳值啊單線完成)

單var形式長得就像下面這個樣子:

function func() {
   var a = 1,
       b = 2,
       sum = a + b,
       myobject = {},
       i,
       j;
   // function body...
   }      

您可以使用一個var語句聲明多個變量,并以逗号分隔。像這種初始化變量同時初始化值的做法是很好的。這樣子可以防止邏輯錯誤(所有未初始化但聲明的變量的初始值是undefined)和增加代碼的可讀性。在你看到代碼後,你可以根據初始化的值知道這些變量大緻的用途,例如是要當作對象呢還是當作整數來使。

你也可以在聲明的時候做一些實際的工作,例如前面代碼中的sum = a + b這個情況,另外一個例子就是當你使用DOM(文檔對象模型)引用時,你可以使用單一的var把DOM引用一起指定為局部變量,就如下面代碼所示的:

function updateElement() {
   var el = document.getElementById("result"),
       style = el.style;
   // 使用el和style幹點其他什麼事...
   }      

預解析:var散布的問題(Hoisting: A Problem with Scattered vars)

JavaScript中,你可以在函數的任何位置聲明多個var語句,并且它們就好像是在函數頂部聲明一樣發揮作用,這種行為稱為 hoisting(懸置/置頂解析/預解析)。當你使用了一個變量,然後不久在函數中又重新聲明的話,就可能産生邏輯錯誤。對于JavaScript,隻 要你的變量是在同一個作用域中(同一函數),它都被當做是聲明的,即使是它在var聲明前使用的時候。看下面這個例子:

// 反例
myname = "global"; // 全局變量
function func() {
    alert(myname); // "undefined"    
    var myname = "local";
    alert(myname); // "local"
    }
func();      

在這個例子中,你可能會以為第一個alert彈出的是”global”,第二個彈出”loacl”。這種期許是可以了解的,因為在第一個alert 的時候,myname未聲明,此時函數肯定很自然而然地看全局變量myname,但是,實際上并不是這麼工作的。第一個alert會彈 出”undefined”是因為myname被當做了函數的局部變量(盡管是之後聲明的),所有的變量聲明當被懸置到函數的頂部了。是以,為了避免這種混 亂,最好是預先聲明你想使用的全部變量。

上面的代碼片段執行的行為可能就像下面這樣:

myname = "global"; // global variable
function func() {
   var myname; // 等同于 -> var myname = undefined;   
   alert(myname); // "undefined"   
   myname = "local";
   alert(myname); // "local"}
   func();      
為了完整,我們再提一提執行層面的稍微複雜點的東西。代碼處理分兩個階段,第一階段是變量,函數聲明,以及正常格式的參數建立,這是一個解析和進入上下文 的階段。第二個階段是代碼執行,函數表達式和不合格的辨別符(為聲明的變量)被建立。但是,出于實用的目的,我們就采用了”hoisting”這個概念, 這種ECMAScript标準中并未定義,通常用來描述行為。

for循環(for Loops)

在for循環中,你可以循環取得數組或是數組類似對象的值,譬如arguments和HTMLCollection對象。通常的循環形式如下:

// 次佳的循環
for (var i = 0; i < myarray.length; i++) {
   // 使用myarray[i]做點什麼
   }      

這種形式的循環的不足在于每次循環的時候數組的長度都要去擷取下。這回降低你的代碼,尤其當myarray不是數組,而是一個HTMLCollection對象的時候。

HTMLCollections指的是DOM方法傳回的對象,例如:

document.getElementsByName()
document.getElementsByClassName()
document.getElementsByTagName()      

還有其他一些HTMLCollections,這些是在DOM标準之前引進并且現在還在使用的。有:

document.images: 頁面上所有的圖檔元素
document.links : 所有a标簽元素
document.forms : 所有表單
document.forms[0].elements : 頁面上第一個表單中的所有域      

集合的麻煩在于它們實時查詢基本文檔(HTML頁面)。這意味着每次你通路任何集合的長度,你要實時查詢DOM,而DOM操作一般都是比較昂貴的。

這就是為什麼當你循環擷取值時,緩存數組(或集合)的長度是比較好的形式,正如下面代碼顯示的:

for (var i = 0, max = myarray.length; i < max; i++) {
   // 使用myarray[i]做點什麼
   }      

這樣,在這個循環過程中,你隻檢索了一次長度值。

在所有浏覽器下,循環擷取内容時緩存HTMLCollections的長度是更快的,2倍(Safari3)到190倍(IE7)之間。//zxx:此資料貌似很老,僅供參考

注意到,當你明确想要修改循環中的集合的時候(例如,添加更多的DOM元素),你可能更喜歡長度更新而不是常量。

伴随着單var形式,你可以把變量從循環中提出來,就像下面這樣:

function looper() {
   var i = 0,
        max,
        myarray = [];
   // ...   
   for (i = 0, max = myarray.length; i < max; i++) {
      // 使用myarray[i]做點什麼   
      }
}      

這種形式具有一緻性的好處,因為你堅持了單一var形式。不足在于當重構代碼的時候,複制和粘貼整個循環有點困難。例如,你從一個函數複制了一個循環到另一個函數,你不得不去确定你能夠把i和max引入新的函數(如果在這裡沒有用的話,很有可能你要從原函數中把它們删掉)。

最後一個需要對循環進行調整的是使用下面表達式之一來替換i++。

i = i + 1
i += 1      

JSLint提示您這樣做,原因是++和–-促進了“過分棘手(excessive trickiness)”。//zxx:這裡比較難翻譯,我想本意應該是讓代碼變得更加的棘手

如果你直接無視它,JSLint的plusplus選項會是false(預設是default)。

還有兩種變化的形式,其又有了些微改進,因為:

  • 少了一個變量(無max)
  • 向下數到0,通常更快,因為和0做比較要比和數組長度或是其他不是0的東西作比較更有效率
//第一種變化的形式:
var i, myarray = [];
for (i = myarray.length; i–-;) {
   // 使用myarray[i]做點什麼
   }


//第二種使用while循環:
var myarray = [],
    i = myarray.length;
while (i–-) {
   // 使用myarray[i]做點什麼
   }      

這些小的改進隻展現在性能上,此外JSLint會對使用i–-加以抱怨。

for-in循環(for-in Loops)

for-in循環應該用在非數組對象的周遊上,使用for-in進行循環也被稱為“枚舉”。

從技術上将,你可以使用for-in循環數組(因為JavaScript中數組也是對象),但這是不推薦的。因為如果數組對象已被自定義的功能增強,就可能發生邏輯錯誤。另外,在for-in中,屬性清單的順序(序列)是不能保證的。是以最好數組使用正常的for循環,對象使用for-in循環。

有個很重要的hasOwnProperty()方法,當周遊對象屬性的時候可以過濾掉從原型鍊上下來的屬性。

思考下面一段代碼:

// 對象var man = {
   hands: 2,
   legs: 2,
   heads: 1
};


// 在代碼的某個地方
// 一個方法添加給了所有對象
if (typeof Object.prototype.clone === "undefined") {
   Object.prototype.clone = function () {};
}      

在這個例子中,我們有一個使用對象字面量定義的名叫man的對象。在man定義完成後的某個地方,在對象原型上增加了一個很有用的名叫 clone()的方法。此原型鍊是實時的,這就意味着所有的對象自動可以通路新的方法。

為了避免枚舉man的時候出現clone()方法,你需要應用hasOwnProperty()方法過濾原型屬性。如果不做過濾,會導緻clone()函數顯示出來,在大多數情況下這是不希望出現的。

// 1.
// for-in 循環
for (var i in man) {
   if (man.hasOwnProperty(i)) { // 過濾     
   console.log(i, ":", man[i]);
   }
}
/* 控制台顯示結果
hands : 2
legs : 2
heads : 1
*/
// 2.
// 反面例子:
// for-in loop without checking hasOwnProperty()
for (var i in man) {
   console.log(i, ":", man[i]);
}
/*
控制台顯示結果
hands : 2
legs : 2
heads : 1
clone: function()
*/      

另外一種使用hasOwnProperty()的形式是取消Object.prototype上的方法。像是:

for (var i in man) {
   if (Object.prototype.hasOwnProperty.call(man, i)) { 
   // 過濾      
   console.log(i, ":", man[i]);
   }
}      

其好處在于在man對象重新定義hasOwnProperty情況下避免命名沖突。也避免了長屬性查找對象的所有方法,你可以使用局部變量“緩存”它。

var i, hasOwn = Object.prototype.hasOwnProperty;
for (i in man) {
    if (hasOwn.call(man, i)) { 
    // 過濾        
    console.log(i, ":", man[i]);
    }
}      
嚴格來說,不使用hasOwnProperty()并不是一個錯誤。根據任務以及你對代碼的自信程度,你可以跳過它以提高些許的循環速度。但是當你對目前對象内容(和其原型鍊)不确定的時候,添加hasOwnProperty()更加保險些。

格式化的變化(通不過JSLint)會直接忽略掉花括号,把if語句放到同一行上。其優點在于循環語句讀起來就像一個完整的想法(每個元素都有一個自己的屬性”X”,使用”X”幹點什麼):

// 警告:通不過JSLint檢測
var i, hasOwn = Object.prototype.hasOwnProperty;
for (i in man) if (hasOwn.call(man, i)) { 
// 過濾    
console.log(i, ":", man[i]);
}      

(不)擴充内置原型((Not) Augmenting Built-in Prototypes)

擴增構造函數的prototype屬性是個很強大的增加功能的方法,但有時候它太強大了。

增加内置的構造函數原型(如Object(), Array(), 或Function())挺誘人的,但是這嚴重降低了可維護性,因為它讓你的代碼變得難以預測。使用你代碼的其他開發人員很可能更期望使用内置的 JavaScript方法來持續不斷地工作,而不是你另加的方法。

另外,屬性添加到原型中,可能會導緻不使用hasOwnProperty屬性時在循環中顯示出來,這會造成混亂。

是以,不增加内置原型是最好的。你可以指定一個規則,僅當下面的條件均滿足時例外:

  • 可以預期将來的ECMAScript版本或是JavaScript實作将一直将此功能當作内置方法來實作。例如,你可以添加ECMAScript 5中描述的方法,一直到各個浏覽器都迎頭趕上。這種情況下,你隻是提前定義了有用的方法。
  • 如果您檢查您的自定義屬性或方法已不存在——也許已經在代碼的其他地方實作或已經是你支援的浏覽器JavaScript引擎部分。
  • 你清楚地文檔記錄并和團隊交流了變化。

如果這三個條件得到滿足,你可以給原型進行自定義的添加,形式如下:

if (typeof Object.protoype.myMethod !== "function") {
   Object.protoype.myMethod = function () {
      // 實作...   
      };
}      

switch模式(switch Pattern)

你可以通過類似下面形式的switch語句增強可讀性和健壯性:

var inspect_me = 0,
    result = '';
switch (inspect_me) {
case 0:
   result = "zero";
   break;
case 1:
   result = "one";
   break;
default:
   result = "unknown";
}      

這個簡單的例子中所遵循的風格約定如下:

  • 每個case和switch對齊(花括号縮進規則除外)
  • 每個case中代碼縮進
  • 每個case以break清除結束
  • 避免貫穿(故意忽略break)。如果你非常确信貫穿是最好的方法,務必記錄此情況,因為對于有些閱讀人而言,它們可能看起來是錯誤的。
  • 以default結束switch:確定總有健全的結果,即使無情況比對。

避免隐式類型轉換(Avoiding Implied Typecasting )

JavaScript的變量在比較的時候會隐式類型轉換。這就是為什麼一些諸如:false == 0 或 “” == 0 傳回的結果是true。為避免引起混亂的隐含類型轉換,在你比較值和表達式類型的時候始終使用===和!==操作符。

var zero = 0;
if (zero === false) {
   // 不執行,因為zero為0, 而不是false
   }


// 反面示例
if (zero == false) {
   // 執行了...
   }      

還有另外一種思想觀點認為==就足夠了===是多餘的。例如,當你使用typeof你就知道它會傳回一個字元串,是以沒有使用嚴格相等的理由。然而,JSLint要求嚴格相等,它使代碼看上去更有一緻性,可以降低代碼閱讀時的精力消耗。(“==是故意的還是一個疏漏?”)

避免(Avoiding) eval()

如果你現在的代碼中使用了eval(),記住該咒語“eval()是魔鬼”。此方法接受任意的字元串,并當作JavaScript代碼來處理。當有 問題的代碼是事先知道的(不是運作時确定的),沒有理由使用eval()。如果代碼是在運作時動态生成,有一個更好的方式不使用eval而達到同樣的目 标。例如,用方括号表示法來通路動态屬性會更好更簡單:

// 反面示例
var property = "name";
alert(eval("obj." + property));


// 更好的
var property = "name";
alert(obj[property]);      

使用eval()也帶來了安全隐患,因為被執行的代碼(例如從網絡來)可能已被篡改。這是個很常見的反面教材,當處理Ajax請求得到的JSON 相應的時候。在這些情況下,最好使用JavaScript内置方法來解析JSON相應,以確定安全和有效。若浏覽器不支援JSON.parse(),你可 以使用來自JSON.org的庫。

同樣重要的是要記住,給setInterval(), setTimeout()和Function()構造函數傳遞字元串,大部分情況下,與使用eval()是類似的,是以要避免。在幕後,JavaScript仍需要評估和執行你給程式傳遞的字元串:

// 反面示例
setTimeout("myFunc()", 1000);
setTimeout("myFunc(1, 2, 3)", 1000);


// 更好的
setTimeout(myFunc, 1000);
setTimeout(function () {
   myFunc(1, 2, 3);
}, 1000);      

使用新的Function()構造就類似于eval(),應小心接近。這可能是一個強大的構造,但往往被誤用。如果你絕對必須使用eval(),你 可以考慮使用new Function()代替。

有一個小的潛在好處,因為在新Function()中作代碼評估是在局部函數作用域中運作,是以代碼中任何被評估的通過var 定義的變量都不會自動變成全局變量。另一種方法來阻止自動全局變量是封裝eval()調用到一個即時函數中。

考慮下面這個例子,這裡僅un作為全局變量污染了命名空間。

console.log(typeof un);    // "undefined"
console.log(typeof deux); // "undefined"
console.log(typeof trois); // "undefined"
var jsstring = "var un = 1; console.log(un);";
eval(jsstring); // logs "1"
jsstring = "var deux = 2; console.log(deux);";
new Function(jsstring)(); // logs "2"
jsstring = "var trois = 3; console.log(trois);";
(function () {
   eval(jsstring);
}()); // logs "3"
console.log(typeof un); // number
console.log(typeof deux); // "undefined"
console.log(typeof trois); // "undefined"      

另一間eval()和Function構造不同的是eval()可以幹擾作用域鍊,而Function()更安分守己些。不管你在哪裡執行 Function(),它隻看到全局作用域。是以其能很好的避免本地變量污染。在下面這個例子中,eval()可以通路和修改它外部作用域中的變量,這是 Function做不來的(注意到使用Function和new Function是相同的)。

(function () {
   var local = 1;
   eval("local = 3; console.log(local)"); // logs "3"   
   console.log(local); // logs "3"}());


(function () {
   var local = 1;
   Function("console.log(typeof local);")(); 
   // logs undefined
   }());      

parseInt()下的數值轉換(Number Conversions with parseInt())

使用parseInt()你可以從字元串中擷取數值,該方法接受另一個基數參數,這經常省略,但不應該。當字元串以”0″開頭的時候就有可能會出問 題,例如,部分時間進入表單域,在ECMAScript 3中,開頭為”0″的字元串被當做8進制處理了,但這已在ECMAScript 5中改變了。為了避免沖突和意外的結果,總是指定基數參數。

var month = "06",
    year = "09";
month = parseInt(month, 10);
year = parseInt(year, 10);      

此例中,如果你忽略了基數參數,如parseInt(year),傳回的值将是0,因為“09”被當做8進制(好比執行 parseInt( year, 8 )),而09在8進制中不是個有效數字。

替換方法是将字元串轉換成數字,包括:

+"08" // 結果是 8
Number("08") // 8      

這些通常快于parseInt(),因為parseInt()方法,顧名思意,不是簡單地解析與轉換。但是,如果你想輸入例如“08 hello”,parseInt()将傳回數字,而其它以NaN告終。

編碼規範(Coding Conventions)

建立和遵循編碼規範是很重要的,這讓你的代碼保持一緻性,可預測,更易于閱讀和了解。一個新的開發者加入這個團隊可以通讀規範,了解其它團隊成員書寫的代碼,更快上手幹活。

許多激烈的争論發生會議上或是郵件清單上,問題往往針對某些代碼規範的特定方面(例如代碼縮進,是Tab制表符鍵還是space空格鍵)。如果你是 你組織中建議采用規範的,準備好面對各種反對的或是聽起來不同但很強烈的觀點。要記住,建立和堅定不移地遵循規範要比糾結于規範的細節重要的多。

縮進(Indentation)

代碼沒有縮進基本上就不能讀了。唯一糟糕的事情就是不一緻的縮進,因為它看上去像是遵循了規範,但是可能一路上伴随着混亂和驚奇。重要的是規範地使用縮進。

一些開發人員更喜歡用tab制表符縮進,因為任何人都可以調整他們的編輯器以自己喜歡的空格數來顯示Tab。有些人喜歡空格——通常四個,這都無所謂,隻要團隊每個人都遵循同一個規範就好了。這本書,例如,使用四個空格縮進,這也是JSLint中預設的縮進。

什麼應該縮進呢?規則很簡單——花括号裡面的東西。這就意味着函數體,循環 (do, while, for, for-in),if,switch,以及對象字面量中的對象屬性。下面的代碼就是使用縮進的示例:

function outer(a, b) {
    var c = 1,
        d = 2,
        inner;
    if (a > b) {
        inner = function () {
            return {
                r: c - d
            };
        };
    } else {
        inner = function () {
            return {
                r: c + d
            };
        };
    }
    return inner;
}      

花括号{}(Curly Braces)

花括号(亦稱大括号,下同)應總被使用,即使在它們為可選的時候。技術上将,在in或是for中如果語句僅一條,花括号是不需要的,但是你還是應該總是使用它們,這會讓代碼更有持續性和易于更新。

想象下你有一個隻有一條語句的for循環,你可以忽略花括号,而沒有解析的錯誤。

// 糟糕的執行個體
for (var i = 0; i < 10; i += 1)
   alert(i);      

但是,如果,後來,主體循環部分又增加了行代碼?

// 糟糕的執行個體
for (var i = 0; i < 10; i += 1)
   alert(i);
   alert(i + " is " + (i % 2 ? "odd" : "even"));      

第二個alert已經在循環之外,縮進可能欺騙了你。為了長遠打算,最好總是使用花括号,即時值一行代碼:

// 好的執行個體
for (var i = 0; i < 10; i += 1) {
   alert(i);
}      

if條件類似:

// 壞
if (true)
   alert(1);
else
   alert(2);


// 好
if (true) {
   alert(1);
} else {
   alert(2);
}      

左花括号的位置(Opening Brace Location)

開發人員對于左大括号的位置有着不同的偏好——在同一行或是下一行。

if (true) {
   alert("It's TRUE!");
}
//或
if (true)
{
   alert("It's TRUE!");
}      

這個執行個體中,仁者見仁智者見智,但也有個案,括号位置不同會有不同的行為表現。這是因為分号插入機制(semicolon insertion mechanism)——JavaScript是不挑剔的,當你選擇不使用分号結束一行代碼時JavaScript會自己幫你補上。這種行為可能會導緻麻 煩,如當你傳回對象字面量,而左括号卻在下一行的時候:

// 警告:意外的傳回值
function func() {
   return
  // 下面代碼不執行   
  {
      name : "Batman"
   }
}      

如果你希望函數傳回一個含有name屬性的對象,你會驚訝。由于隐含分号,函數傳回undefined。前面的代碼等價于:

// 警告:意外的傳回值
function func() {
   return undefined;
  // 下面代碼不執行  
   {
      name : "Batman"
   }
}      

總之,總是使用花括号,并始終把在與之前的語句放在同一行:

function func() {
   return {
      name : "Batman"
   };
}      
關于分号注:就像使用花括号,你應該總是使用分号,即使他們可由JavaScript解析器隐式建立。這不僅促進更科學和更嚴格的代碼,而且有助于解決存有疑惑的地方,就如前面的例子顯示。

空格(White Space)

空格的使用同樣有助于改善代碼的可讀性和一緻性。在寫英文句子的時候,在逗号和句号後面會使用間隔。在JavaScript中,你可以按照同樣的邏輯在清單模樣表達式(相當于逗号)和結束語句(相對于完成了“想法”)後面添加間隔。

适合使用空格的地方包括:

  • for循環分号分開後的的部分:如for (var i = 0; i < 10; i += 1) {...}
  • for循環中初始化的多變量(i和max):for (var i = 0, max = 10; i < max; i += 1) {...}
  • 分隔數組項的逗号的後面:var a = [1, 2, 3];
  • 對象屬性逗号的後面以及分隔屬性名和屬性值的冒号的後面:var o = {a: 1, b: 2};
  • 限定函數參數:myFunc(a, b, c)
  • 函數聲明的花括号的前面:function myFunc() {}
  • 匿名函數表達式function的後面:var myFunc = function () {};

使用空格分開所有的操作符和操作對象是另一個不錯的使用,這意味着在+, -, *, =, <, >, <=, >=, ===, !==, &&, ||, +=等前後都需要空格。

// 寬松一緻的間距
// 使代碼更易讀
// 使得更加“透氣”
var d = 0,
    a = b + 1;
if (a && b && c) {
    d = a % c;
    a += d;
}


// 反面例子
// 缺失或間距不一
// 使代碼變得疑惑
var d = 0,
    a = b + 1;
if (a&&b&&c) {
    d=a % c;
    a+= d;
}      

最後需要注意的一個空格——花括号間距。最好使用空格:

  • 函數、if-else語句、循環、對象字面量的左花括号的前面({)
  • else或while之間的右花括号(})

空格使用的一點不足就是增加了檔案的大小,但是壓縮無此問題。

有一個經常被忽略的代碼可讀性方面是垂直空格的使用。你可以使用空行來分隔代碼單元,就像是文學作品中使用段落分隔一樣。

命名規範(Naming Conventions)

另一種方法讓你的代碼更具可預測性和可維護性是采用命名規範。這就意味着你需要用同一種形式給你的變量和函數命名。

下面是建議的一些命名規範,你可以原樣采用,也可以根據自己的喜好作調整。同樣,遵循規範要比規範是什麼更重要。

以大寫字母寫構造函數(Capitalizing Constructors)

JavaScript并沒有類,但有new調用的構造函數:

var adam = new Person();      

因為構造函數仍僅僅是函數,僅看函數名就可以幫助告訴你這應該是一個構造函數還是一個正常的函數。

命名構造函數時首字母大寫具有暗示作用,使用小寫命名的函數和方法不應該使用new調用:

function MyConstructor() {...}
function myFunction() {...}      

分隔單詞(Separating Words)

當你的變量或是函數名有多個單詞的時候,最好單詞的分離遵循統一的規範,有一個常見的做法被稱作“駝峰(Camel)命名法”,就是單詞小寫,每個單詞的首字母大寫。

對于構造函數,可以使用大駝峰式命名法(upper camel case),如MyConstructor()。對于函數和方法名稱,你可以使用小駝峰式命名法(lower camel case),像是myFunction(), calculateArea()和getFirstName()。

要是變量不是函數呢?開發者通常使用小駝峰式命名法,但還有另外一種做法就是所有單詞小寫以下劃線連接配接:例如,first_name, favorite_bands, 和 old_company_name,這種标記法幫你直覺地區分函數和其他辨別——原型和對象。

ECMAScript的屬性和方法均使用Camel标記法,盡管多字的屬性名稱是罕見的(正規表達式對象的lastIndex和ignoreCase屬性)。

其它命名形式(Other Naming Patterns)

有時,開發人員使用命名規範來彌補或替代語言特性。

例如,JavaScript中沒有定義常量的方法(盡管有些内置的像Number, MAX_VALUE),是以開發者都采用全部單詞大寫的規範來命名這個程式生命周期中都不會改變的變量,如:

// 珍貴常數,隻可遠觀
var PI = 3.14,
    MAX_WIDTH = 800;      

還有另外一個完全大寫的慣例:全局變量名字全部大寫。全部大寫命名全局變量可以加強減小全局變量數量的實踐,同時讓它們易于區分。

另外一種使用規範來模拟功能的是私有成員。雖然可以在JavaScript中實作真正的私有,但是開發者發現僅僅使用一個下劃線字首來表示一個私有屬性或方法會更容易些。考慮下面的例子:

var person = {
    getName: function () {
        return this._getFirst() + ' ' + this._getLast();
    },


    _getFirst: function () {
        // ...    },
    _getLast: function () {
        // ...    }
};      

在此例中,getName()就表示公共方法,部分穩定的API。而_getFirst()和_getLast()則表明了私有。它們仍然是正常的公共方法,但是使用下劃線字首來警告person對象的使用者這些方法在下一個版本中時不能保證工作的,是不能直接使用的。注意,JSLint有些不鳥下劃線字首,除非你設定了noman選項為:false。

下面是一些常見的_private規範:

  • 使用尾下劃線表示私有,如name_和getElements_()
  • 使用一個下劃線字首表_protected(保護)屬性,兩個下劃線字首表示__private (私有)屬性
  • Firefox中一些内置的變量屬性不屬于該語言的技術部分,使用兩個前下劃線和兩個後下劃線表示,如:__proto__和__parent__。

注釋(Writing Comments)

你必須注釋你的代碼,即使不會有其他人向你一樣接觸它。通常,當你深入研究一個問題,你會很清楚的知道這個代碼是幹嘛用的,但是,當你一周之後再回來看的時候,想必也要耗掉不少腦細胞去搞明白到底怎麼工作的。

很顯然,注釋不能走極端:每個單獨變量或是單獨一行。但是,你通常應該記錄所有的函數,它們的參數和傳回值,或是任何不尋常的技術和方法。要想到注 釋可以給你代碼未來的閱讀者以諸多提示;閱讀者需要的是(不要讀太多的東西)僅注釋和函數屬性名來了解你的代碼。

例如,當你有五六行程式執行特定的任務, 如果你提供了一行代碼目的以及為什麼在這裡的描述的話,閱讀者就可以直接跳過這段細節。沒有硬性規定注釋代碼比,代碼的某些部分(如正規表達式)可能注釋 要比代碼多。

最重要的習慣,然而也是最難遵守的,就是保持注釋的及時更新,因為過時的注釋比沒有注釋更加的誤導人。

關于作者(About the Author )

Stoyan Stefanov是Yahoo!web開發人員,多個O'Reilly書籍的作者、投稿者和技術評審。他經常在會議和他的部落格www.phpied.com上發表web開發主題的演講。Stoyan還是smush.it圖檔優化工具的創造者,YUI貢獻者,雅虎性能優化工具YSlow 2.0的架構設計師。

繼續閱讀