JavaScript 中的型別檢查

oschina發表於2013-11-16

  在Badoo的時候我們寫了大量的JS指令碼,光是在我們的移動web客戶端上面就有大概60000行,可想而知,維護這麼多JS可是相當具有挑戰性的。在寫如上規模js指令碼客戶端應用的時候我們必須對一件事保持警惕,就是避免異常的發生。在本篇文章裡面,我想談談一部分型別檢查異常,這時候你或許很難碰到 - 一個TypeError

 

  在MDN連結裡面是這麼解釋的:

"當傳遞給操作符或者函式的操作符或者引數與操作符或者函式本身所期望的操作符或函式型別不相容的時候就會丟擲一個TypeError" -MDN

  所以,想要避免有TypeError丟擲,我們不僅需要檢測下傳遞給函式的值是否正確,還要在在某運算元上使用操作符之前檢查所寫的任何有關驗證這個運算元的程式碼。比如,某個操作符不能檢測null或undefined,而instanceof又不能檢測非函式的引數,在這種情況下把這些操作符使用到某運算元肯定會因為不相容而丟擲TypeError。如果你學過類似Java這樣的靜態型別程式語言就會發現Javascript在這上面極度的敏感,使得你可能會有去使用DartTypeScript 的“超集Javascript”語言的衝動。如果你已經習慣了寫JavaScript或者說JavaScript的程式設計基礎蠻好,你所需要做的就是不要灰心。因為加上型別檢測功能並很複雜,更何況可以幫助那些想讀懂你程式碼的人

  那下面就以一個很直接的例項開始解說吧。這個例子主要是從伺服器上獲取資料,然後在資料上使用操作符後渲染出HTML程式碼。

Api.get('/conversations', function (conversations) {

    var intros = conversations.map(function (c) {
        var name = c.theirName;
        var mostRecent = c.messages[0].text.substring(0, 30);
        return name + ': ' + mostRecent;
    });

    App.renderMessages(intros);

});

  乍眼看這程式碼時,我們會發現不能確定裡面的conversations該是什麼型別的資料。既然程式碼裡面有一個很明顯需要傳一個陣列為變數的資料地圖函式,那我們還是可以假定下它的型別,不過這種假定型別還不一定是合理的,因為實際上它可以代表任何能實現一個資料地圖函式功能的型別。傳給資料地圖的函式可以給c變數生成許多種假定的型別。如果這些假定型別是錯誤的,那麼就會丟擲一個TypeError錯誤,這樣一來,導致renderMessages()函式永遠都不可能被呼叫。

  那麼,在本例項中,我們怎麼去檢測驗證型別呢?嗯,首先我們來看下Javascript中各種驗證資料型別的函式吧。

  typeof

  typeof 運算子返回一個字串來表明這個運算物件的型別。但是它返回的型別非常有限,例如以下所返回的全部為 "object"

typeof {};
typeof [];
typeof null;
typeof document.createElement('div');
typeof /abcd/;

  instanceof

  instanceof 運算子是用來確定一個物件的原型鏈包含原型屬性的一個建構函式。

[] instanceof Array; // true

var Foo = function () {};
new Foo() instanceof Foo; // true

  這樣使用instanceof去檢查一個 native 物件不是一個好主意,因為它並不適用與原始事物的值.

'a' instanceof String; // false
5 instanceof Number; // false
true instanceof Boolean; //false

  Object.prototype.toString

  這個方法在各大JS框架中經常被用於推斷資料型別,也正是因為這個方法的使用規範非常簡明,它普遍適用於各大瀏覽器。在 15.2.4.2 這個版本的 ECMA-262 規範中,toString方法是這樣定義的:

  • 如果引數是未定義的值,則返回"[object Undefined]".
  • 如果引數為null,則返回"[object Null]".
  • 如果適用ToObject函式傳遞引數,則返回物件.
  • 如果引數為類,則返回包含物件的類.(Let class be the value of the [[Class]] internal property of O.)
  • 返回一個由"[物件", 類, 和"]"拼接而成的字串.

  因此,這個方法永遠會以 “[Foo 物件]”的方式返回一個字串,這個Foo有可能是 “空”或是“未定義”又或者是一個用來建立此字串的類。 使用這個方法將轉化一個普通的值、或是一個表示式為任何我們想要的結果,而且這個結果將以字串的形式呈現。

