1. 程式人生 > >《編寫可維護的JavaScript》讀書筆記之程式設計風格-變數、函式和運算子

《編寫可維護的JavaScript》讀書筆記之程式設計風格-變數、函式和運算子

變數、函式和運算子

變數宣告

變數宣告是通過 var 語句來完成的。JavaScript 中允許多次使用 var 語句,此外 var 語句幾乎可以用在 JavaScript 指令碼中的任意地方。

【注意】:不論 var 語句是否真正會被執行,所有的 var 語句都提前到包含這段邏輯的函式的頂部執行。

function doSomething() {
    
    var result = 10 + value;
    var value = 10;
    return result;
}

// 等價於
function doSomething() {
    
    var result;
    var value;
    
    result = 10 + value;
    value = 10;
    return result;
}

【說明】:在函式內部任意地方定義變數和在函式頂部定義變數是完全一樣的。因此,一種流行的風格是將所有變數宣告放在函式頂部而不是散落在各個角落。簡言之,依照這種風格寫出的程式碼邏輯和 JavaScript 引擎解析這段程式碼的習慣是非常相似的。

【建議】:總是將區域性變數的定義作為函式內第一條語句。Crockford 程式設計規範、SproutCore 程式設計風格指南和 Dojo 程式設計風格指南也推薦這樣做。

【其他】:

  • Crockford 程式設計規範推薦在函式頂部使用單 var 語句。
  • Dojo 程式設計風格指南規定,只有當變數之間有關聯性時,才允許使用單 var 語句。

【推薦】:
將所有的 var 與合併為一個語句(單 var 語句),每個變數的初始化獨佔一行。對於那些沒有初始值的變數來說,它們應當出現在 var 語句的尾部。

function doSomethingWithItems(items) {
    
    var value = 10,
        result = value + 10,
        i,
        len;
    
    // ...
}

【優點】:保持成本最低,推薦合併 var 語句,可以讓程式碼更短、下載更快。

函式宣告

和變數宣告一樣,函式宣告會被 JavaScript 引擎提前。因此,在程式碼中函式的呼叫可以出現在函式宣告之前。

// 不好的寫法
doSomething();

function doSomething() {
    alert("Hello world!");
}

// 這段程式碼是可以正常執行的,會被引擎解析為:
function doSomething() {
    alert("Hello world!");
}

doSomething();

【推薦】:總是先宣告 JavaScript 函式然後使用函式。Crockford 程式設計規範包含這種設計,還推薦函式內部的區域性函式應當緊接著變數宣告之後宣告。

function doSomethingWithItems(items) {
    
    var i, len,
        value = 10,
        result = value + 10;
        
    function doSomething(items) {
        // 程式碼邏輯
    }
    
    // ...
}

【注意】:

  • 當函式在宣告之前就使用時,在 JSLint 和 JSHint 中都會給出警告。
  • 函式宣告不應當出現在語句塊之內。
// 不好的寫法
if (condition) {
    function doSomething() {
        alert("Hi!");
    }
} elsew {
    function doSomething() {
        alert("Yo!");
    }
}

【說明】:這段程式碼在不同瀏覽器中的執行結果也是不盡相同的。不管 condition 的計算結果如何,大多數瀏覽器都會自動使用第二個宣告。而 Firefox 則根據 condition 的計算結果選用合適的函式宣告。這種場景是 ECMAScript 的一個灰色地帶,應當儘可能避免。函式宣告應當在條件語句的外部使用。這種模式也是 Google 的 JavaScript 風格指南明確禁止的。

函式呼叫間隔

一般情況下,對於函式呼叫寫法推薦的風格是,在函式名和左括號之間沒有空格。這樣做是為了將它和塊語句(block statement)區分開來。

// 好的寫法
doSomething(item);

// 不好的寫法:看起來像一個塊語句
doSomething (item);

// 用來做對比的塊語句
while (item) {
    // 程式碼邏輯
}

【說明】:

  • Crockford 程式設計規範對此有明確的規定。
  • jQuery 核心風格指南中的規定更進一步,規定應當在左括號之後和右括號之前都加上空格。
