1. 程式人生 > >《編寫可維護的JavaScript》讀書筆記之程式設計實踐-避免“空比較”

《編寫可維護的JavaScript》讀書筆記之程式設計實踐-避免“空比較”

避免“空比較”

在 JavaScript 中,我們常會看到這種程式碼:變數與 null 的比較,用來判斷變數是否被賦予一個合理的值。比如:

var Controller = {
    process : function(items) {
        // 不好的寫法
        if(items !== null) {
            items.sort();
            items.forEach(function(item) {
                // 執行一些邏輯
            });
        }
    }
};
  • 意圖:如果引數 items 不是一個數組,則停止接下來的操作。
  • 問題:和 null 的比較並不能真正避免錯誤的發生。
  • 原因:items 的值可以是 1,也可以是字串,甚至可以是任意物件。這些值都和 null 不想等,進而會導致 process() 方法一旦執行到 sort() 時就會出錯。
  • 結論:僅僅和 null 比較並不能提供足夠的資訊來判斷後續程式碼的執行是否真的安全。

檢測原始值

如果你希望一個值是字串、數字、布林值或 undefined,最佳選擇是使用 typeof 運算子。typeof 運算子會返回一個表示值的型別的字串。

  • 字串:typeof 返回 “string”。
  • 數字:typeof 返回 “number”。
  • 布林值:typeof 返回 “boolean”。
  • undefined:typeof 返回 “undefined”。

typeof 語法:

typeof variable
/*
 * 儘管這是合法的 JavaScript 語法,這種用法讓 typeof
 * 看起來像一個函式而非運算子。因此,更推薦無括號的寫法
 */
typeof(variable)

檢測 4 種原始值的做法:

// 檢測字串
if(typeof name === "string") {
    anotherName = name.substring(3);
}

// 檢測數字
if(typeof count === "number") {
    unpdateCount(count);
}

// 檢測布林值
if(typeof found === "boolean" && found) {
    message("Found!");
}

// 檢測 undefined
if(typeof MyApp === "undefined") {
    MyApp = {
        // 其他的程式碼
    };
}

注意:

typeof 運算子用於一個未宣告的變數也不會報錯。未定義的變數和值為 undefined 的變數通過 typeof 都將返回 “undefined”。

關於 null:

  • 概述:原始值 null 一般不應用於檢測,因為簡單地和 null 比較通常不會包含足夠的資訊以判斷值的型別是否合法。但有個例子,如果所期望的值真的是 null,則可以直接和 null 進行比較。這時應當使用 === 或者 !== 來和 null 進行比較。
  • 示例