var type = function (o) {
    var s = Object.prototype.toString.call(o);
    return s.match(/\[object (.*?)\]/)[1].toLowerCase();
}

type({}); // "object"
type([]); // "array"
type(5); // "number"
type(null); // "null"
type(); // "undefined"
type(/abcd/); // "regex"
type(new Date()); // "date"

  這就是要解決的問題,對嗎?很不幸的這還不是。仍然有一些例項中的這個方法能夠返回一些不是我們預期的值。

type(NaN); // "number"
type(document.body); // "htmlbodyelement"

  這兩個例子返回的值也許並不是我們所預想的那樣。在例子NaN中,返回“number”是因為技術上NaN是一種數字型別,雖然幾乎我們知道的所有的例子中,如果一個“東西”是數字,就不是非數字。被用來實現<body>元素的內部類是HTMLBodyElement(至少在谷歌和火狐瀏覽器中如此),每一個元素都有各自的指定類。在大多數應用場景中,我們只想知道如果一個“東西”是否是一個元素,如果我們關心元素的標籤名,我們可以使用tagNameproperty來獲取。然而,我們能夠修改我們現有的方法來處理這些事情。

var type = function (o) {

    // handle null in old IE
    if (o === null) {
        return 'null';
    }

    // handle DOM elements
    if (o && (o.nodeType === 1 || o.nodeType === 9)) {
        return 'element';
    }

    var s = Object.prototype.toString.call(o);
    var type = s.match(/\[object (.*?)\]/)[1].toLowerCase();

    // handle NaN and Infinity
    if (type === 'number') {
        if (isNaN(o)) {
            return 'nan';
        }
        if (!isFinite(o)) {
            return 'infinity';
        }
    }

    return type;
}

  現在我們有一個可以對任何我們感興趣的返回正確型別的方法,我們可以改進原來的例子以確保我們沒有任何型別錯誤。

Api.get('/conversations', function (conversations) {

    // 大家讀到這裡就知道conversation應該是個陣列
    if (type(conversations) !== 'array') {
        App.renderMessages([]);
        return;
    }

    var intros = conversations.map(function (c) {

        if (type(c) !== 'object') {
            return '';
        }

        var name = type(c.theirName) === 'string' ? c.theirName : '';
        var mostRecent = '';

        if (type(c.messages) === 'array' &&
            type(c.messages[0]) === 'object' &&
            type(c.messages[0].text) === 'string') {
            mostRecent = c.messages[0].text.substring(0, 30);
        }

        return name + ': ' + mostRecent;
    });

    //在這裡可以做的更多
    App.renderMessages(intros);
});

  很明顯的事實是我們不得不新增更多的程式碼來阻止出現型別錯誤的風險,但Badoo只是讓我們新增很少的JavaScript程式碼,這意味著我們的應用更加穩定。最後,很明顯type方法每一次檢測的時候都要和字串做比較。這很好改進。我們可以構建和Underscore/LoDash/jQuery類似的API:

['Null',
 'Undefined',
 'Object',
 'Array',
 'String',
 'Number',
 'Boolean',
 'Function',
 'RegExp',
 'Element',
 'NaN',
 'Infinite'
].forEach(function (t) {
    type['is' + t] = function (o) {
        return type(o) === t.toLowerCase();
    };
});

// examples:
type.isObject({}); // true
type.isNumber(NaN); // false
type.isElement(document.createElement('div')); // true
type.isRegExp(/abc/); // true

  這也是我們在行動網路應用中型別檢測的JavaScript方法,而且我們發現這些程式碼很簡單而且不容易出錯。在gist裡可以看到type方法的介紹。

  原文地址:http://techblog.badoo.com/blog/2013/11/01/type-checking-in-javascript/

相關文章