Web前端開發最佳實踐(10):JavaScript代碼不好讀,不好維護?你需要改變寫代碼的習慣


前言

這篇文章本應該在上一篇文章:使用更嚴格的JavaScript編碼方式,提高代碼質量之前發布,但當時覺得這篇文章太過基礎,也就作罷。后來咨詢了一些初級的開發者,他們覺得有必要把這篇文章也放上來。盡管這篇文章內容基礎,但是很多初中級開發者還是會犯同樣的錯誤,發布出來也算是再一次提醒。

良好的編碼習慣,這是每個程序員應具備的最基本素質。無論是前端程序員還是后端程序員,都要遵循基本的規范,減少因代碼混亂而造成難以維護的局面。要做到不管有多少人共同參與同一個項目,一定要確保每一行代碼都像是同一個人編寫的。

提高代碼的可讀性和可維護性,有一些共同的方法,比如注意代碼格式整齊,縮進合理,規范的命名等等。但有一些方式還是和所使用語言本書的特性有關。JavaScript是一種弱類型語言,有着相對松散的限制,這種特點使得開發者可以更靈活更高效地編寫JavaScript代碼。但同時也存在着一些設計上的缺陷,使得開發者很容易編寫帶有潛在問題的代碼。JavaScript引擎在運行這些有潛在問題的代碼時可能並不會報錯或者警告,所以發現這些問題就變的很困難。這些形式各異、隱含有邏輯錯誤的代碼,影響着代碼整體的可讀性和可維護性。因而,需要使用更嚴格的編碼規范,避免出現這些不合規范的代碼帶來錯誤。如下是提高JavaScript代碼可維護性的一些最佳實踐方法

1. 避免定義全局變量或函數

定義全局的變量和函數,會影響代碼的可維護性。如果在頁面中運行的JavaScript代碼是在相同的作用域里面,那這就意味着代碼之間的定義存在互相影響的可能。如果在其中一段代碼中定義了全局的變量或函數,則這些全局的變量或函數在另一段代碼中將會是透明的,意味着在另一段代碼中可以操作或者覆蓋這些變量或函數。但很多時候,這樣的情形並不是設計需要,而是誤操作。例如,在項目中的一位開發者定義了如下的全局變量和函數:

var length = 0;
function init(){…}
function action() {…}

如果另外一個開發者在不知道已定義這些變量和函數的情況下,也定義了相同名稱的變量或函數,則后定義的函數或者方法會覆蓋之前的定義。在代碼中出現這樣的情形是非常嚴重的,導致了變量值被重置或者函數邏輯改變,從而發生不可預知的錯誤。

有很多的手段可以解決因為定義了全局變量而導致代碼污染的情況。最簡單的方法是把變量和方法封裝在一個變量對象上,使其變成對象的屬性。例如:

var myCurrentAction = {
     length: 0,
     init: function(){…},
     action: function(){…}
}

這樣基本上可以避免全局變量或方法被覆蓋的情況。但這種方案也有弊端,所有變量和函數的訪問都需要通過主對象來實現了,比如訪問如上的length變量,就需要通過myCurrentAction.length來訪問。這就增加了代碼的重復度和代碼編寫的繁瑣性。另一種改進的方案是把全局的變量包含在一個局部作用域中,然后在這個作用域中完成這些變量的定義以及變量使用的邏輯。比如,可以通過定義一個匿名函數實現:

(function () {
    var length = 0;
    function init(){…}
    function action() {…} 
})();

所有的邏輯都包含在了這個立即執行的匿名函數中,形成了一個獨立的模塊,最大限度地防止了代碼之間的污染。當然,在實際的業務中,模塊之間會有交互,這時則可以使用return語句,返回需要公開的接口,比如要公開上述代碼中的init函數,則如上代碼應修改為如下形式:

var myCurrentAction = (function () {
var length = 0;
function init(){…}
function action() {…} 
return {
    init: init
}
})(); 

經過如此調整,外部代碼訪問init方法時,就可以調用myCurrentAction.init了。此方案既巧妙地做到了代碼邏輯的封裝,又公開了外部需要訪問的接口,是代碼模塊化的最佳實踐方式之一。

另外一個避免定義全局變量的方式是:確保在定義變量時使用var關鍵字。如果定義變量時沒有使用var,瀏覽器解析時並不會報錯,而是自動把這一變量解析為全局變量,比如如下的代碼就定義了一個全局的變量length:

