JavaScript檢測原始值、引用值、屬性

上週寫過一篇讀書筆記《編寫可維護的JavaScript》之編程實踐,其中 第8章 避免『空比較』是博主在工做中遇坑較多的雷區,因此特此把該章節從新整理分享,但願你們再也不坑隊友(>﹏<)。javascript

在 JavaScript 中,咱們經常會看到這樣的代碼:變量與null的比較(這種用法頗有問題),用來判斷變量是否被賦予了一個合理的值。好比:java

var Controller = {
    process: function(items) {
        if (items !== null) { // 很差的寫法
            items.sort();
            items.forEach(function(item) {
                // 執行一些邏輯
            });
        }
    }
}

在這段代碼中,process()方法顯然但願items是一個數組,由於咱們看到items擁有sort()forEach()。這段代碼的意圖很是明顯:若是參數items不是一個組數,則中止接下來的操做。這種寫法的問題在於,和null的比較並不能真正避免錯誤的發生。items的值能夠是1,也能夠是是字符串,甚至能夠是任意對象。這些值都和null不相等,進而會致使process()方法一旦執行到sort()時就會出錯。正則表達式

僅僅和null比較並不能提供足夠的信息來判斷後續代碼的執行是否真的安全。好在 JavaScript 爲咱們提供了不少種方法來檢測變量的真實值。編程

檢測原始值

在 JavaScript 中有5種原始類型(也稱爲簡單數據類型): StringNumberBooleanUndefinedNull。若是你但願一個值是StringNumberBooleanUndefined,最佳選擇是使用typeof運算符,它會返回一個表示類型的字符串。跨域

  • 對於字符串,typeof返回"string"數組

  • 對於數字,typeof返回"number"瀏覽器

  • 對於布爾值,typeof返回"boolean"安全

  • 對於undefined,typeof返回"undefined"函數

typeof的基本語法是:typeof variable,你還能夠這樣用:typeof(variable),儘管這是合法的 JavaScript 語法,這種用法讓typeof看起來像一個函數而非運算符。鑑於此,咱們更推薦無括號的寫法。this

使用typeof來檢測這4種原始類型是很是安全的作法。來看下面這些例子。

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

// 檢測"Number"
if (typeof count === "number") {
    updateCount(count);
}

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

// 檢測"Undefined"
if (typeof MyApp === "undefined") {
    MyApp = {
        // 其餘代碼
    };
}

typeof運算符的獨特之處在於,將其用於一個未聲明的變量也不會報錯。未定義的變量和值爲undefined的變量經過typeof都將返回"undefined"

最後一個原始類型null,經過typeof將返回"object",這看上去很怪異,被認爲是標準規範的嚴重 bug,所以在編程時要 杜絕使用typeof來檢測null的類型

console.log(typeof null);   // "object"

簡單地和null進行比較一般不會包含足夠的信息以判斷值的類型是否合法,因此null通常不該用於檢測語句。

但有一個例外,若是所指望的值真的是null,則能夠直接和null進行比較。例如:

// 若是你須要檢測 null,則使用這種方法
var element = document.getElementById("my-div");
if (element !== null) {
    element.className = "found";
}

若是 DOM 元素不存在,則經過document.getElementById()獲得的值爲null。這個方法要麼返回一個節點,要麼返回null。因爲這時null是可預見的一種輸出,則能夠用恆等運算符===或非恆等運算符!==來檢測返回結果。

typeof運算符的返回值除了上述提到的stringnumberbooleanundefinedobject以外,還有function。從技術的角度來說,函數在 JavaScript 中也是對象,不是一種數據類型。然而,函數也確實有一些特殊的屬性,所以經過typeof運算符來區分函數和其餘對象是有必要的。這一特性將在後面 檢測函數 中用到。

檢測引用值

在 JavaScript 中除了原始值以外的都是引用值(也稱爲對象),經常使用的引用類型有:ObjectArrayDateRegExp,這些引用類型都是 JavaScript 的內置對象。typeof運算符在判斷這些引用類型時全都返回"object"

