JavaScript 編寫規范
說明
如果本文檔中有任何錯誤的、不符合行規的,敬請斧正。
引言
不管有多少人共同參與同一項目,一定要確保每一行代碼都像是同一個人編寫的。
</blockquote>若您對本文檔中任何內容有不明白的,或者感覺不合行規的,請您按以下格式向我郵件說明,同時也歡迎任何人參與討論,共同完善本文檔。本文檔中很多也是根據我自己現在的公司需求來定的。
郵件不是QQ,請盡可能清晰的組織你的語言,將問題描述得更清楚,同時,不要使用過多的樣式化,合乎文檔排版標準,謝謝。
</blockquote>規范內容
全局命名空間污染與 IIFE
總是將代碼包裹成一個 IIFE(Immediately-Invoked Function Expression),用以創建獨立隔絕的定義域。這一舉措可防止全局命名空間被污染。
IIFE 還可確保你的代碼不會輕易被其它全局命名空間里的代碼所修改(i.e. 第三方庫,window 引用,被覆蓋的未定義的關鍵字等等)。
不推薦
javascriptvar x = 10, y = 100;console.log(window.x + ' ' + window.y);</pre>
推薦
javascript(function(log, w, undefined){ 'use strict';var x = 10, y = 100;
// 將輸出 'true true' log((w.x === undefined) + ' ' + (w.y === undefined));
}(window.console.log, window));</pre>
IIFE(立即執行的函數表達式)
無論何時,想要創建一個新的封閉的定義域,那就用 IIFE。它不僅避免了干擾,也使得內存在執行完后立即釋放。
所有腳本文件建議都從 IIFE 開始。
立即執行的函數表達式的執行括號應該寫在外包括號內。雖然寫在內還是寫在外都是有效的,但寫在內使得整個表達式看起來更像一個整體,因此推薦這么做。
不推薦
(function(){})();推薦
(function(){}());所以,請用下列寫法來格式化你的 IIFE 代碼:
javascript(function(){ 'use strict';// 你的代碼寫在這里
}());</pre>
如果你想引用全局變量或者是外層 IIFE 的變量,可以通過下列方式傳參:
javascript(function($, w, d){ 'use strict';$(function() { w.alert(d.querySelectorAll('div').length); }); }(jQuery, window, document));</pre>
嚴格模式
ECMAScript 5 嚴格模式可在整個腳本或獨個方法內被激活。它對應不同的 javascript 語境會做更加嚴格的錯誤檢查。嚴格模式也確保了 javascript 代碼更加的健壯,運行的也更加快速。
嚴格模式會阻止使用在未來很可能被引入的預留關鍵字。
你應該在你的腳本中啟用嚴格模式,最好是在獨立的 IIFE 中應用它。避免在你的腳本第一行使用它而導致你的所有腳本都啟動了嚴格模式,這有可能會引發一些第三方類庫的問題。
不推薦
javascript// 代碼從這里開始 'use strict';(function(){
// 你在代碼寫在這里
}());</pre>
推薦
javascript(function(){ 'use strict';// 你在代碼寫在這里
}());</pre>
變量聲明
總是使用var來聲明變量。如不指定var,變量將被隱式地聲明為全局變量,這將對變量難以控制。如果沒有聲明,變量處于什么定義域就變得不清(可以是在document或window中,也可以很容易地進入本地定義域)。所以,請總是使用 var 來聲明變量。
采用嚴格模式帶來的好處是,當你手誤輸入錯誤的變量名時,它可以通過報錯信息來幫助你定位錯誤出處。
不推薦
x = 10; y = 100;推薦
var x = 10, y = 100;理解 JavaScript 的定義域和定義域提升
在 JavaScript 中變量和方法定義會自動提升到執行之前。JavaScript 只有function級的定義域,而無其他很多編程語言中的塊定義域,所以使得你在某一 function 內的某語句和循環體中定義了一個變量,此變量可作用于整個 function 內,而不僅僅是在此語句或循環體中,因為它們的聲明被 JavaScript 自動提升了。
我們通過例子來看清楚這到底是怎么一回事:
原 function
javascript(function(log){ 'use strict';var a = 10;
for(var i = 0; i < a; i++) { var b = i * i; log(b); }
if(a === 10) { var f = function() { log(a); }; f(); }
function x() { log('Mr. X!'); } x();
}(window.console.log));</pre>
被 JS 提升過后
javascript(function(log){ 'use strict'; // 在封閉環境中使用的所有變量將被提升到函數的頂部 var a, i, b, f; // 所有的函數也將被提升到函數頂部 function x() { log('Mr. X!'); }a = 10;
for(i = 0; i < a; i++) { b = i * i; log(b); }
if(a === 10) { // 功能分配只會導致懸掛變量,但在函數體不會被懸掛 // 只有用真正的函數聲明的整體功能才能懸掛 f = function() { log(a); }; f(); }
x();
}(window.console.log));</pre>
根據以上提升過程,你是否可理解以下代碼?
有效代碼
javascript(function(log){ 'use strict';var a = 10;
i = 5;
x();
for(var i; i < a; i++) { log(b); var b = i * i; }
if(a === 10) { f = function() { log(a); }; f();
var f;
}
function x() { log('Mr. X!'); }
}(window.console.log));</pre>
正如你所看到的這段令人充滿困惑與誤解的代碼導致了出人意料的結果。只有良好的聲明習慣,也就是下一章節我們要提到的聲明規則,才能盡可能的避免這類錯誤風險。
提升聲明
為避免上一章節所述的變量和方法定義被自動提升造成誤解,把風險降到最低,我們應該手動地顯示地去聲明變量與方法。也就是說,所有的變量以及方法,應當定義在 function 內的首行。
只用一個var關鍵字聲明,多個變量用逗號隔開。
不推薦
(function(log){
‘use strict';var a = 10;
var b = 10;for(var i = 0; i < 10; i++) {
var c = a * b * i;
}function f() {
}
var d = 100;
var x = function() {
return d * d;
};
log(x());}(window.console.log));
推薦```javascript (function(log){ 'use strict';
var a = 10, b = 10, i, c, d, x;
function f() {
}
for(i = 0; i < 10; i++) { c = a b i; }
d = 100; x = function() { return d * d; }; log(x());
}(window.console.log));</pre>
把賦值盡量寫在變量申明中。
不推薦
javascriptvar a, b, c;a = 10; b = 10; c = 100;</pre>
推薦
javascriptvar a = 10, b = 10, c = 100;總是使用帶類型判斷的比較判斷
總是使用===精確的比較操作符,避免在判斷的過程中,由 JavaScript 的強制類型轉換所造成的困擾。
如果你使用===操作符,那比較的雙方必須是同一類型為前提的條件下才會有效。
如果你想了解更多關于強制類型轉換的信息,你可以讀一讀 Dmitry Soshnikov 的這篇文章。
在只使用==的情況下,JavaScript 所帶來的強制類型轉換使得判斷結果跟蹤變得復雜,下面的例子可以看出這樣的結果有多怪了:
javascript(function(log){ 'use strict';log('0' == 0); // true log('' == false); // true log('1' == true); // true log(null == undefined); // true
var x = { valueOf: function() { return 'X'; } };
log(x == 'X');
}(window.console.log));</pre>
明智地使用真假判斷
當我們在一個if條件語句中使用變量或表達式時,會做真假判斷。if(a == true)是不同于if(a)的。后者的判斷比較特殊,我們稱其為真假判斷。這種判斷會通過特殊的操作將其轉換為true或false,下列表達式統統返回false:false,0,undefined,null,NaN,''(空字符串).
這種真假判斷在我們只求結果而不關心過程的情況下,非常的有幫助。
以下示例展示了真假判斷是如何工作的:
javascript(function(log){ 'use strict';function logTruthyFalsy(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));</pre>
變量賦值時的邏輯操作
邏輯操作符||和&&也可被用來返回布爾值。如果操作對象為非布爾對象,那每個表達式將會被自左向右地做真假判斷。基于此操作,最終總有一個表達式被返回回來。這在變量賦值時,是可以用來簡化你的代碼的。
不推薦
javascriptif(!x) { if(!y) { x = 1; } else { x = y; } }推薦
x = x || y || 1;這一小技巧經常用來給方法設定默認的參數。
javascript(function(log){ 'use strict';function multiply(a, b) { a = a || 1; b = b || 1;
log('結果 ' + a * b);
}
multiply(); // 結果 1 multiply(10); // 結果 10 multiply(3, NaN); // 結果 3 multiply(9, 5); // 結果 45
}(window.console.log));</pre>
分號
總是使用分號,因為隱式的代碼嵌套會引發難以察覺的問題。當然我們更要從根本上來杜絕這些問題[1] 。以下幾個示例展示了缺少分號的危害:
javascript// 1. MyClass.prototype.myMethod = function() { return 42; } // 這里沒有分號(function() { // 包裹在一個函數的一些初始化代碼,用來創建當本地變量。 })();
var x = { 'i': 1, 'j': 2 } // 這里也沒有分號
// 2. 嘗試在 IE 或者 Firefox 瀏覽器上運行
[ffVersion, ieVersion]isIE;
var THINGS_TO_EAT = [apples, oysters, sprayOnCheese] // 這里也沒有分號
// 3. -1 == resultOfOperation() || die();</pre>
那么,發生了什么?
- JavaScript 錯誤 —— 首先返回42的那個function被第二個 function 當中參數傳入調用,接著數字 42 也被“調用”而導致出錯;
- 八成你會得到no such property in undefined的錯誤提示,因為在真實環境中的調用是這個樣子:x[ffVersion, ieVersion][isIE]();
- die總是被調用。因為數組減1的結果是NaN,它不等于任何東西(無論resultOfOperation是否返回NaN)。所以最終的結果是die()執行完所獲得值將賦給THINGS_TO_EAT。
</ol>為什么會這樣?
JavaScript 中語句要以分號結束,否則它將會繼續執行下去,不管換不換行。以上的每一個示例中,函數聲明或對象或數組,都變成了在一句語句體內。要知道閉合圓括號并不代表語句結束,JavaScript 不會終結語句,除非它的下一個 token 是一個中綴符[2] 或者是圓括號操作符。
這真是讓人大吃一驚,所以乖乖地給語句末加上分號吧。
澄清:分號與函數
分號需要用在表達式的結尾,而并非函數聲明的結尾。區分它們最好的例子是:
javascriptvar foo = function() { return true; }; // 這里需要分號function foo() { return true; } // 這里需要分號</pre>
嵌套函數
嵌套函數是非常有用的,比如用在持續創建和隱藏輔助函數的任務中。你可以非常自由隨意地使用它們。
語句塊內的函數聲明
切勿在語句塊內聲明函數,在 ECMAScript 5 的嚴格模式下,這是不合法的。函數聲明應該在定義域的頂層。但在語句塊內可將函數申明轉化為函數表達式賦值給變量。
不推薦
javascriptif (x) { function foo() {} }推薦
javascriptif (x) { var foo = function() {}; }異常
基本上你無法避免出現異常,特別是在做大型開發時(使用應用開發框架等等)。
在沒有自定義異常的情況下,從有返回值的函數中返回錯誤信息一定非常的棘手,更別提多不優雅了。不好的解決方案包括了傳第一個引用類型來接納錯誤信息,或總是返回一個對象列表,其中包含著可能的錯誤對象。以上方式基本上是比較簡陋的異常處理方式。適時可做自定義異常處理。
在復雜的環境中,你可以考慮拋出對象而不僅僅是字符串(默認的拋出值)。
javascriptif(name === undefined) { throw { name: '系統錯誤', message: '任何時候都應該定義一個 name!' } }標準特性
總是優先考慮使用標準特性。為了最大限度地保證擴展性與兼容性,總是首選標準的特性,而不是非標準的特性(例如:首選string.charAt(3)而不是string[3];首選 DOM 的操作方法來獲得元素引用,而不是某一應用特定的快捷方法)。
簡易的原型繼承
如果你想在 JavaScript 中繼承你的對象,請遵循一個簡易的模式來創建此繼承。如果你預計你會遇上復雜對象的繼承,那可以考慮采用一個繼承庫,比如 Proto.js by Axel Rauschmayer
簡易繼承請用以下方式:
javascript(function(log){ 'use strict';// 構建函數 function Apple(name) { this.name = name; } // 定義一個 apple 的方法 Apple.prototype.eat = function() { log('吃 ' + this.name); };
// 構建函數 function GrannySmithApple() { // 執行父級構建函數 Apple.prototype.constructor.call(this, 'Foobar'); } // 當復制時,設置父級 prototype Object.create FoobarApple.prototype = Object.create(Apple.prototype); // 設置構建函數為子類型 FoobarApple.prototype.constructor = FoobarApple;
// 呼叫頂級方法 FoobarApple.prototype.eat = function() { // 確定您已經通過 call(this) 將它應用到了正確的對象中 Apple.prototype.eat.call(this);
log('可憐的 Foobar');
};
// 實例 var apple = new Apple('測試蘋果'); var foobarApple = new FoobarApple();
log(apple.name); // 測試蘋果 log(foobarApple.name); // Foobar
// 實例檢測 log(apple instanceof Apple); // true log(apple instanceof FoobarApple); // false
log(foobarApple instanceof Apple); // true log(foobarApple instanceof FoobarApple); // true
// 執行呼叫了頂級方法的方法 foobarApple.eat();
}(window.console.log));</pre>
使用閉包
閉包的創建也許是 JS 最有用也是最易被忽略的能力了,查看《關于閉包如何工作的合理解釋》。
切勿在循環中創建函數
在簡單的循環語句中加入函數是非常容易形成閉包而帶來隱患的。下面的例子就是一個典型的陷阱:
不推薦
javascript(function(log, w){ 'use strict';// 在本閉包中,numbers 與 i 被定義 var numbers = [1, 2, 3], i;
for(i = 0; i < numbers.length; i++) { w.setTimeout(function() { // 本斷代碼最終將彈出三次對話框,而內容均是: // 第 3 個數字為 undefined // 這是因為,i 在循環之前被定義,而 for 循環之后,將 i 的值變為 3 // 接著, alert 執行,此時, alert 中的 i 將只為 3, // 同時,numbers[3] 為 undefined w.alert('第 ' + i + ' 個數字為 ' + numbers[i]); }, 0); }
}(window.console.log, window));</pre>
接下來的改進雖然已經解決了上述例子中的問題或 bug,但還是違反了不在循環中創建函數或閉包的原則。
不推薦
javascript(function(log, w){ 'use strict';// 在本閉包中,numbers 與 i 被定義 var numbers = [1, 2, 3], i;
for(i = 0; i < numbers.length; i++) { // 創建一個新的閉包函數 (function(index, number){ w.setTimeout(function() { // Will output as expected 0 > 1, 1 > 2, 2 > 3 w.alert('Index ' + index + ' with number ' + number); }, 0); }(i, numbers[i])); }
}(window.console.log, window));</pre>
接下來的改進已解決問題,而且也遵循了規范。可是,你會發現看上去似乎過于復雜繁冗了,應該會有更好的解決方案吧。
不完全推薦
javascript(function(log, w){ 'use strict';// 在本閉包中,numbers 與 i 被定義 var numbers = [1, 2, 3], i;
// 在 for 循環外面創建一個函數 function alertIndexWithNumber(index, number) { return function() { w.alert('Index ' + index + ' with number ' + number); }; }
// for(i = 0; i < numbers.length; i++) { w.setTimeout(alertIndexWithNumber(i, numbers[i]), 0); }
}(window.console.log, window));</pre>
將循環語句轉換為函數執行的方式問題能得到立馬解決,每一次循環都會對應地創建一次閉包。函數式的風格更加值得推薦,而且看上去也更加地自然和可預料。
推薦
javascript(function(log, w){ 'use strict';// 在本閉包中,numbers 與 i 被定義 var numbers = [1, 2, 3], i;
numbers.forEach(function(number, index) { w.setTimeout(function() { w.alert('Index ' + index + ' with number ' + number); }, 0); });
}(window.console.log, window));</pre>
eval 函數(魔鬼)
eval()不但混淆語境還很危險,總會有比這更好、更清晰、更安全的另一種方案來寫你的代碼,因此盡量不要使用evil函數。
this 關鍵字
只在對象構造器、方法和在設定的閉包中使用this關鍵字。this的語義在此有些誤導。它時而指向全局對象(大多數時),時而指向調用者的定義域(在eval中),時而指向 DOM 樹中的某一節點(當用事件處理綁定到 HTML 屬性上時),時而指向一個新創建的對象(在構造器中),還時而指向其它的一些對象(如果函數被call()和apply()執行和調用時)。
正因為它是如此容易地被搞錯,請限制它的使用場景:
- 在構造函數中
- 在對象的方法中(包括由此創建出的閉包內)
</ul>首選函數式風格
函數式編程讓你可以簡化代碼并縮減維護成本,因為它容易復用,又適當地解耦和更少的依賴。
接下來的例子中,在一組數字求和的同一問題上,比較了兩種解決方案。第一個例子是經典的程序處理,而第二個例子則是采用了函數式編程和 ECMA Script 5.1 的數組方法。
例外:往往在重代碼性能輕代碼維護的情況之下,要選擇最優性能的解決方案而非維護性高的方案(比如用簡單的循環語句代替forEach)。
</blockquote>不推薦
javascript(function(log){ 'use strict';var arr = [10, 3, 7, 9, 100, 20], sum = 0, i;
for(i = 0; i < arr.length; i++) { sum += arr[i]; }
log('數組 ' + arr + ' 的和為:' + sum)
}(window.console.log));</pre>
推薦
javascript(function(log){ 'use strict';var arr = [10, 3, 7, 9, 100, 20];
var sum = arr.reduce(function(prevValue, currentValue) { return prevValue + currentValue; }, 0);
log('數組 ' + arr + ' 的和為:' + sum)
}(window.console.log));</pre>
另一個例子通過某一規則對一個數組進行過濾匹配來創建一個新的數組。
不推薦
javascript(function(log){ 'use strict';var numbers = [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('列表 numbers ' + numbers + ' 中只有 ' + numbersGreaterTen + ' 個數字大于 10');
}(window.console.log));</pre>
推薦
javascript(function(log){ 'use strict';var numbers = [11, 3, 7, 9, 100, 20, 14, 10];
var numbersGreaterTen = numbers.filter(function(element) { return element > 10; });
log('列表 numbers ' + numbers + ' 中只有 ' + numbersGreaterTen + ' 個數字大于 10');
}(window.console.log));</pre>
使用 ECMA Script 5
建議使用 ECMA Script 5 中新增的語法糖和函數。這將簡化你的程序,并讓你的代碼更加靈活和可復用。
數組和對象的屬性迭代
用 ECMA5 的迭代方法來迭代數組。使用Array.forEach或者如果你要在特殊場合下中斷迭代,那就用Array.every。
javascript(function(log){ 'use strict';// 遍歷一個數據,然后在某個條件達到時,終止遍歷 [1, 2, 3, 4, 5].every(function(element, index, arr) { log(element + ' at index ' + index + ' in array ' + arr);
if(index !== 5) { return true; }
});
// 定義一個簡單的 JavaScript 對象 var obj = { a: 'A', b: 'B', 'c-d-e': 'CDE' };
// 遍歷對象的鍵 Object.keys(obj).forEach(function(element, index, arr) { log('鍵 ' + element + ' 的值為 ' + obj[element]); });
}(window.console.log));</pre>
不要使用 switch
switch在所有的編程語言中都是個非常錯誤的難以控制的語句,建議用if else來替換它。
數組和對象字面量
用數組和對象字面量來代替數組和對象構造器。數組構造器很容易讓人在它的參數上犯錯。
不推薦
javascript// 長度為 3. var a1 = new Array(x1, x2, x3);// 長度為2 var a2 = new Array(x1, x2);
// 如果 x1 是一個數字,并且他還是一個自然數,那么長度將為 x1 // 如果 x1 是一個數字,但是它不是一個自然數,那么將報出一個異常 // 其它的情況下數組將包含一個只有 x1 一個元素的數組 var a3 = new Array(x1);
// 長度為0 0. var a4 = new Array();</pre>
正因如此,如果將代碼傳參從兩個變為一個,那數組很有可能發生意料不到的長度變化。為避免此類怪異狀況,請總是采用更多可讀的數組字面量。
推薦
javascriptvar a = [x1, x2, x3]; var a2 = [x1, x2]; var a3 = [x1]; var a4 = [];對象構造器不會有類似的問題,但是為了可讀性和統一性,我們應該使用對象字面量。
不推薦
javascriptvar o = new Object();var o2 = new Object(); o2.a = 0; o2.b = 1; o2.c = 2; o2['strange key'] = 3;</pre>
應該寫成這樣:
推薦
javascriptvar o = {};var o2 = { a: 0, b: 1, c: 2, 'strange key': 3 };</pre>
修改內建對象的原型鏈
修改內建的諸如Object.prototype和Array.prototype是被嚴厲禁止的。修改其它的內建對象比如Function.prototype,雖危害沒那么大,但始終還是會導致在開發過程中難以 debug 的問題,應當也要避免。
自定義 toString() 方法
你可以通過自定義toString()來控制對象字符串化。這很好,但你必須保證你的方法總是成功并不會有其它副作用。如果你的方法達不到這樣的標準,那將會引發嚴重的問題。如果toString()調用了一個方法,這個方法做了一個斷言1 ,當斷言失敗,它可能會輸出它所在對象的名稱,當然對象也需要調用toString()。
圓括號
一般在語法和語義上真正需要時才謹慎地使用圓括號。不要用在一元操作符上,例如delete,typeof和void,或在關鍵字之后,例如return,throw,case,new等。
字符串
統一使用單引號'',不使用雙引號""。這在創建 HTML 字符串非常有好處:
var msg = '這是一段HTML <div class="makes-sense"></div>';三元條件判斷(if 的快捷方法)
用三元操作符分配或返回語句。在比較簡單的情況下使用,避免在復雜的情況下使用。沒人愿意用 10 行三元操作符把自己的腦子繞暈。
不推薦
javascriptif(x === 10) { return 'valid'; } else { return 'invalid'; }推薦
return x === 10 ? 'valid' : 'invalid';
斷言一般指程序員在測試測序時的假設,一般是一些布爾表達式,當返回是 true 時,斷言為真,代碼運行會繼續進行;如果條件判斷為 false,代碼運行停止,你的應用被終止。 ?
</li> </ol> </div> 來自:http://hao.jser.com/archive/7717/