書寫高質量JavaScript程式碼的要義(TheEssentialsofWritingHighQualityJavaScript)翻譯
原文:The Essentials of Writing High Quality JavaScript
才華橫溢的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();
//zxx:關於JavaScript的置頂解析,我上週專門翻譯了篇文章,您有興趣可以看看:“翻譯 – 解釋JavaScript的置頂解析”。
為了完整,我們再提一提執行層面的稍微複雜點的東西。程式碼處理分兩個階段,第一階段是變數,函式宣告,以及正常格式的引數建立,這是一個解析和進入上下文的階段。第二個階段是程式碼執行,函式表示式和不合格的識別符號(為宣告的變數)被建立。但是,出於實用的目的,我們就採用了”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; }
//zxx:我就琢磨著這正面和反面例子不長得一樣嗎…原文就是如此,我也不好擅自改動。
最後需要注意的一個空格——花括號間距。最好使用空格:
- 函式、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的架構設計師。
相關文章
- 如何書寫高質量的jQuery程式碼jQuery
- 書寫高質量jQuery程式碼的12條經驗jQuery
- 《Effective JavaScript 編寫高質量JavaScript程式碼的68個有效方法》JavaScript
- 藉助SublimeLinter編寫高質量的JavaScript & CSS程式碼JavaScriptCSS
- 讀書筆記:編寫高質量javascript的68個方法筆記JavaScript
- 怎樣編寫高質量的java程式碼Java
- 編寫高質量的程式碼,從命名入手
- 對《JavaScript高階程式設計(第4版)》一書中為何要翻譯promise的回覆JavaScript程式設計Promise
- 如何編寫高質量的C#程式碼(一)C#
- iOS 編寫高質量Objective-C程式碼iOSObjectC程式
- [轉]高質量JAVA程式碼編寫規範Java
- 如何編寫高質量和可維護的程式碼
- 書寫高質量sql的一些建議SQL
- 書寫高質量程式碼之狀態維護
- iOS 編寫高質量Objective-C程式碼(四)iOSObjectC程式
- iOS 編寫高質量Objective-C程式碼(一)iOSObjectC程式
- iOS 編寫高質量Objective-C程式碼(三)iOSObjectC程式
- iOS 編寫高質量Objective-C程式碼(二)iOSObjectC程式
- iOS 編寫高質量Objective-C程式碼(八)iOSObjectC程式
- iOS編寫高質量Objective-C程式碼(四)iOSObjectC程式
- iOS編寫高質量Objective-C程式碼(二)iOSObjectC程式
- iOS 編寫高質量Objective-C程式碼(五)iOSObjectC程式
- iOS 編寫高質量Objective-C程式碼(六)iOSObjectC程式
- iOS 編寫高質量Objective-C程式碼(七)iOSObjectC程式
- iOS編寫高質量Objective-C程式碼(六)iOSObjectC程式
- 我們應該如何編寫高質量的前端程式碼前端
- 藉助 SublimeLinter 編寫高質量的 JS & CSS 程式碼JSCSS
- 監管機器翻譯質量?且看阿里如何搭建翻譯質量評估模型阿里模型
- 編寫高質量程式碼
- 高質量的程式碼 - 函式(1)函式
- JavaScript寫程式碼要規範JavaScript
- 編寫高質量程式碼的思考
- 編寫靈活、穩定、高質量的HTML程式碼的規範HTML
- 編寫靈活、穩定、高質量的CSS程式碼的規範CSS
- iOS 編寫高質量Objective-C程式碼(一)—— 簡介iOSObjectC程式
- Code Complete — 建立高質量的程式碼
- [編寫高質量iOS程式碼的52個有效方法](九)塊(block)iOSBloC
- 如何提高Java程式碼質量-優雅的寫程式碼Java