(function () {
    length = 0;
    function init(){…}
    function action() {…} 
})(); 

這種可以不通過var關鍵字而定義變量的方式也是JavaScript代碼靈活的一種體現,但同時也是代碼中潛在問題的根源之一。很多時候,開發者並非想通過這種方式定義一個全局變量,只是錯誤地遺漏了var關鍵字。規范的制定者也意識到了這種靈活性帶來的問題,所以在JavaScript代碼的嚴格模式中,變量定義必須添加var關鍵字,否則會報編譯錯誤。

2. 使用簡化的編碼方式

在JavaScript中,提供了很多種簡化的編碼方式,這些方式保持了代碼的簡潔性,但同時也提高了可讀性。如下示例將使用復雜的方式創建對象和數組,這種方式是在后端語言中慣用的方式:

// 對象創建
var person = new Object();
person.age = 25;
person.name = 'dang';

// 數組創建
var list = new Array();
list[0] = 12;
list[1] = 20;
list[2] = 24;

在JavaScript中,可以使用JSON方式創建對象和數組。如果開發者熟悉JavaScript,則使用這種方式更簡潔易讀,代碼如下:

// 對象創建
person = {age: 25, name: 'dang'};

// 數組創建
list = [12, 20, 24];

3. 使用比較運算符=而不是

JavaScript有兩組相等的運算符:=(嚴格相等)和!(嚴格不等)及(相等)和!=(不等)。=和!會比較兩個基礎類型值是否相等,或者兩個復雜對象是否指向同一個地址,而和!=則會先進行比較值的類型轉換,在把兩個比較值的類型轉換為相同類型后才會進行比較運算,所以只有在兩個比較值的類型一致時,它才與第一組相等運算符等同。==和!=在比較時的類型轉換規則也很復雜,具體如下:

undefinednull與自己比較時結果為true;它們相互比較時結果也為true;但與其它類型比較時,結果為false;原始類型(數值、布爾和字符類型)進行比較時,會先轉換為數值類型再比較;對象和原始類型比較時,會先將對象轉換為原始類型,然后再比較。來看看相應的示例:

null == undefined; // true
0 == null; // false

false == '0' // true
false == 'false' // false
'\n  123  \t' == 123 // true

var p = {toString: function(){ return '1'}}
p == 1; // true

要記住如上的這些規則很難,而使用=和!這兩個嚴格相等運算符進行比較時並不存在類型轉換的過程,因此會返回正確的結果。為了避免出現隱含的錯誤,推薦使用=和!運算符,不要使用==和!=運算符。

4. 避免使用with語句

在JavaScript中,with語句可用來快捷地訪問對象的屬性。with語句的格式如下:

with (object) {
    statement
} 

with語句的使用原理是:JavaScript解析和運行時,會給with語句單獨建立了一個作用域,而和with語句結合的對象上的屬性則成為了此作用域的局部變量,因此可以直接訪問。比如:

with (Math) {
    a = PI * r * r;
    x = r * cos(PI);
    y = r * sin(PI / 2);
} 

上面代碼和如下的代碼會完成同樣的事情:

a = Math.PI * r * r;
x = r * Math.cos(PI);
y = r * Math.sin(PI / 2);

從代碼量上看,使用with語句的確簡化了代碼,但不幸的是,使用with語句可能也會帶來不可思議的bug以及兼容問題:
首先,使用with語句,使得代碼難以閱讀,對於with語句內部的變量引用,只有在運行時才能知道變量屬於哪個對象。比如:

function f(x, o) {
	with (o) {
	    print(x);
	}
}

來看一下with語句中的x變量,但從代碼分析,x可能是參數上傳入的x,也可能是o對象上的屬性o.x,這取決於實際運行時的上下文。 當從代碼無法確認實際邏輯時,這段代碼就可能會有潛在的bug。如上的代碼中,可能開發者在代碼中使用x的期望是從參數傳入x,但如果實際運行時o對象上有x屬性,則with語句內部的x會成為o對象上的x屬性,這就和開發者預期不同了。

其次,with語句存在兼容問題,如下的示例來自mozilla開發網站:

function f(foo, values) {
    with (foo) {
        console.log(values)
    }
}