console.log(typeof {});             // "object"
console.log(typeof []);             // "object"
console.log(typeof new Date());     // "object"
console.log(typeof new RegExp());   // "object"

檢測某個引用值類型的最好方法是使用instanceof運算符,instanceof的基本語法是:

value instanceof constructor

// 檢測日期
if (value instanceof Date) {
    console.log(value.getFullYear);
}

// 檢測 Error
if (value instanceof Error) {
    throw value;
}

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

instanceof的一個有意思的特性是它不只檢測構造這個對象的構造器,還檢測原型鏈。原型鏈包含了不少信息,包括定義對象所採用的繼承模式。好比,默認狀況下,每一個對象都繼承自Object,所以每一個對象的value instanceof Object都會返回ture。好比:

var now = new Date();
console.log(now instanceof Object); // ture
console.log(now instanceof Date);   // ture

instanceof運算符也能夠檢測自定義的類型,好比:

function Person(name){
    this.name = name;
}
var me = new Person("Nicholas");
console.log(me instanceof Object);  // ture
console.log(me instanceof Person);  // ture

這段示例代碼中建立了Person類型。變量mePerson的實例,所以me instanceof Persontrue。上文也提到,全部的對象都被認爲是Object的實例,所以me instanceof Object也是ture

在 JavaScript 中檢測 內置類型自定義類型 時,最好的作法就是使用instanceof運算符,這也是惟一的方法。

但有一個嚴重的限制,假設兩個瀏覽器幀(frame)裏都有構造函數Person,幀A中的Person實例frameAPersonInstance傳入到幀B中,則會有以下結果:

console.log(frameAPersonInstance instanceof frameAPerson)    // ture
console.log(frameAPersonInstance instanceof frameBPerson)    // false

儘管兩個Person的定義是徹底同樣的,但在不一樣幀(frame)裏,他們被認爲是不一樣類型。有兩個很是重要的內置類型也有這個問題:ArrayFunction,因此檢測它們通常不使用instanceof

檢測函數

從技術上講,JavaScript 中的函數是引用類型,一樣存在Function構造函數,每一個函數都是其實例,好比:

function myFunc() {}

// 很差的寫法
console.log(myFunc instanceof Function); // true

然而,這個方法亦不能跨幀(frame)使用,由於每一個幀都有各自的Function構造函數,好在typeof運算符也是能夠用於函數的,返回"function"

function myFunc() {}

// 好的寫法
console.log(typeof myFunc === "function"); // true

檢測函數最好的方法是使用typeof,由於他能夠跨幀(frame)使用。

typeof來檢測函數有一個限制。在 IE 8 和更早版本的 IE 瀏覽器中,使用typeof來檢測 DOM 節點中的函數都返回"object"而不是"function"。好比:

// IE8 及更早版本的IE
console.log(typeof document.createElement);         // "object"
console.log(typeof document.getElementById);        // "object"
console.log(typeof document.getElementByTagName);   // "object"

之因此出現這種怪異的現象是由於瀏覽器對 DOM 的實現有差別。簡言之,這些早版本的 IE 並無將 DOM 實現爲內置的 JavaScript 方法,致使內置typeof運算符將這些函數識別爲對象。由於 DOM 是有明肯定義的,瞭解到對象成員若是存在則意味着它是一個方法,開發者每每經過in運算符來檢測 DOM 的方法,好比:

// 檢測 DOM 方法
if ("querySelectorAll" in document) {
    var images = document.querySelectorAll("img");
}

這段代碼檢查querySelectorAll是否認義在document中,若是是,則使用這個方法。儘管不是最理想的方法,若是想在 IE 8 及更早瀏覽器中檢測 DOM 方法是否存在,這是最安全的作法。在其餘全部的情形中,typeof運算符是檢測 JavaScript 函數的最佳選擇。

檢測數組

JavaScript 中最古老的跨域問題之一就是在幀(frame)之間來回傳遞數組。開發者很快發現instanceof Array在此場景中不能返回正確的結果。正如上文提到的,每一個幀都有各自的Array構造函數,所以一個幀中的實例在另一個幀裏不會被識別。