// 如果你需要檢測 null,則使用這種方法
var element = document.getElementById("my-div");
if(element !== null) {
    element.className = "found";
}
/*
 * 如果 DOM 元素不存在,則獲得的節點的值為 null。這個方法要麼返回一個節點,要麼返回 null。
 * 由於這時 null 是可預見的一種輸出,則可以使用 !== 來檢測返回結果。
  • 注意:執行 typeof null 則返回 “object”,這是一種低效的判斷 null 的方法,甚至被認為是標準規範的嚴重 bug,因此在程式設計時杜絕使用 typeof 來檢測 null 的型別。如果你需要檢測 null,則直接使用恆等運算子(===)或非恆等運算子(!==)。

檢測引用值

引用值也稱作物件(object),在 JavaScript 中除了原始值之外的值都是引用。

常見的內建引用型別:

  • Object
  • Array
  • Date
  • RegExp
  • Error

對引用值的檢測:

  • typeof 運算子在判斷這些引用型別以及 null 時都會返回 “object”,因此 typeof 不適合檢測引用值。
var arr = [1, 2],
	obj = {name: "clvsit"},
	date = Date(),
	reg = RegExp(),
	err = Error();
console.log(typeof arr === "object"); // true
console.log(typeof obj === "object"); // true
console.log(typeof date === "object"); // false
console.log(typeof reg === "object"); // true
console.log(typeof err === "object"); // true

從上述示例可以發現,typeof date === “object” 返回 false,為什麼會這樣?

console.log(typeof date); // 'string'
console.log(date.toString()); // "Sat Jan 05 2019 15:06:28 GMT+0800 (中國標準時間)"
console.log(date); // "Sat Jan 05 2019 15:06:28 GMT+0800 (中國標準時間)"

實踐可知,在使用 Date 物件時會自動呼叫 Date 物件的 toString() 方法,因此 typeof date 得到的結果為 ‘string’。

  • 檢測某個引用值的型別的最好方法是使用 instanceof 運算子。

instanceof:

  • 語法
value instanceof constructor
  • 示例
// 檢測日期
if(value instanceof Date) {
    console.log(value.getFullYear());
}

// 檢測正則表示式
if(value instanceof ReqExp) {
    if(value.test(anotherValue)) {
        console.log("Matches");
    }
}

// 檢測 Error
if(value instanceof Error) {
    throw value;
}
  • 注意
  1. instanceof 不僅檢測構造這個物件的構造器,還檢測原型鏈。而原型鏈包含了很多資訊,包括定義物件所採用的繼承模式。比如,預設情況下,每個物件都繼承來自 Object,因此每個物件的 value instanceof Object 都會返回 true(使用 value instanceof Object 來判斷物件是否屬於某個特定型別的做法並非最佳)。
var now = new Date();

console.log(now instanceof Object);  // true
console.log(now instanceof Date);    // true
  1. instanceof 運算子也可以檢測自定義的型別(最好的做法)。
function Person(name) {
    this.name = name;
}

var me = new Person("Nicholas");

console.log(me instanceof Object);  // true
console.log(me instanceof Person);  // true

存在的限制:

  • 情景:假設一個瀏覽器幀(frameA) 裡的一個物件被傳入到另一個瀏覽器幀(frameB)中,兩個幀裡都定義了建構函式 Person。如果來自幀 A 的物件是 幀 A 的 Person 的例項,則如下規則成立:
// true
frameAPersonInstance instanceof frameAPerson;

// false
frameAPersonInstance instanceof frameBPerson;
  • 原因
    每個幀都擁有 Person 的一份拷貝,被認為是該幀中 Person 的拷貝的例項,儘管兩個定義可能完全一樣。
  • 注意
    這個問題不僅出現在自定義型別上,其他兩個非常重要的內建型別也存在這個問題:函式和陣列。對於這兩個型別來說,一般用不著使用 instanceof。

檢測函式

從技術上講,JavaScript 中的函式是引用型別,同樣存在 Function 建構函式,每個函式都是其例項。

function myFunc() {}

// 不好的寫法
console.log(myFunc instanceof Function);  // true
  • 注意
  1. instanceof 不能跨幀(frame)使用,因為每個幀都有各自的 Function 建構函式。
  2. typeof 運算子也是可以用於函式的,返回 “function”。檢測函式最好的方法是使用 typeof,因為它可以跨幀使用。
function myFunc() {}

// 好的寫法
console.log(typeof myFunc === "function");  // true
  1. typeof 檢測函式的限制:在 IE 8 和更早版本的 IE 瀏覽器中,使用 typeof 來檢測 DOM 節點(比如 document.getElementById())中的函式都返回 “object” 而不是 “function”。之所以出現這種現象是因為瀏覽器對 DOM 的實現有差異。簡言之,早期的 IE 並沒有將 DOM 實現為內建的 JavaScript 方法,導致內建 typeof 運算子將這些函式識別為物件。
  2. 在 IE 8 及早期版本,開發者往往通過 in 運算子來檢測 DOM 的方法。
// 檢測 DOM 方法
if("querySelectorAll" in document) {
    images = document.querySelectorAll("img");
}
  1. 儘管使用 in 檢測 DOM 方法不是最理想的方法,但如果想在 IE 8 及更早瀏覽器中檢測 DOM 方法是否存在,這是最安全的做法。在其他所有情形中,typeof 運算子是檢測 JavaScript 函式的最佳選擇。

檢測陣列

JavaScript 最古老的跨域問題之一就是在幀之間來回傳遞陣列。因為每個幀都有各自的 Array 建構函式,因此一個幀中的例項在另外一個幀裡面不會被識別。

  • 檢測方法
  1. 鴨式辨型(duck typing)
// 採用鴨式辨型的方法檢測陣列
function isArray(value) {
    return typeof value.sort === "function";
}
/*
 * 缺陷:陣列是唯一包含 sort() 方法的物件。
 * 如果傳入 isArray() 的引數是一個包含 sort() 
 * 方法的物件,也會返回 true。
 */