// jQuery 風格
doSomething( item );
  • 這種風格是為了讓引數更易讀。但存在一些例外情況,特別是和這些傳入單引數的函式相關的場景,這些引數包括物件直接量、陣列直接量、函式表示式或者字串。
// jQuery 例外情況
doSomething(function() {});
doSomething({ item: item });
doSomething([ item ]);
doSomething("Hi!");
  • 通常情況下,如果一個風格有超過一個例外,那麼這種風格是不好的,因為這會給開發者帶來一些困惑。

立即呼叫的函式

JavaScript 中允許宣告匿名函式(本身沒有命名的函式),並將匿名函式賦值給變數或者屬性。

var doSomething = function() {
    // 函式體
};

【用法】:在匿名函式的尾部加上一對圓括號來立即執行並返回一個值,然後將這個值賦值給變數。

// 不好的寫法
var value = function() {
    
    // 函式體
    
    return  {
        message: "Hi";
    }
}();

【問題】:會讓人誤以為將一個匿名函式賦值給了這個變數。除非讀完整段程式碼看到最後一行的那對圓括號,否則你不會知道是將函式賦值給變數還是將函式的執行結果賦值給變數。這種困惑會影響程式碼的可讀性。

【改進】:為了讓立即執行函式能夠被一眼看出來,可以將函式用一對圓括號包裹起來。

// 好的寫法
var value = (function() {
    
    // 函式體
    
    return {
        messge: "Hi";
    }
}());

【說明】:通過圓括號將立即執行函式包裹這種方式,可以清晰地表明這是一個立即執行的函式,並且新增這一對圓括號並不會改變程式碼的邏輯。Crockford 程式設計規範推薦這種模式,並且在省略圓括號的情況下,JSLint 會發出警告。

嚴格模式

ECMAScript5 引入嚴格模式(strict mode),希望通過這種方式來謹慎地解析執行 JavaScript,以減少錯誤。

【指令】:

"use strict"

【說明】:

  • 儘管這看起來像是一個沒有賦值給變數的字串,但 ECMAScript5 JavaScript 引擎還是會將其識別為一條指令,以嚴格模式來解析程式碼。
  • 該編譯指令(pragma)不僅用於全域性,也適用於區域性,比如一個函式內。但是不推薦將 “use strict” 用在全域性作用域中(儘管所有流行的程式設計規範中都沒有提及),因為這會讓檔案中的所有程式碼都以嚴格模式來解析,所以如果將若干個檔案連接合併成一個檔案時,當其中一個檔案在全域性作用域中啟用了嚴格模式,則所有的程式碼都將以嚴格模式解析。由於嚴格模式中的運算子規則和在非嚴格模式下的情形有很大不同,因此其他檔案中的(非嚴格模式下的)程式碼很可能會報錯。

【示例】:

// 不好的寫法:全域性的嚴格模式
"use strict"
function doSomething() {
    // 程式碼
}

// 好的寫法
function doSomething() {
    "use strict";
    // 程式碼
}

【技巧】:如果你希望在多個函式中應用嚴格模式而不必寫很多行 “use strict” 的話,可以使用立即執行函式。

// 好的寫法
(funciton() {
    "use strict";
    function doSomething() {
        // 程式碼
    }
    function doSomethingElse() {
        // 程式碼
    }
})();

【注意】:當 “use strict” 出現在函式體之外,在 JSLint 和 JSHint 中都會給出警告。

相等

由於 JavaScript 具有強制型別轉換機制(type coercion),JavaScript 中的判斷相等操作是很微妙的。對於某些運算來說,為了得到成功的結果,強制型別轉換會驅使某種型別的變數自動轉換成其他不同型別,這種情形往往會造成意想不到的結果。

【常見的場景】:
使用了判斷相等運算子 == 和 != 的時候。當要比較的兩個值的型別不同時,這兩個運算子都會有強制型別轉換。

  • 字串和數字比較:字串會被轉換為數字,類似使用 Number() 轉換函式。
  • 布林值和數字比較:布林值會首先轉換為數字,然後進行比較。
  • 物件和其他型別比較:首先呼叫物件的 valueOf() 方法,得到原始型別值再進行比較。如果沒有定義 valueOf(),則呼叫 toString()。