如果在ECMAScript 5環境中調用f([1,2,3], obj),則with語句中的values引用的是obj對象。如果在ECMAScript 6環境中調用 f([1,2,3], obj),由於Array.prototype引入了values屬性,因此with語句中的values引用的是[1,2,3].values。
此外, with語句的設計方面也有缺陷,在with語句內部修改和with語句結合的對象后,並不能同步到with內部,即不能保證對象數據的一致性。舉個例子:

var group = {
    value: {
        node: 1
    }
};
with(group.value) {
    group.value = {
        node: 2
    };
    // 顯示錯誤: 1
    console.log(node);
}
// 顯示正確: 2
console.log(group.value.node);

如上的例子中,在with內部修改了group.value對象,設置了group.value.node值為2,但在with語句內部的node值並沒用同步修改為2。

基於以上的分析,在使用with語句的過程中,開發者通過閱讀代碼不能知道它將會做什么,即無法確定代碼是否會正確地做期望的事情,並且with語句也存在設計上的缺陷,所以應該在代碼中避免使用with語句。

5. 避免使用eval

在JavaScript中,eval函數的用法很簡單,它會接受一個字符串參數,把字符串內容作為代碼執行,並返回執行結果。典型的用法如下:

eval("x=1;y=2; x*y") 

但這個函數存在被濫用的情況。很多新手因為不了解JavaScript語法,所以會在某些不恰當的場合使用eval函數。比如想得到對象上的屬性值,但由於屬性名是通過變量傳入的,所以無法用點操作符,這個時候就可能會想要使用eval,代碼類似如下形式:

eval('obj.' + key);

其實可以使用下標法取得屬性值:

obj[key]

從eval的功能上看,使用eval函數會讓代碼難以閱讀,影響代碼的可維護性。除此之外,eval的使用也存在安全性問題,因為它會執行任意傳入的代碼,而傳入的代碼有可能是未知的或者來自不受控制的源,所以盡量避免使用eval。其實在大多數的情況下,都是可以使用其它方案來代替eval的功能。上例便是其中一個典型的例子,使用下標法代替使用eval函數取得了對象的屬性。

和eval函數類似的還有setTimeout和setInterval函數,這兩個函數也可以接受字符串參數,當傳入的參數為字符串時,它們會做類似eval函數的處理,把字符串當作代碼執行。所以使用這兩個函數時,應該避免使用字符串類型參數。此外,Function構造器也和eval函數的功能類似,所以也應該避免使用。

6. 不要編寫檢測瀏覽器的代碼

經常在一些老舊的JavaScript代碼中存在瀏覽器判斷的邏輯,即根據瀏覽器的不同做不同的處理。這種判斷瀏覽器的做法在五六年以前還算是有一定合理性的,因為當時瀏覽器的發展很緩慢,瀏覽器的功能變化不大。但隨着瀏覽器更新的速度越來越快,並且瀏覽器之間的差異也越來越小,原來這些判斷瀏覽器版本的代碼邏輯就不適時宜了,甚至有可能導致邏輯上的錯誤。因為瀏覽器之前不支持的功能有可能在新版本中得到了支持,瀏覽器的bug也可能在新版本中得到了修正。這樣一來,之前那些通過判斷瀏覽器而修正的bug就可能完全沒有作用了,開發者不得不重新修改代碼來適應新的瀏覽器。所以,最佳的做法是不要編寫檢測瀏覽器的代碼,取而代之的是檢測瀏覽器是否支持某個特定功能。開發者可以借助目前流行的Modernizr框架來檢測瀏覽器的特性支持。

當然,也存在某些特定情況需要判斷瀏覽器的版本,尤其是判斷IE瀏覽器。這個時候,最好是把針對特定瀏覽器的代碼邏輯放置在單獨的文件中,方便后期的維護和移除。比如,已經知道IE8及以下版本瀏覽器不支持HTML5的新標簽,所以如果要在頁面上使用HTML5新標簽,則需要針對這些瀏覽器加入兼容代碼。這時,可把兼容代碼放在單獨的文件中,頁面中添加如下代碼:

<!--[if lt IE 9]>
<script src="javascript/html5.js"></script>
<![endif]--> 

后期如果頁面不再支持IE8及以下版本瀏覽器,則只需移除此代碼引用即可。

jQuery 從 1.9 版開始,移除了 $.browser$.browser.version ,取而代之的是$.support。jQuery的做法正是為了讓開發者不再借助$.browser$.browser.version來判斷瀏覽器版本,而是使用$.support來判斷瀏覽器的特性支持。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM