JavaScript Best Practice Part.1: JSLint

前陣子有長輩問我 JavaScript 的 Best Practice,一時還真講不出來,因為我不太有把經驗整理出來的習慣,所以有了這系列的文章,雖然會有幾篇不知道XD。

那天被問到的時候我一時只想的到先過 JSLint 再說,所以第一篇就先從 JSLint 開始講起,JSLint 是 Douglas Crockford 在 2002 年時發表的 JavaScript 程式碼的檢查工具,除了基本的語法檢查外,還多了不少限制和要求,可以讓你的程式碼品質提昇,光是讓你的程式碼能過 JSLint 檢查就可以減少很多可能的問題了,接下來就針對各項主要的檢查項目做介紹。

全域變數

全域變數很危險,因為這些變數可能會和其他的程式碼產生衝突,畢竟你可以控制自己的程式不用全域變數,但是你無法控制其他人的,甚至是其他人惡意 code,再加上 JavaScript 中只要變數使用前沒有先用var宣告過,該變數就會是全域變數,所以在 JavaScript 中是很容易誤用全域變數的,一般的作法是把自己的 code 全部放到同一個 namespace (物件)之下,這樣就可以讓程式使用到的全域變數最小化到只剩一個,或是把你的程式碼整個用 closure 包起來。JSLint 限制全域變數的使用,沒有宣告的全域變數都會被視為錯誤而跑出警告,宣告的方法是寫在註解裡面如下:

/*global myApp:true, myApp2:true */

結尾分號

每行結尾都要有分號,沒寫雖然程式碼也可以跑,但是寫了比較沒事,尤其是在使用簡單的 JavaScript 壓縮工具時很有差。不過有時候程式敘述沒完,到下一行還要繼續,像是太長的字串要分兩行,然後用 + 接起來時該怎麼辦? JSLint 其實也是可以接受,只是加號要放在行尾,不是行首。

另外有有嚴格要求函數結尾的分號使用,分成兩種:

var f1 = function () {};
function f2() {}

f1 這種宣告會要求要有分號結尾,f2 的方式則要求不可以加上多餘的分號。

if, for 一定要大括號

當然不寫大括號會有一行被認定是在該條件之內需要被執行的程式碼,不過為了保險不誤判起見(容易發生在修改程式碼時), JSLint 強制要求一定要大括號。

for in 要檢查該屬性是否屬於該物件直接擁有

簡單說就是要這樣的寫法:

for (var i in obj) {
    if (obj.hasOwnProperty(i)) {
        ....
    }
}

這是為了確保不會存取到不想存取的屬性,舉例來說,像是 prototype 這套 JavaScript 函式庫會對原生的陣列物件加上一些新的 method 到它的 prototype 裡面,這時如果想用 for in 寫法來跑這個陣列就會把這些新增的 method 也一起抓出來,所以需要用hasOwnProperty來檢查一下,不過我個人的建議是,不要用 for in 寫法,一來它效率比較慢,二來可能會遇到這種問題,還要一個一個檢查,所以能不用就不用最好,尤其是跑陣列時。

不要用 with

總之就是不要用 with,因為會讓你產生混淆,不知道你存取到的變數到底是哪個 scope 來的,而且還有效能問題, with 會多產生一層 scope chain ,本來直接可以存取到的變數反而跑到第二層去了。

===and!==

和 null, 0, undefined, true, false 這些值比較時,一定要用===!==,因為 JavaScript 有神祕的型別轉換,讓你的 null==undefined 但是 null != false ,當然還有其他各種有趣的比較,總之你確定是要判斷是否是以上列舉值的其中任一種時,就用===!==吧,如果只是要 true/false 判斷,可以用 !! 來把值轉成 boolean。

eval is evil

不要用 eval ,或是丟程式碼到 setTimeout、setInterval、Function 裡(和 eval 等價),雖然少數時候會需要 eval,不過大部分的程式應用可以不使用 eval, 它有安全性的問題和效率的問題,如果需要處理 JSON 格式的資料,那大部分的 Library 都有函數可以處理,沒的話也可以使用 Crockford 的 json2.js ,它相容於現在新瀏覽器內建的 JSON 物件,可以安心使用。

說到 JSON 就不得不提一下,其實它的字串只允許使用雙括號 " ,而且物件屬性名稱有要求一定要用字串形式,用雙括號包起來,不是只有字串值才需要,詳細可以看看 JSON 官網 的鐵路圖,只是因為很多人使用 eval 來讀 JSON 資料,才會產生誤解以為 JSON 和 JavaScript 語法完全一樣,嚴格說來只是子集而已,這邊衍生的問題是,錯誤的 JSON 格式在用原生 JSON 或是 json2.js 時會過不了。

使用 {} 建立物件, [] 建立陣列

不要用 new Object() 和 new Array() 了,直接用 {} 和 [] 吧,還可以同時給初始值,速度也比較快。當然newString()new Number()new Boolean()也別用...

parseInt

parseInt 可以指定要是幾進位的整數形式,不過第二個參數也可以省略,只是預設值不是固定的,如果你的字串是 0 開頭的話,它會幫你當成 8 進位,如果是 0x 開頭的話會當成 16 進位,不過後者的問題比較小,問題是前者,如果你想要把 09 轉成整數,你本來預期是 9 ,但是因為被當成 8 進位,09 不存在,所以他會回傳 0 ,因此 JSLint 要求使用 parseInt 時一定要加上第二個參數,指定字串顯示的數值是幾進位的形式。

使用 obj.name 取代 obj["name"]

可以的話就使用前者的方法,速度比較快,也比較省程式碼。

變數只能宣告一次

在同一個 scope 下,同樣的變數名稱只允許宣告一次,當然也是為了錯誤認知。

設定

其實 JSLint 有不少選項可以設定,甚至可以允許 eval ,畢竟有時候會有需要,和全域變數一樣是寫在註解裡面,我自己現在的設定如下:

/*jslint browser: true, forin: true, onevar: true, white: true*/

第一個 browser 選項是會提供部分瀏覽器內建的全域變數和函數,我不知道為什麼有些函數反而會關掉,像是escape有使用到的話還要自己加到 global 裡面。

第二個 forin 是前面提到的 hasOwnProperty 檢查,我通常是關掉不檢查的(設成 true),因為我很少需要物件繼承的複雜資料結構,所以比較不會有使用 for in 的需要,加上陣列也不會用 for in 來跑,所以就省去這項檢查了。

第三個 onevar 是限制每個 function 只能有一次 var 宣告,這也是一個效率問題,後面的文章會再詳細介紹。

第四個則是嚴格的縮排檢查,預設是四個空白,另外在有名稱的 function 宣告時會要求名稱後面直接接 () 中間不留空白, anonymous function 則否,當然主要目的是為了讓兩者區隔比較明顯,不會把 "function" 看成函數名稱。

var f1 = function () {};
function f2() {}

像是前面舉過的例子,f1 後面的函數在宣告時是屬於匿名的,他的 () 就要和前面的 "function" 間留一個空白,f2 就要求函數名稱和後面的 () 接在一起。

其他

還有不少設定和檢查說明我這篇文章沒有提到,可以參考 JSLint Instruction ,而除了這些其實還有不少細部檢查沒列出的,就要等遇到時才知道了(要翻 原始碼 也是可以的)。

下一篇文章會講一些和效能有關的東西,這兩篇應該都還很偏 coding style XD。

補充:查了一下發現 escape/upescape 已經不推薦使用了,以後請用 encodeURI/decodeURI 。