var obj = {
    sort: function () {
    
        // ...
    }
};
isArray(obj) // true; 
  1. Kangax 方案
function isArray(value) {
    return Object.prototype.toString.call(value) === "[object Array]";
}
  • 解釋
    呼叫某個值的內建 toString() 方法在所有瀏覽器中都會返回標準的字串結果。對於陣列來說,返回的字串為 “[object Array]”,也不用考慮陣列例項實在哪個幀中被構造出來。
  • 缺陷
    對於自定義物件使用這種方法會存在問題,比如內建 JSON 物件將返回 “[object JSON]”。
  • 後續
    ECMAScript5 將 Array.isArray() 正式引入 JavaScript。唯一的目的就是準確地檢測一個值是否為陣列。Array.isArray() 可以檢測跨幀傳遞的值。同樣很多 JavaScript 類庫都實現了這個方法。
function isArray(value) {
    if(typeof Array.isArray === "function") {
        return Array.isArray(value);
    } else {
        return Object.prototype.toString.call(value) === "[object Array]";
    }
}

檢測屬性

另外一種用到 null(以及 undefined)的場景是當檢測一個屬性是否在物件中存在時。

// 不好的寫法:檢測假值
if(object[propertyName]) {
    // 一些程式碼
}

// 不好的寫法:和 null 相比較
if(object[propertyName] !== null) {
    // 一些程式碼
}

// 不好的寫法:和 undefined 比較
if(object[propertyName] !== undefined) {
    // 一些程式碼
}
  • 解釋
    上面這段程式碼裡的每個判斷,實際上是通過給定的名字來檢查屬性的值,而非判斷給定的名字所指的屬性是否存在。因為當屬性值為假值(false value)時結果會出錯,比如 0、""、false、null 和 undefined。畢竟,這些都是屬性的合法值。比如,如果屬性記錄了一個數字,則這個值可以是零,這樣的話,上段程式碼中的第一個判斷就會導致錯誤。以此類推,如果屬性值為 null 或者 undefined 時,三個判斷都會導致錯誤。

  • 合適的方式
    判斷屬性是否存在的最好方法是使用 in 運算子。因為 in運算子僅僅會簡單地判斷屬性是否存在,而不會去讀屬性的值,這樣就可以避免解釋部分提到的問題。如果例項物件的屬性存在、或者繼承自物件的原型,in 運算子都會返回 true。

var object = {
    count : 0,
    related : null
}

// 好的寫法
if("count" in object) {
    // 這裡的程式碼會執行
}

// 不好的寫法
if(object["count"]) {
    // 這裡的程式碼不會執行
}

// 好的寫法
if("related" in object) {
    // 這裡的程式碼會執行
}

// 不好的寫法:檢測是否為 null
if(object["related"] !== null) {
    // 這裡程式碼不會執行
}

hasOwnProperty():

  • 概述
    如果只想檢查例項物件的某個屬性是否存在,可以使用 hasOwnProperty() 方法,所有繼承自 Object 的 JavaScript 物件都有這個方法。如果例項中存在這個屬性則返回 true(如果這個屬性只存在於原型裡,則返回 false)。
  • 注意
    在 IE 8 及早期版本,DOM 物件並非繼承自 Object,因此也不包含這個方法。也就是說,你在呼叫 DOM 物件的 hasOwnProperty() 方法之前應當先檢測其是否存在(假如你已經知道物件不是 DOM,則可以省略這一步)。
// 對於所有非 DOM 物件來說,這是好的寫法
if(object.hasOwnProperty("related")) {
    // 執行這裡的程式碼
}

// 如果你不確定是否為 DOM 物件,則這樣來寫
if("hasOwnProperty" in object && object.hasOwnProperty("related")) {
    // 執行這裡的程式碼
}
  • 總結
    因為存在 IE 8 以及更早版本 IE 的情形,在判斷例項物件的屬性是否存在時,推薦使用 in 運算子。只有在需要判斷例項屬性時才會用到 hasOwnProperty()。