優秀的Stoyan Stefanov在他的新書中(《Javascript Patterns》)介紹了很多編寫高品質代碼的技巧,比如避免使用全域變數,使用單一的var關鍵字,迴圈式預存長度等等。
這篇文章不僅僅從代碼本身來考慮如何最佳化編碼,也從代碼的設計階段來考慮,包括書寫API文檔,同事的review,使用JSLint。這些習慣都能協助你編寫更加高品質的、更易於理解的、可維護的代碼(讓你的代碼在多年之後仍使你引以為傲)。
編寫可維護的代碼
軟體的BUG修複需要花費大量的精力。尤其當代碼已經發布之後,隨著時間的增長,維護的成本愈發的高。當你一發現BUG的時候,就立即去修複,這時候你的代碼還是熱乎的,你也不需要回憶,因為就是剛剛寫好的。但是當你做了其他任務,幾乎完全忘記了這份代碼,這時候就需要:
◆重新學習和理解問題
◆理解代碼是如何解決問題的
另外一個問題是,在大項目或者大公司裡面,經常是解決BUG的人不是產生BUG的人,而且也不是發現BUG的人。所以減少理解代碼的時間就是最重要的問題,無論這個代碼是你自己以前寫的還是團隊中的其他成員寫的,因為我們都想去搞搞新的有意思的東西,而不是去維護那些個陳舊的代碼。
還有一個開發中的普遍問題就是,往往讀代碼的時間比寫代碼的時間還要多。有時候你鑽研一個問題,可以花整整一個下午的時間來考慮代碼的編寫。這個代碼當時是可以工作的,但是隨著開發的進行,其他東西發生了很大的變化,這時候也就需要你自己來重新審查修改編寫代碼。比如:
◆還有BUG沒有解決
◆添加了新的功能
◆程式需要在新的環境中運行(比如一個新上市的瀏覽器)
◆代碼有問題
◆代碼需要重寫因為修改了架構甚至要使用另一個語言
因為這些原因,也許你當時一個下午寫好的代碼,後面需要花費幾周的時間來閱讀。所以編寫可維護的代碼對於軟體的成功至關重要。
可維護的程式碼封裝括:
◆可讀性
◆連續性
◆預見性
◆看起來是一個人寫的
◆有文檔
最少化全域變數
Javascript使用函數來約定範圍。一個在函數內部聲明的變數在外部是不可見的。所以,全域變數也就是聲明在任何函數之外的或者沒有被聲明的變數。
Javascript中,在任何函數之外有個可訪問的全域對象,每一個你建立的全域變數都是這個對象的一個屬性。在瀏覽器中,為了方便,通常用window來指代這個全域變數。下面的代碼就是說明如何建立一個全域變數:
- myglobal = "hello"; // antipattern
- console.log(myglobal); // "hello"
- console.log(window.myglobal); // "hello"
- console.log(window["myglobal"]); // "hello"
- console.log(this.myglobal); // "hello
全域變數的問題
全域變數的問題在於,他在你的所有代碼或者一個頁面中都共用。他們在同一個命名空間下面,這通常會造成變數名衝突–兩個同名的變數,但是確實不同的用處。
通常在一些頁面中需要引入一些其他人的代碼,比如:
◆第三方的JS庫
◆廣告夥伴的指令碼
◆第三方的使用者行為分析或者統計指令碼
◆不同的組件、按鈕等等
加入其中一個第三方組件定義了一個全域變數:result。然後在你的程式中,也定義了一個全域變數result。最後的這個result會覆蓋點之前的result,這樣第三方的指令碼就會停止工作。
所以,為了對其他的指令碼友好,在一個頁面中使用越少的全域變數越好。在後面會有一些方法來告訴你如何減少全域變數,比如使用命名空間,或者自執行的匿名函數,但是最好的避免全域變數的方法就是使用var關鍵字來聲明變數。
因為javascript的兩個特性,建立一個全域變數非常的簡單。第一,你可以使用一個甚至沒有聲明的變數,第二,在javascript中,所有未聲明的變數都會成為全域對象的一個屬性(就像一個聲明了的全域變數一樣)。看看這個例子:
- function sum(x,y){
- result = x + y;
- return result;
- }
在這個代碼中,result在沒有被聲明的情況下就被使用了,這個代碼也能很好的工作,但是在調用了這個函數之後,就會多一個名為result的全域變數,這是所有問題的根源了。
解決這個問題的辦法就是使用var:
- function sum(x,y){
- var result = x + y;
- return result;
- }
兩外一個不好的習慣就是在聲明變數的時候使用鏈式的方法來賦值,這時候,a是局部變數,但是b就成為了全域變數。
- function foo(){
- var a=b=0;
- ....
- }
這是因為,b = 0這個運算式先執行,執行的時候b並沒有被聲明,所以b就成為了全域變數,然後返回這個運算式的值0,給聲明了的變數a,換句話說,就好像你輸入的是:
- var a = (b=0);
如果你已經聲明變數,那麼這種鏈式的賦值沒有問題:
- function foo(){
- var a,b;
- ...
- }
另外一個避免使用全域變數的原因是考慮到程式的可移植性。如果你想讓你的代碼在不同的環境中都可以工作,那麼使用全域變數就很可能會與新的系統中的全域變數衝突(或許在之前的系統中沒有問題)。
忘記var的影響
使用var聲明的全域變數和沒有使用var產生的全域變數還有一個區別在於刪除:
使用var聲明建立的全域變數不能被刪除
沒有使用var聲明的全域變數可以被刪除
這說明沒有使用var聲明產生的全域變數不是真正的變數,他們只是全域對象的屬性。屬性可以通過delete刪除,但是變數不行:
- // define three globals
- var global_var = 1;
- global_novar = 2; // antipattern
- (function () {
- global_fromfunc = 3; // antipattern
- }());
-
- // attempt to delete
- delete global_var; // false
- delete global_novar; // true
- delete global_fromfunc; // true
-
- // test the deletion
- typeof global_var; // "number"
- typeof global_novar; // "undefined"
- typeof global_fromfunc; // "undefined"
在ES5的strict 模式下,給一個為聲明的變數賦值會報錯。
讀取全域對象
在瀏覽器中,你可以通過window變數來讀取全域對象(除非你在函數內部重新定義了window對象)。但在有的環境中,可能不叫window,那麼你可以使用下面的代碼來擷取全域對象:
- var global = (function(){
- return this;
- })();
這樣可以擷取到全域對象的原因是在function的內部,this指向全域對象。但是這在ES5的strict 模式下會不起作用,你需要適配一些其他模式。當你開發自己的庫的時候,你可以把你的代碼封裝在一個立即函數中,然後將this作為一個參數傳進來。
單個var模式
在你的代碼的頂部只是用一個var關鍵字,會有以下的好處:
◆對於所有需要的變數,在一個地方就可以全部看到
◆避免使用一個未定義的變數
◆協助你記憶聲明的變數,減少全域變數
◆更精簡的代碼
書寫很簡單:
- function func() {
- var a = 1,
- b = 2,
- sum = a + b,
- myobject = {},
- i,
- j;
- // function body...
- }
通過一個var和逗號來聲明多個變數。在聲明的時候給變數賦預設值也是不錯的做法,可以避免一些邏輯錯誤,提高代碼的可讀性。而後你閱讀的代碼的時候也可以根據變數的預設值來方便的猜測變數的用途。
你也可以在聲明變數的時候做一些實際的工作,比如sum = a + b;另外,在操作DOM元素的時候,你也可以把DOM元素的引用儲存在一個變數中:
- function updateElement() {
- var el = document.getElementById("result"),
- style = el.style;
- // do something with el and style...
- }
濫用了的var
JavaScript允許你在函數內部有多個var語句,但是卻都表現的如同在函數的頂部聲明一樣。這個特性在你使用一個變數然後在後面又聲明了這個變數時會導致一些奇怪的邏輯問題。對於JavaScript來說,只要變數在同一個範圍,那麼就認為是聲明了的,就算是在var語句之前使用也一樣。看看這個例子:
- myname = "global"; // global variable
- function func() {
- alert(myname); // "undefined"
- var myname = "local";
- alert(myname); // "local"
- }
- func();
在這個例子中,或許你期望第一次會彈出global,第二次彈出local。因為第一次的時候沒有還沒有使用var聲明myname,這是應該是全域變數的myname,第二次聲明了,然後alert之後應該是local的值。而事實上不是這樣的,只要你在函數中出現了var myname,那麼js就認為你在這個函數中聲明了這個變數,但是在讀取這個變數的值的時候,因為var語句還沒有執行,所以是undefined,很奇怪的邏輯吧。上面的代碼相當於:
- myname = "global"; // global variable
- function func() {
- var myname; // same as -> var myname = undefined;
- alert(myname); // "undefined"
- myname = "local";
- alert(myname); // "local"
- }
- func();
我們來解釋一下這個現象,在代碼的解析中,分兩個步驟,第一步先處理變數函數的聲明,這一步處理整個代碼的上下文。第二步就是代碼的運行時,建立函數運算式以及未定義的變數。實際上,我們只是假設了這個概念,這並不在ECMAScript的規範中,但是這個行為常常就是這樣解釋的。
[1] [2] [3] 下一頁