寫一篇關於最佳實踐的文章是十分困難的事情。你們將要讀到的內容是顯而易見,但是明智的做法。
然而,多年來在網上瀏覽並檢視其他開發者提交的程式碼的經歷告訴我,想要在網路的程式設計環境中找到一定的常識是幾乎不可能的事,並且一旦你身處於一個專案中時,隨著 deadline 的步步逼近,“做明智且有邏輯的事”就會在優先順序列表中被推得越來越遠。
所以,我決定創作這篇文章更簡單地讓你瞭解,什麼是最佳實踐,以及我這麼多年積累的優秀建議。其中大部分都來之不易(通過實驗等方法得到的)。記住下面給出的建議,讓它們成為你大腦的一部分,這是一個快速有效的方法幫助你不用考慮太多就能接受這些建議。我相信你一定會發現與你的想法不一致的地方,這是好事 —— 你應該質疑讀到的東西,然後努力找到更好的解決方法。不過,我發現遵循下面這些原則可以使我成為一名更有效率的開發者,也方便了其他開發者在我的工作基礎上進行後續開發。
下面是本文的組織結構:
- 給變數和函式命名 ——變數名和函式名儘量簡短且易讀
- 避免全域性
- 堅持嚴格的編碼規範
- 根據需要儘量多地新增註釋,但也別太多
- 避免在 JavaScript 中混合其他技術
- 當有其實用意義的時候,可以考慮使用簡化符號
- 模組化 —— 每個任務對應一個函式
- 漸進增強
- 允許配置與轉換
- 避免多重巢狀
- 優化迴圈
- 最小化 DOM 訪問
- 不要屈服於瀏覽器的獨有特性
- 不要相信任何資料
- 用 JavaScript 增加功能,不要建立太多內容
- 站在巨人的肩上構建
- 開發環境程式碼並不等於生產環境程式碼
給變數和函式命名 ——變數名和函式名儘量簡短且易讀
一種很簡單但又很可怕的情況是你經常會在 JavaScript 中碰到像 x1
、fe2
或者 xbqne
的變數名,或者 —— 另一種極端命名 —— 像incrementorForMainLoopWhichSpansFromTenToTwenty
或者createNewMemberIfAgeOverTwentyOneAndMoonIsFull
這樣的變數名。
這種命名沒有任何意義 —— 好的變數和函式命名應該易於理解,並告知它的作用 —— 不多也不少。還有一個需要避免的陷阱是在命名中將數值與功能結合。由於合法飲酒年齡因國家而異,isLegalDrinkingAge()
就比 isOverEighteen()
更加適合,除了飲酒外,還有其他事情也是一樣需要考慮年齡限制的。
匈牙利命名法是一種值得采用的不錯的命名方案(還有一些其他的命名方案可以考慮),優勢在於你知道應該怎樣去命名,而不僅僅知道它是什麼名字。
舉個例子,如果你有一個名為 familyName
的變數,它是一個字串,那麼根據 “匈牙利命名法” 你應該將其寫為 sFamilyName
。一個名為 member
的物件可被寫為 oMember
,一個名為 isLegal
的布林物件可被寫為 bIsLegal
。在某些情況下,這種命名資訊量很大,但在某些情況下又似乎額外開銷太大,這取決於你是否選擇使用它。
保持英文也是一個好主意。程式語言都使用英文,所以為何不把這個作為你程式碼邏輯中的一環呢。除錯一段時間的韓語與斯洛維尼亞語程式碼後,我向你保證這對於一名非母語者來說的確不是一件有趣的事。
把你的程式碼當成敘事。如果你可以一行一行地閱讀並理解它在講述什麼,說明你做的不錯。如果你需要使用畫板來理清邏輯流程,那麼你的程式碼還需要再優化一下。如果你想要對比一個真實的世界,試著去閱讀 Dostojewski(俄國作家)的作品吧 —— 看見寫有 14 個俄羅斯名字的一頁後,我完全迷茫了,其中有 4 個都是假名。不要寫出那樣的程式碼 —— 雖然那樣看起來更像藝術而非產品,但並不是一件好事。
避免全域性
全域性變數和函式名是一個非常糟糕主意。因為頁面中的每個 JavaScript 檔案都在同一個範圍內執行。如果你的程式碼中有全域性變數或者函式,後面指令碼中包含的相同變數和函式名將會覆蓋你的全域性變數或函式。
下面是幾個避免使用全域性變數的變通方法 —— 現在我們一個個展示它們。如果你有三個函式和一個變數,就像這樣:
1 2 3 4 5 6 7 8 9 10 |
var current = null; function init() { … } function change() { … } function verify() { … } |
你可以使用一個物件字面量來
保護它們不被重寫:
1 2 3 4 5 6 7 8 9 10 11 12 |
var myNameSpace = { current:null, init:function() { … }, change:function() { … }, verify:function() { … } } |
這會起作用,但有一個缺點 —— 呼叫函式或改變變數值時,你都需要使用主物件的名字:init()
是 myNameSpace.init()
,current
是 myNameSpace.current
等等。這很討厭並重復。
但我們可以簡單地使用一個匿名函式包含所有事物並通過這種方式保護此域。同樣意味著你不需將語法從 function name()
轉換為 name:function()
。這個特性被稱為模組化開發:
1 2 3 4 5 6 7 8 9 10 11 12 |
myNameSpace = function() { var current = null; function init() { … } function change() { … } function verify() { … } }(); |
繼續,這樣改進仍有問題。這些方法與變數對於外界來說都不再可用。如果你想讓它們可用,需要在一個 return 程式碼塊裡包含你想要讓其變成 public 的事物:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
myNameSpace = function() { var current = null; function verify() { … } return { init:function() { … } change:function() { … } } }(); |
在連結兩個方法和改變語法方面,這幾乎把我們帶回了起點。因此我更喜歡像下面一樣(我賦予其“展示性模組化”稱號):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
myNameSpace = function() { var current = null; function verify() { … } return { init:function() { … } change:function() { … } } }(); |
代替返回屬性和函式,我僅僅返回了指向它們的內容。這使得其他地方可以不通過 myNameSpace
,更簡單地呼叫函式以及訪問變數。
這也意味著對於一個函式,如果你想要為內部連結賦予一個更長的描述性的名稱,但同時為外部賦予一個更短的名稱時,可以擁有一個公共別名:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
myNameSpace = function() { var current = null; function init() { … } function change() { … } function verify() { … } return{ init:init, set:change } }(); |
現在呼叫 myNameSpace.set()
將會連結到 change()
方法。
如果你完全不需要外部使用你的變數或者函式,可以簡單地使用另一對括號包圍整個結構,執行的時候不宣告任何名稱。
1 2 3 4 5 6 7 8 9 10 11 12 |
(function() { var current = null; function init() { … } function change() { … } function verify() { … } })(); |
這樣就可以將所有東西裝進一個整潔的小包裹裡,外界無法訪問,但裡面卻能夠輕鬆地共享變數和函式。
堅持嚴格的程式碼規範
瀏覽器對於 JavaScript 語法是十分寬容的,但這不能成為你編寫完全依賴瀏覽器執行的草率程式碼的理由。
讓你的程式碼通過 JSLint(一個可以給予你程式碼中語法警告的詳細報告及意義的 JavaScript 審查工具)執行是檢查自己程式碼質量最簡單的方法。人們已為各種編輯器編寫了外掛(比如 JS Tools for TextMate),可以在儲存的時候自動檢查你的程式碼。
JSLint 返回的結果可能會比較棘手,正如其開發者 Douglas Crockford 所說,這個工具會打擊你的感受。然而當我安裝 TextMate JS bundle 並開始遵循 JSLint 的規範編碼,發現自己寫出了比以前好出不少的程式碼。
清晰有理的程式碼意味著更少修復令人困惑的 bug,更簡單地移交給其他開發者以及更棒的安全性。當你依賴於從各處竊取程式碼並使其工作時,很可能還有一處安全漏洞也使用了同樣的程式碼。補充一句,當瀏覽器修復了自己不完善的地方,你的程式碼在其下個版本就會失效。
另外,有效的程式碼意味著可以通過指令碼轉換為其他格式,而 hacky code(醜陋的程式碼)卻需要人為做那樣的事情。
如果需要的話,儘可能多地新增註釋,但也不要太多
註釋是你傳達給其他開發者的資訊(和你自己,如果你在其他地方工作幾個月後回來檢視你的程式碼)。數年來人們一直激烈地爭吵到底要不要使用註釋,主要的爭論點在於優秀的程式碼應該能自我解釋。
在我看來,這場爭論的辯解實在太主觀了。你沒辦法期待每一名開發者都能靠著同樣的解釋去理解一些程式碼在幹什麼。
如果你正確地新增註釋,它們不會影響到任何人。我們會在這篇文章的最後一點說明,但是如果終端使用者們在程式碼的最後看見你的一堆註釋,就不太好了。
另一個竅門是保持節制。如果你想要使用 /* */
符號進行註釋,那麼就應該說明一件重要的事情。使用 //
的單行註釋會造成一個問題,人們可能因為你沒有剝離註釋而看輕你的程式碼,一般來說,你並不需要那麼多註釋。
如果你想註釋掉自己的一部分程式碼等待後面開發時使用或除錯其他程式碼,下面有一種可以選擇的技巧:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
module = function() { var current = null; function init() { … }; /* function show() { current = 1; }; function hide() { show(); }; */ return{ init:init, show:show, current:current } }(); |
編寫程式碼時在閉合的 */
之前增加一個 //
,這樣你就可以通過在開始的 /*
前面簡單地增加或刪除一個 /
來註釋或取消註釋整個程式碼塊。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
module = function() { var current = null; function init() { … }; /* function show() { current = 1; }; function hide() { show(); }; // */ return{ init:init, show:show, current:current } }(); |
就如上面所示的那樣編寫程式碼,在開始的 /*
前面增加一個 /
,將會把多行註釋轉換為兩個單行註釋,“unhiding”(不再隱藏)之間的程式碼,使其被執行。移除這個 /
則會重新註釋。
在 JavaDoc 規範中大量關於應用註釋文件的內容十分有意義。你在編碼的同時就在生成自己產品的全部文件。Yahoo User Interface library 的成功在一定程度上得歸功於這點,其甚至可以成為一個工具幫助你為自己產品建立一份相同的文件。在你成為一名更有經驗的 JavaScripter 之前都不太需要關心這個,我只是為了完整性提及一下 JavaDoc。
避免在 JavaScript 中混合其他技術
你可以在文件中同時使用 JavaScript 和 DOM 創造自己需要的任何事物,但這肯定不是完成同一件事情最有效率的方式。下面的程式碼給每個 class 為 “mandatory” 的輸入框增加了一個紅色 border,裡面沒有任何事物。
1 2 3 4 5 6 7 8 9 |
var f = document.getElementById('mainform'); var inputs = f.getElementsByTagName('input'); for(var i = 0, j = inputs.length; i < j; i++) { if (inputs[i].className === 'mandatory' && inputs[i].value === '') { inputs[i].style.borderColor = '#f00'; inputs[i].style.borderStyle = 'solid'; inputs[i].style.borderWidth = '1px'; } } |
這樣能起作用,然而也意味著如果之後需要對樣式做一些改變,你得通過修改這裡的 JavaScript 程式碼實現。更改越複雜,編輯這裡的程式碼就變得越困難。另外,不是每一名 JavaScript 開發者都能夠熟練使用 CSS 或者對其感興趣,這意味著在達成結果之前,你得在 JavaScript 中反覆做大量地修改。當這裡有一個錯誤時,給該 element 增加一個名為 “error” 的 class,通過這樣的行為你可以確保該樣式資訊處於更有效的 CSS 中:
1 2 3 4 5 6 7 |
var f = document.getElementById('mainform'); var inputs = f.getElementsByTagName('input'); for(var i = 0, j = inputs.length; i < j; i++) { if (inputs[i].className === 'mandatory' && inputs[i].value === '') { inputs[i].className += ' error'; } } |
如果你打算使用 CSS 修飾 document 層疊,將會增加大量效率。再舉一個例子,如果你想要隱藏 document 中有同一個 class 的所有 DIV。你可以遍歷所有 DIV,檢查它們的 class,然後改變它們的樣式集合。在較新的瀏覽器中,你也可以使用一個 CSS 選擇器引擎修改同樣的樣式集合。但是最簡單地方法是,使用 JavaScript 在 parent element 上設定一個 class,然後在 CSS 中使用 div.selectorclass{}
的語法遍歷每行 element.triggerclass
。把隱藏 DIV 的任務交給 CSS 設計師吧,他知道怎麼做最好。
當有實用意義的時候,可以考慮使用簡化符號
簡化符號是一把雙刃劍:一方面它可以使你的程式碼保持簡短,另一方面對於其他接手你的程式碼的開發者而言,會使閱讀變得困難,他們或許無法搞清楚這些簡化符號幹了什麼。不過,下面我會列出幾種可以使用(並且應該使用)簡化符號的情況。
物件或許是你在 JavaScript 中最常用的事物。傳統方法書寫物件一般是這樣做:
1 2 3 4 5 6 7 8 |
var cow = new Object(); cow.colour = 'brown'; cow.commonQuestion = 'What now?'; cow.moo = function() { console.log('moo'); } cow.feet = 4; cow.accordingToLarson = 'will take over the world'; |
但這意味著你得為每個屬性或函式重寫一遍物件名,這可真是令人厭煩啊。我們可以取而代之,使用下面這種更有意義的結構,也被稱為 object literal:
1 2 3 4 5 6 7 8 9 |
var cow = { colour:'brown', commonQuestion:'What now?', moo:function() { console.log('moo'); }, feet:4, accordingToLarson:'will take over the world' }; |
在 JavaScript 中陣列是一個容易令人困惑的知識點。你會發現大量的指令碼中都這樣定義一個陣列:
1 2 3 4 5 6 |
var aweSomeBands = new Array(); aweSomeBands[0] = 'Bad Religion'; aweSomeBands[1] = 'Dropkick Murphys'; aweSomeBands[2] = 'Flogging Molly'; aweSomeBands[3] = 'Red Hot Chili Peppers'; aweSomeBands[4] = 'Pornophonique'; |
這裡進行了大量無意義的重複操作;其實可以使用 []
陣列縮寫符更加快速地建立一個陣列:
1 2 3 4 5 6 7 |
var aweSomeBands = [ 'Bad Religion', 'Dropkick Murphys', 'Flogging Molly', 'Red Hot Chili Peppers', 'Pornophonique' ]; |
你可以在一些教程裡面發現這個術語 “associative array”(關聯陣列)。這其實是一個誤稱,帶有內容宣告而不是一個 index 的陣列實際上就是物件,同樣應該被定義。
條件選擇句同樣可以使用 “三元選擇符” 縮短。例如下面的結構定義了一個值為 1 或者 -1 的變數,取決於另一個變數的值:
1 2 3 4 5 6 |
var direction; if (x > 100) { direction = 1; } else { direction = -1; } |
我們可以將其縮短為單獨一行:
1 |
var direction = (x > 100) ? 1 : -1; |
問號前面的內容都是判斷條件,緊接其後的值是為 true
的情況,再後面的值是為 false
的情況。三元選擇符可以巢狀,但為了保持可讀性我都避免那樣使用。
另一個 JavaScript 中常見的情形是當變數未定義時,為其提供一個預設值,就像這樣:
1 2 3 4 5 |
if (v) { var x = v; } else { var x = 10; } |
此處的簡短符號是 ||
:
1 |
var x = v || 10; |
這樣就可以自動判斷當 v 未定義時,為 x 賦值 10,就這麼簡單。
模組化 —— 每個任務對應一個函式
這是一個通用的程式設計最佳實踐 —— 請確定你建立的函式一次只完成一個工作,這樣其他開發者可以簡單地除錯與修改你的程式碼,而不需瀏覽所有程式碼才能弄清每一個程式碼塊執行了什麼功能。
同樣也能應用於建立通用任務的輔助函式。如果你發現自己在不同的函式中做著相同的事情,那麼最好建立一個更加通用的輔助函式,需要時重用其功能。
並且,一進一出比在函式內部修改程式碼更有意義。比如說你想編寫一個建立新連結的輔助函式。可以這樣做:
1 2 3 4 5 6 |
function addLink(text, url, parentElement) { var newLink = document.createElement('a'); newLink.setAttribute('href', url); newLink.appendChild(document.createTextNode(text)); parentElement.appendChild(newLink); } |
這能夠好好工作,但你或許會發現自己又不得不增加不同的 attribute,取決於你要給哪種 element 增加它適用的連結。舉個例子:
1 2 3 4 5 6 7 8 9 10 11 12 |
function addLink(text,url,parentElement) { var newLink = document.createElement('a'); newLink.setAttribute('href',url); newLink.appendChild(document.createTextNode(text)); if (parentElement.id === 'menu') { newLink.className = 'menu-item'; } if (url.indexOf('mailto:') !== -1) { newLink.className = 'mail'; } parentElement.appendChild(newLink); } |
這會使該函式更加特殊,難以適用於不同情形。一個更加清晰地方式是返回這個連結,當需要的時候,在主函式中覆蓋這些額外的情況。在這裡將 addLink()
改為更加通用的 createLink()
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
function createLink(text,url) { var newLink = document.createElement('a'); newLink.setAttribute('href', url); newLink.appendChild(document.createTextNode(text)); return newLink; } function createMenu() { var menu = document.getElementById('menu'); var items = [ { t:'Home', u:'index.html' }, { t:'Sales', u:'sales.html' }, { t:'Contact', u:'contact.html' } ]; for(var i = 0; i < items.length; i++) { var item = createLink(items.t, items.u); item.className = 'menu-item'; menu.appendChild(item); } } |
通過使自己的所有函式只執行一種任務,你可以為應用建立一個主要的 init()
函式,包含所有應用構造。這種方式能夠幫助你簡單地修改應用、移除功能,而不需瀏覽 document 中殘存的依賴。
漸進增強
Progressive Enhancement(漸進增強)是一項主要在 Graceful Degredation 上討論相關細節的開發實踐模型。大體上你需要做的就是編寫出在任何可用的技術中都能執行的程式碼。對於 JavaScript,意味著當指令碼不可用時(比如在 BlackBerry 上,或者因為一名過分熱情的安全警察),你的 web 產品仍需允許使用者到達他們的主要目標,不能因為他們無法開啟而缺失的 JavaScript 功能就阻止他們訪問,或者不想讓他們訪問。
令人驚訝地是當你面對一個問題時,將會頻繁選擇建立大量複雜的 JavaScript 指令碼去解決它,但其實這個問題可以使用更加簡單的解決方式。我遇見過的一個案例是在頁面上建立一個允許使用者搜尋不同資料的搜尋框,可以搜尋 web、圖片、新聞等等。
最初的版本中,不同的資料選項都是連結,會重寫表單 action 的 attribute 指向後端不同指令碼來表現相應搜尋。
問題在於如果 JavaScript 被禁用,這些連結仍然會顯示出來,但任何搜尋都只會返回標準值,因為表單的 action 沒被改變。解決方法很簡單:除連結以外我們提供了一個單選按鈕組的選項,也使用一個後端指令碼分給其相應的搜尋指令碼。
這樣不僅使每個人都能得到正確的搜尋結果,也方便統計每個功能選項有多少使用者選擇。通過使用我們管理的正確 HTML 結構,成功避免利用 JavaScript 同時完成切換表單 action 與點選跟蹤指令碼的功能,每一位使用者都能使用,無需在意環境問題。
允許配置與轉換
如何讓程式碼保持可維護性和整潔?最成功的要點之一就是建立一個 configuration object(可配置物件),包含所有可能隨時間改變的事物。包括你建立的 element 裡面使用到的所有文字(按鍵值與圖片的選擇文字)、CSS class 以及 ID 名、你所建立的介面的通用引數。
例如 Easy YouTube player(一個 YouTube 視訊下載外掛)有著下列配置物件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
// This is the configuration of the player. Most likely you will // never have to change anything here, but it is good to be able // to, isn’t it? config = { CSS:{ // IDs used in the document. The script will get access to // the different elements of the player with these IDs, so // if you change them in the HTML below, make sure to also // change the name here! IDs:{ container:'eytp-maincontainer', canvas:'eytp-playercanvas', player:'eytp-player', controls:'eytp-controls', volumeField:'eytp-volume', volumeBar:'eytp-volumebar', playerForm:'eytp-playerform', urlField:'eytp-url', sizeControl:'eytp-sizecontrol', searchField:'eytp-searchfield', searchForm:'eytp-search', searchOutput:'eytp-searchoutput' // Notice there should never be a comma after the last // entry in the list as otherwise MSIE will throw a fit! }, // These are the names of the CSS classes, the player adds // dynamically to the volume bar in certain // situations. classes:{ maxvolume:'maxed', disabled:'disabled' // Notice there should never be a comma after the last // entry in the list as otherwise MSIE will throw a fit! } }, // That is the end of the CSS definitions, from here on // you can change settings of the player itself. application:{ // The YouTube API base URL. This changed during development of this, // so I thought it useful to make it a parameter. youtubeAPI:'http://gdata.youtube.com/apiplayer/cl.swf', // The YouTube Developer key, // please replace this with your own when you host the player!!!!! devkey:'AI39si7d…Y9fu_cQ', // The volume increase/decrease in percent and the volume message // shown in a hidden form field (for screen readers). The $x in the // message will be replaced with the real value. volumeChange:10, volumeMessage:'volume $x percent', // Amount of search results and the error message should there // be no reults. searchResults:6, loadingMessage:'Searching, please wait', noVideosFoundMessage:'No videos found : (', // Amount of seconds to repeat when the user hits the rewind // button. secondsToRepeat:10, // Movie dimensions. movieWidth:400, movieHeight:300 // Notice there should never be a comma after the last // entry in the list as otherwise MSIE will throw a fit! } } |
如果你將這個作為模組化的一部分,甚至可以使其公有化,以允許操作者在初始化你的模組之前僅需重寫他們需要的部分。
保持程式碼簡單地可維護性是十分重要的一件事,避免未來有相關需求的維護者不得不閱讀全部程式碼,尋找他們應該修改的地方。如果這不夠顯眼,你就是採取了被廢棄的或者說非常醜陋的解決方式。一旦需要升級時,不雅的解決方式無法接受補丁,並且完全失去了重用程式碼的機會。
避免長巢狀
巢狀程式碼解釋了邏輯結構並使其更加易讀,但太長的巢狀會讓人難以搞清楚你在嘗試什麼。程式碼閱讀者不應被強迫水平滾動,或在遇見喜歡包裹長串程式碼的編輯者時遭受困惑(這會使得你嘗試縮排的努力毫無意義)。
另一個關於巢狀的問題是變數名和迴圈。一般你會使用 i 作為開始第一個迴圈的迭代變數,接下來,你會繼續使用 j、k、l 等等。這樣很快就會變得凌亂:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
function renderProfiles(o) { var out = document.getElementById('profiles'); for(var i = 0; i < o.members.length; i++) { var ul = document.createElement('ul'); var li = document.createElement('li'); li.appendChild(document.createTextNode(o.members[i].name)); var nestedul = document.createElement('ul'); for(var j = 0; j < o.members[i].data.length; j++) { var datali = document.createElement('li'); datali.appendChild( document.createTextNode( o.members[i].data[j].label + ' ' + o.members[i].data[j].value ) ); nestedul.appendChild(datali); } li.appendChild(nestedul); } out.appendChild(ul); } |
就像我正在做的,我使用了常見的 —— 應被拋棄的 —— 變數名 ul 與 li,為了巢狀列表我需要 nestedul 與 datali。如果列表要繼續巢狀下去,我就需要更多的變數名,一直持續。更有意義的做法是將為每個成員建立巢狀列表的任務放進各自函式中,並通過恰當的資料呼叫。這樣也能幫助我們防止一個套一個的迴圈。addMemberData()
函式非常通用,極有可能在其他時間派上用場。經過這些考慮後,我就可以像下面這樣重寫程式碼:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
function renderProfiles(o) { var out = document.getElementById('profiles'); for(var i = 0; i < o.members.length; i++) { var ul = document.createElement('ul'); var li = document.createElement('li'); li.appendChild(document.createTextNode(data.members[i].name)); li.appendChild(addMemberData(o.members[i])); } out.appendChild(ul); } function addMemberData(member) { var ul = document.createElement('ul'); for(var i = 0; i < member.data.length; i++) { var li = document.createElement('li'); li.appendChild( document.createTextNode( member.data[i].label + ' ' + member.data[i].value ) ); } ul.appendChild(li); return ul; } |
優化迴圈
如果你不能正確使用迴圈,它們就會變得非常緩慢。最常見的錯誤之一是在每次迭代判斷中讀取陣列的長度屬性:
1 2 3 4 |
var names = ['George', 'Ringo', 'Paul', 'John']; for(var i = 0; i < names.length; i++) { doSomeThingWith(names[i]); } |
這意味著迴圈每次執行,JavaScript 便會讀取一次該陣列的長度。你可以通過將長度值儲存在其他變數中來避免這個問題:
1 2 3 4 5 |
var names = ['George', 'Ringo', 'Paul', 'John']; var all = names.length; for(var i = 0; i < all; i++) { doSomeThingWith(names[i]); } |
更簡短的優化方法是在迴圈判斷塊中建立一個第二變數:
1 2 3 4 |
var names = ['George', 'Ringo', 'Paul', 'John']; for(var i = 0, j = names.length; i < j; i++) { doSomeThingWith(names[i]); } |
另一件需要確定的事情是你已將大計算量的程式碼放在迴圈外部,包括正規表示式與 —— 更重要的 —— DOM 處理。你可以在迴圈中建立 DOM 節點,但不要將它們插入 document。你會在下一節的 DOM 最佳實踐中學到更多。
最小化 DOM 訪問
在瀏覽器中訪問 DOM 是一件昂貴的事情。DOM 是一個非常複雜的 API,在瀏覽器中渲染會花費大量時間。執行復雜的 web 應用時,你可以發現你的電腦已被其他工作佔滿了 —— 修改需要花費更長時間或者只能顯示一半等等。
為了確保你的程式碼足夠快速,不會拖累瀏覽器停止,則應儘量最小化訪問 DOM。不要不斷地建立和使用 element,而需建立一個工具函式將 string 變為 DOM 元素,然後在生成過程最後呼叫這個函式影響一次瀏覽器渲染,而不是不斷地干擾。
不要屈服於瀏覽器的獨有特性
以某個瀏覽器為中心編寫程式碼是一種會讓程式碼難以維護並很快過時的方式。如果你瀏覽網頁,將發現大量指令碼特定了某個瀏覽器,其他瀏覽器更新版本後就會停止執行。
這是費時費力的行為 —— 就像本篇教程展現的一樣,我們應該基於標準建立程式碼,而不是針對某一個瀏覽器。web 服務於每個人,不是一群使用最先進配置的精英使用者。當瀏覽器市場快速更新時,你卻需要回溯自己的程式碼並保持修復。這既沒有效率也不有趣。
如果僅有一個瀏覽器擁有一些令人驚訝的工作特性,並且你的確需要使用,將程式碼放在專屬於它的指令碼文件中,然後以瀏覽器和版本號命名。這意味著當該瀏覽器被廢棄時,你可以更輕易地發現和移除這個功能。
不要相信任何資料
談論程式碼與資料安全時,要記住的要點之一便是不要相信任何資料。不僅僅因為邪惡的傢伙想要竊取你的系統;它起始於簡單的可用性。使用者總會輸入錯誤的資料。不是因為他們很愚蠢,而是因為太忙了,總被其他事物分心或你指示的詞語令他們困惑。比如說我預定了一個月而不是六天的旅館房間,只因為輸入了一個錯誤的數字。。。我認為自己還是足夠聰明的。
簡而言之,確保進入系統的所有資料都是清晰且確實需要的。在後臺編寫從 URL 中檢索到的引數時,這是非常重要的。在 JavaScript 中,測試傳遞給函式的引數型別十分重要(使用關鍵詞 typeof
)。當 members
不是一個陣列時,下面的程式碼將會出現錯誤(例如對於一個 string,它將會為 string 的每個字元建立一個列表):
1 2 3 4 5 6 7 8 9 10 |
function buildMemberList(members) { var all = members.length; var ul = document.createElement('ul'); for(var i = 0; i < all; i++) { var li = document.createElement('li'); li.appendChild(document.createTextNode(members[i].name)); ul.appendChild(li); } return ul; } |
為了讓程式碼正常工作,你需要檢查 members
的型別,確保是一個陣列:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function buildMemberList(members) { if (typeof members === 'object' && typeof members.slice === 'function') { var all = members.length; var ul = document.createElement('ul'); for(var i = 0; i < all; i++) { var li = document.createElement('li'); li.appendChild(document.createTextNode(members[i].name)); ul.appendChild(li); } return ul; } } |
陣列會設下陷阱,告訴你自己是物件。為了確定它們是陣列,可以檢驗一個只有陣列才擁有的方法。
另一個不安全的實踐是從 DOM 閱讀資訊,不做校驗就使用。比如說,我曾經不得不除錯一些導致 JavaScript 功能中止的程式碼。這些程式碼用於 —— 因為我自身的一些原因 —— 在 innerHTML
之外從一個頁面元素中讀取一個使用者名稱,然後作為引數被一個函式呼叫。使用者名稱可能是任意包括單引號和引號的 UTF-8 字元。這樣將結束任何字串,剩下的部分就會成為錯誤資料。並且如果有任意使用者使用像 Firebug 或者 Opera DragonFly 這樣的工具改變 HTML,就可以將使用者名稱改成任何東西,並注入你的函式。
同樣適用於只在客戶端驗證的表單。我曾經通過重寫一個選擇以提供另一個選項,註冊了一個不可用的 email 地址。因為表單沒在後臺驗證,使得該程式毫無阻礙地執行。
對於 DOM 訪問,檢驗自己嘗試訪問的元素再修改十分有必要,也是你所期待的 —— 否則程式碼將會執行失敗或造成奇怪的渲染 bug。
用 JavaScript 增加功能,不要建立太多內容
就像你在其他示例中看見的那樣,在 JavaScript 中建立大量 HTML 會變得十分緩慢與古怪。特別在 Internet Explorer 上,當它一直通過 innerHTML
載入和操控內容時,如果你修改文件就會遇見各種各樣的問題(在 Google 搜尋 “操作中止錯誤” 看看一段悲哀和痛苦的故事)。
在頁面維護方面,建立大量 HTML 標記也是一個糟糕的主意,因為不是每一位維護者都擁有和你一樣水平的開發技巧,他們很可能對你的程式碼感到困惑。
我發現當我不得不使用一個大量依賴 JavaScript 的 HTML 模板建立應用時,通過 Ajax 載入這個模板更有用。那樣維護者不需要涉及到你的 JavaScript 程式碼,便可以修改 HTML 結構和重要文字。唯一的障礙就是告訴他們,你需要哪些 ID 以及是否有必要遵循已定義順序的中心 HTML 結構。你可以用內聯 HTML 註釋做到這些(然後當你載入好模板後取走這些註釋)。示例可以檢視 Easy YouTube template 的原始碼。
在這個指令碼中,當正確的 HTML 容器可用時,載入模板,在後面的 setupPlayer()
方法中應用事件處理程式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
var playercontainer = document.getElementById('easyyoutubeplayer'); if (playercontainer) { ajax('template.html'); }; function ajax(url) { var request; try { request = new XMLHttpRequest(); } catch(error) { try { request = new ActiveXObject('Microsoft.XMLHTTP'); } catch(error) { return true; } } request.open('get', url, true); request.onreadystatechange = function() { if (request.readyState == 4) { if (request.status) { if (request.status === 200 || request.status === 304) { if (url === 'template.html') { setupPlayer(request.responseText); } } } else { alert('Error: Could not find template…'); } } }; request.setRequestHeader('If-Modified-Since','Wed, 05 Apr 2006 00:00:00 GMT'); request.send(null); }; |
通過此方法,我允許人們以任何需要的方式轉換和改變這個外掛,而無需修改 JavaScript 程式碼。
站在巨人的肩上構建
無可否認,近幾年 JavaScript 庫和框架已經統治了 web 開發市場。這不是壞事 —— 如果它們都能正確使用的話。所有優秀的 JavaScript 庫都只做了一件事:簡化你的開發生活,不再奔波於瀏覽器間的不一致,不再不斷修復瀏覽器支援漏洞。JavaScript 庫為你提供了一種可測的、基於函式的構建選擇。
不通過庫初學 JavaScript 是很好的主意,因為你可以切實地知道發生了什麼,但當真正開始開發網站時,你需要使用一個 JS 庫。你會處理更少的問題,並且出現的 bug 至少都是可以復現的,而不是隨機出現的瀏覽器問題。
我的個人愛好是 Yahoo User Interface Library(YUI),基於 JQuery、Dojo 和 Prototype,但還有一堆優秀的庫,你需要從中找到最適合自己和產品的那個庫。
有時所有的庫都很適合,在相同的專案中使用幾個庫可不是一個好主意。這會提升不必要的複雜性和維護難度。
開發環境程式碼並不等於生產環境程式碼
最後一點我要談論的不是 JavaScript 本身,而是如何使它更好地適應你的開發策略。因為JavaScript 的任何修改都會迅速影響你的網站的功能和效能,儘可能優化你的程式碼是一件很吸引人的事,甚至可以不顧及對於維護性的影響。
這裡有許多聰明的技巧,你可以應用到 JavaScript 中讓其表現得更棒。另一方面它們中的大部分都伴隨著使程式碼更難以理解和維護的風險。
為了寫出健全的、工作穩定的 JavaScript 指令碼,我們需要跳出這種迴圈,停止為機器而不是為其他開發者優化程式碼。大多數時候,有些在其他語言中是常識的事卻不為大部分 JavaScript 開發者所知。一個構建指令碼可以移除縮排、註釋,用陣列查詢替代字串(避免 MSIE 為每個字串的單獨例項建立一個字串物件 —— 甚至在條件中),並做其他所有需要的細節工作,以讓我們的 JavaScript 在瀏覽器中飛翔。
如果我們更多關注於使原始程式碼易於理解,方便其他開發者擴充套件,我們就可以建立出完美的構建指令碼。如果我們優化過度,則永遠得不到這個結果。不要為你自己或瀏覽器構建程式碼 —— 為下一位從你這裡接手的開發者構建程式碼。
總結
JavaScript 的主要訣竅在於避免採用簡單的途徑。JavaScript 是一種非常通用的語言,並且因為其執行的環境擁有很高的寬容度,十分容易寫出看似完成工作的草率程式碼。然而同樣的程式碼將會在幾個月後回來徹底刺傷你。
如果你想擁有一份 web 開發者的工作,JavaScript 開發會成為你知識領域中十分必要的一環。如果你想從現在開始,那麼你是幸運的,我自己和其他許多人已經犯了大量錯誤,完成了所有試驗和自我改正;現在我們可以沿著這些知識前行了。