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

前言

這篇文章本應該在上一篇文章:使用更嚴格的JavaScript編碼方式,提升代碼質量以前發佈,但當時以爲這篇文章太過基礎,也就做罷。後來諮詢了一些初級的開發者,他們以爲有必要把這篇文章也放上來。儘管這篇文章內容基礎,可是不少初中級開發者仍是會犯一樣的錯誤,發佈出來也算是再一次提醒。javascript

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

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

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

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

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

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

有不少的手段能夠解決由於定義了全局變量而致使代碼污染的狀況。最簡單的方法是把變量和方法封裝在一個變量對象上,使其變成對象的屬性。例如:node

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來判斷瀏覽器的特性支持。

相關文章
相關標籤/搜索