var object = {
    toString: function() {
        return "Ox19";
    }
};

console.log(object == 25);  // true
  • null 和 undefined 相等。

【說明】:

  • 由於強制型別轉換的緣故,推薦不要使用 == 和 !=,而是應當使用 === 和 !==。用這些運算子作比較不會涉及強制型別轉換。
  • jQuery 核心風格指南則允許在和 null 比較時用 ==,因為這時程式編寫者往往是想判斷值是否為 null 或 undefined。
if(object.a == null) {
    // 等價於 object.a === null || object.a === undefined
}

【推薦】:毫無例外地總是使用 === 和 !==。

【注意】:如果使用了 == 或 !=,JSLint 預設會報警告,而 JSHint 則會針對使用 == 或 != 來比較假值的情形報警告。

eval()

在 JavaScript 中,eval() 的引數是一個字串,eval() 會將傳入的字串當作程式碼來執行。開發者可以通過這個函式來載入外部的 JavaScript 程式碼,或者隨即生成 JavaScript 程式碼並執行它。

eval("alert('Hi!')");

var count = 10;
var number = eval("5 + count");
console.log(number);    // 15

【說明】:在 JavaScript 中 eval() 並不是唯一可以執行 JavaScript 字串的函式,使用 Function 建構函式亦可以做到這一點,setTimeout() 和 setInterval() 也可以。

setTimeout("document.body.style.background='red'", 50);
setInterval("document.title = 'It is now '" + (new Date()), 1000);

【通用原則】:嚴禁使用 Function,並且只在別無他法時使用 eval()。setTimeout() 和 setInterval() 也是可以使用的,但不要用字串形式而要用函式。

setTimeout(function() {
    document.body.style.background = 'red';
}, 50);

setInterval(function() {
    document.title = 'It is now ' + (new Date());
}, 1000);

【說明】:ECMAScript5 嚴格模式對於 eval() 有著嚴格的限制,禁止在一個封閉的作用域中使用它建立新變數或者函式。這條限制幫助我們避免了 eval() 先天的安全漏洞。

原始包裝型別

JavaScript 中的一個不易被瞭解且被常常誤解的方面是,這門語言對原始包裝型別(primitive wrapper types)的依賴。

【概述】:JavaScript 裡有 3 種原始包裝型別:String、Boolean 和 Number。每種型別都代表全域性作用域中的一個建構函式,並分別表示各自對應的原始值的物件。

【主要作用】:讓原始值具有物件般的性行為。

var name = "Nicholas";
console.log(name.toUpperCase());

【說明】:儘管 name 是一個字串,是原始型別不是的物件,但你仍然可以使用諸如 toUpperCase() 之類的方法,即將字串當作物件來對待。這種做法之所以行得通,是因為在這條語句的表象背後 JavaScript 引擎建立了 String 型別的新例項,緊跟著就被銷燬了,當再次需要時就會又建立另外一個物件。當再次需要時就會又建立另外一個物件。

【示例】:

var name = "Nicholas";
name.author = true;
console.log(name.author);   // undefined

【說明】:在第 2 行結束後,author 屬性就不見了。因為表示在這個字串的臨時 String 物件在第 2 行執行結束後就銷燬了,在第 3 行中又建立了一個新 String 物件,但此時該 String 物件沒有 author 屬性。

【建議】:儘管我們可以使用這些包裝型別,但推薦大家避免使用它們。因為開發者的思路常常會在物件和原始值之間跳來跳去,這樣會在增加出 bug 的概率,從而使開發者陷入困惑。何況也沒有理由自己手動建立包裝型別物件。

【注意】:Google 的 JavaScript 風格指南禁止使用原始包裝型別。當你使用 String、Number 或 Boolean 來建立新物件時,JSLint 和 JSHint 都會給出警告。