關於如何在 JavaScript 中檢測數組類型已經有狠多研究了,最終 Kangax 給出了一種優雅的解決方案:

function isArray(value) {
    return Object.prototype.toString.call(value) === "[object Array]";
}

Kangax 發現調用某個值的內置toString()方法在全部瀏覽器中都會返回標準的字符串結果。對於數組來講,返回的字符串爲"[object Array]",也不用考慮數組實例實在哪一個幀(frame)中被構造出來的。這種方法在識別內置對象時每每十分有用,但對於自定義對象請不要用這種方法。

ECMAScript5 將Array.isArray()正式引入 JavaScript。惟一的目的就是準確地檢測一個值是否爲數組。同 Kangax 的函數同樣,Array.isArray()也能夠檢測跨幀(frame)傳遞的值,所以不少 JavaScript 類庫目前都相似地實現了這個方法。

function isArray(value) {
    if (typeof Array.isArray === "function") {
        return Array.isArray(value);
    } else {
        return Object.prototype.toString.call(value) === "[object Array]";
    }
}

IE 9+、FireFox 4+、Safari 5+、Opera 10.5+、Chrome 都實現了Array.isArray()方法。

檢測屬性

另一種用到null(以及undefined)的場景是當檢測一個屬性是否在對象中存在時,好比:

// 很差的寫法:檢測假值
if (object[propertyName]) {
    // 一些代碼
}

// 很差的寫法:和null相比較
if (object[propertyName] != null) {
    // 一些代碼
}

// 很差的寫法:和undefined相比較
if (object[propertyName] != undefined) {
    // 一些代碼
}

上面這段代碼裏的每一個判斷,其實是經過給定的名字來檢查屬性的值,而並不是判斷給定的名字所指的屬性是否存在。在第一個判斷中,當屬性值爲假值時結果會出錯,好比:0""(空字符串)falsenullundefined,畢竟這些都是屬性的合法值。

判斷屬性是否存在的最好的方法是使用in運算符。in運算符僅僅會簡單地判斷屬性是否存在,而不去讀屬性的值,若是實例對象的屬性存在、或者繼承自對象的原型,in運算符都會返回true。好比:

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

// 好的寫法
if ("count" in object) {
    // 這裏的代碼會執行
}

// 很差的寫法:檢測假值
if (object["count"]) {
    // 這裏的代碼不會執行
}

// 好的寫法
if ("related" in object) {
    // 這裏的代碼會執行
}

// 很差的寫法,檢測是否爲
if (object["related"] != null) {
    // 這裏的代碼不會執行
}

若是你只想檢查實例對象的某個屬性是否存在,則使用hasOwnProperty()方法。全部繼承自Object的 JavaScript 對象都有這個方法,若是實例中存在這個屬性則返回true(若是這個屬性只存在於原型裏,則返回false)。須要注意的是,在 IE 8 以及更早版本的 IE 中,DOM 對象並不是繼承自 Object,所以也不包含這個方法。也就是說,你在調用 DOM 對象的hasOwnProperty()方法以前應當先檢測其是否存在。

// 對於全部非 DOM 對象來講,這是好的寫法
if (object.hasOwnProperty("related")) {
    // 執行這裏的代碼會
}

// 若是你不肯定是否爲 DOM 對象,則這樣來寫
if ("hasOwnProperty" in object && object.hasOwnProperty("related")) {
    // 執行這裏的代碼會
}

由於存在 IE 8 以及更早版本的 IE 的情形,在判斷實例對象的屬性是否存在時,我更傾向於使用in運算符,只有在須要判斷實例屬性時纔會用到hasOwnProperty()

無論你何時須要檢測屬性的存在性,請使用in運算符或者hasOwnProperty()。這樣作能夠避免不少 bug。

擴展閱讀

歡迎來到 石佳劼的博客,若有疑問,請在「原文」評論區 留言,我會盡可能爲您解答。


相關文章
相關標籤/搜索