高質量JavaScript程式碼基本要點彙總(持續更新)
本博文包括一些與程式碼不太相關的習慣,但對整體程式碼的建立息息相關,包括撰寫API文件、執行同行評審以及執行JSLint。這些習慣和最佳做法可以幫助你寫出更好的,更易於理解和維護的程式碼,這些程式碼在幾個月或是幾年之後再回過頭看看也是會覺得很自豪的。所以將不斷從專案中,日常累積,並時常回頭來回味。。。
全域性變數的問題
全域性變數的問題在於,JavaScript應用程式和web頁面上的所有程式碼都共享了這些全域性變數,他們住在同一個全域性名稱空間,所以當程式的兩個不同部分定義同名但不同作用的全域性變數的時候,命名衝突在所難免。
1) 避免不自覺創建出全域性變數
function sum(x, y){ result = x + y; return result; }
程式碼照常執行正常,但是在呼叫函式後最後的結果就多了一個全域性名稱空間。極有可能遇上命名衝突,而不被察覺
2)隱式全域性變數
function foo(){
var a = b = 0;
// ...
}
從右到左的賦值,首先,是賦值表示式b = 0,此情況下b是未宣告的。這個表示式返回值是0,在將其分配給通過var定義的這個區域性變數a。所以就會想全域性空間新增一個b的全域性變數。
3) 忘記var 的副作用
通過var 建立的全域性變數(任何函式之外的程式中建立)是不能被刪除的
無var 建立的隱式全域性變數(無視是否在函式中建立)是能被刪除的
var global_var = 1; global_novar = 2; //反例 (function(){ global_fromfunc = 3; //反例 }()); //試圖刪除 delect global_var; //false delect global_novar; //true delect global_fromfunc; //true //測試該刪除 typeof global_var; //"number" typeof global_noval; //"undefined" typeof global_fromfunc; //"undefined"
預解析:var散佈的問題
JavaScript中,你可以在函式的任何位置宣告多個var語句,並且它們就好像是在函式頂部宣告一樣發揮作用,這種行為稱為 hoisting(懸置/置頂解析/預解析)。當你使用了一個變數,然後不久在函式中又重新宣告的話,就可能產生邏輯錯誤。對於JavaScript,只 要你的變數是在同一個作用域中(同一函式),它都被當做是宣告的,即使是它在var宣告前使用的時候。
myname = "global"; //全域性變數 function func(){ alert(myname); //'nudefined' var myname = "local"; alert(myname); //'local' } func();
在上述程式碼中,你可能會以為第一個alert彈出的是”global”,第二個彈出”loacl”。這種期許是可以理解的,因為在第一個alert 的時候,myname未宣告,此時函式肯定很自然而然地看全域性變數myname,但是,實際上並不是這麼工作的。第一個alert會彈 出”undefined”是因為myname被當做了函式的區域性變數(儘管是之後宣告的),所有的變數聲明當被懸置到函式的頂部了。因此,為了避免這種混亂,最好是預先宣告你想使用的全部變數。
for迴圈
在for迴圈中,可以迴圈取得陣列或是陣列類似物件的值,例如arguments和HTMLcollection物件。通常迴圈形式如下:
for(var i=0; i< myarray.length; i++){
...
}
這種形式的迴圈的不足在於每次迴圈的時候都要去獲取一次陣列的長度,尤其是當myarray不是陣列的時候,而是一個HTMLCollection物件(DOM方法返回的物件)的時候。要獲取長度代價很高。由此可以如下優化:
for(var i=0, max=myarray.length; i<max; i++){
...
}
for-in迴圈
for-in迴圈應該用在非陣列物件的遍歷上,使用for-in 進行迴圈也被稱為“列舉”
從技術上講,for-in迴圈陣列不被推薦,因為如果陣列物件已被自定義的功能增強,就可能發生邏輯錯誤。另外,在for-in中,屬性列表的順序(序列)是不能保證的。所以最好陣列使用正常的for迴圈,物件使用for-in迴圈。
有個很重要的hasOwnProperty()
方法,當遍歷物件屬性的時候可以過濾掉從原型鏈上下來的屬性。
var man = {
hands: 2,
legs: 2,
head: 1
};
.......
if(typeof Object.prototype.clone ==='undefined'){
Object.prototype.clone = function(){};
}
在這個例子中,我們有一個使用物件字面量定義的名叫man的物件。在man定義完成後的某個地方,在物件原型上增加了一個很有用的名叫 clone()的方法。此原型鏈是實時的,這就意味著所有的物件自動可以訪問新的方法。為了避免列舉man的時候出現clone()方法,你需要應用hasOwnProperty()
方法過濾原型屬性。如果不做過濾,會導致clone()函式顯示出來,在大多數情況下這是不希望出現的
for (var i in man){
if(man.hasOwnProperty(i)){ //過濾
console.log(i, ":", map[i]);
}
}
不擴充套件內建原型
屬性新增到原型中,可能會導致不使用hasOwnProperty屬性時在迴圈中顯示出來,這會造成混亂。因此,不增加內建原型是最好的。你可以指定一個規則,僅當下面的條件均滿足時例外:
- 可以預期將來的ECMAScript版本或是JavaScript實現將一直將此功能當作內建方法來實現。例如,你可以新增ECMAScript 5中描述的方法,一直到各個瀏覽器都迎頭趕上。這種情況下,你只是提前定義了有用的方法。
- 如果您檢查您的自定義屬性或方法已不存在——也許已經在程式碼的其他地方實現或已經是你支援的瀏覽器JavaScript引擎部分。
- 你清楚地文件記錄並和團隊交流了變化
-
if(typeof Object.prototype.myMethod !== "function"){ Object.prototype.myMethod = function(){ ... }; }
當滿足那三個條件,就可以用上述程式碼進行自定義的新增。
避免隱式型別轉換
JavaScript的變數在比較的時候會隱式型別轉換。這就是為什麼一些諸如:false == 0 或 “” == 0 返回的結果是true。為避免引起混亂的隱含型別轉換,在你比較值和表示式型別的時候始終使用===和!==操作符。
var zero = 0;
if(zero === false){
//不執行,不會進行強制轉換
}
//反例
if(zero == false){
//執行了
}
eval()是魔鬼。此方法接受任意的字串,並當作JavaScript程式碼來處理。當有 問題的程式碼是事先知道的(不是執行時確定的),沒有理由使用eval()。如果程式碼是在執行時動態生成,有一個更好的方式不使用eval而達到同樣的目 標。例如,用方括號表示法來訪問動態屬性會更好更簡單:
//反例
var prototype = "name";
alert(eval("obj." + property));
//更好的
var property = "name";
alert(obj[property]);
同樣重要的是要記住,給setInterval(), setTimeout()和Function()建構函式傳遞字串,大部分情況下,與使用eval()是類似的,因此要避免。使用新的Function()構造就類似於eval(),應小心接近。
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);
}()); //log 3
console.log(typeof un); //number
console.log(typeof deux); //undefined
console.log(typeof trois); //undefined
eval()和Function構造不同的是eval()可以干擾作用域鏈,它會訪問和修改它外部作用域中的變數,而Function要好很多,它只會使用全域性變數
(function(){
var local = 1;
eval("local = 3; console.log(local)"); //logs 3
console.log(local);
}());
(function(){
var local = 1;
Function("console.log(typeof local);")(); //logs undefined
}());
ParseInt()下的數值轉換
使用parseInt()你可以從字串中獲取數值,該方法接受另一個基數引數,這經常省略,但不應該。當字串以“o“開頭的時候就可能會出問題,例如,部分時間進入表單域,在ECMAScript 3中,開頭為”0″的字串被當做8進位制處理了,但這已在ECMAScript 5中改變了。為了避免矛盾和意外的結果,總是指定基數引數。
var month = "06",
year = "09";
month = parseInt(month,10);
year = parseInt(year,10);
此例中,如果忽略了基數引數,如parseInt(year),返回的值將是O,因為”09“被當做8進位制(好比執行了parseInt(year, 8)),而而09在8進制中不是個有效數字。
左花口號的位置
這個例項中,仁者見仁智者見智,但也有個案,括號位置不同會有不同的行為表現。這是因為分號插入機制(semicolon insertion mechanism)——JavaScript是不挑剔的,當你選擇不使用分號結束一行程式碼時JavaScript會自己幫你補上。這種行為可能會導致麻 煩,如當你返回物件字面量,而左括號卻在下一行的時候:上一個Funtion等同於下一個
//警告:意外的返回值
function func(){
return
//下面程式碼不執行
{
name: "Batman";
}
}
//警告: 意外的返回值
function func(){
return undefined;
//下面的程式碼不執行
{
name:"Batman";
}
}
空格
空格的使用同樣有助於改善程式碼的可讀性和一致性。在寫英文句子的時候,在逗號和句號後面會使用間隔。在JavaScript中,你可以按照同樣的邏輯在列表模樣表示式(相當於逗號)和結束語句(相對於完成了“想法”)後面新增間隔。
適合使用空格的地方:
1) for迴圈分號分開後的部分: 如for (var i =0; i < 10; i++ ) { ... }
2) for迴圈中初始化的多變數:for ( var i = 0, max = 10; i < max; i += 1) { ... }
3) 分割陣列項的逗號的後面 : var a = [ a, b, c] ;
4) 限定函式引數:myFunc(a, b, c)
5) 物件屬性逗號的後面以及分隔屬性名和屬性值的冒號的後面: var o = {a: 1, b: 2}
6) 函式宣告的花括弧的前面 : function myFunc() {}
7) 匿名函式表示式function的後面: var myFunc = function () {}
8) 使用空格分開所有的操作符和操作物件是另一個不錯的使用,這意味著在+, -, *, =, <, >, <=, >=, ===, !==, &&, ||, +=
等前後都需要空格。
其他命名方式
1. 由於javascript沒有定義常量的方法,所以開發者都採用全部單詞大寫的規範來命名這個程式生命週期中都不會改變的變數
var PI = 3.14,
MAX_WIDTH = 800;
2. 全域性變數名字全部大寫。全部大寫命名全域性變數可以加強減小全域性變數數量的實踐,同時讓它們易於區分。(可選)
3. 另外一種使用規範來模擬功能的是私有成員。雖然可以在JavaScript中實現真正的私有,但是開發者發現僅僅使用一個下劃線字首來表示一個私有屬性或方法會更容易些。
var Person = {
getName: function() {
return this._getFirst() + '' + this._getLast();
},
_getFirst: function() {
//...
}
_getLast: function() {
//...
}
};
在此例中,getName()
就表示公共方法,部分穩定的API。而_getFirst()
和_getLast()
則表明了私有。它們仍然是正常的公共方法,但是使用下劃線字首來警告person物件的使用者這些方法在下一個版本中時不能保證工作的,是不能直接使用的。
下面是一些常見的_private規範:
- 使用尾下劃線表示私有,如name_和getElements_()
- 使用一個下劃線字首表_protected(保護)屬性,兩個下劃線字首表示__private (私有)屬性
- Firefox中一些內建的變數屬性不屬於該語言的技術部分,使用兩個前下劃線和兩個後下劃線表示,如:__proto__和__parent__。