Hatena::ブログ(Diary)

latest log このページをアンテナに追加 RSSフィード

2009-10-06

type detection

2011-07-10追記 モダンブラウザでは、 typeof /^a/ は "function" ではなく "object" に修正されています。2009年10月(このエントリの初出)時点で /^a/("a") と記述可能な場合がありましたが、現在はエラーになります。

2010-01-21追記 http://d.hatena.ne.jp/uupaa/20100120/1263998056

2009-10-18追記

uu.type(new String('hoge'))とか渡した時4が帰ってきたりするので、プリミティブラッパー(ってJSで言うのか?)の判定を_TYPEに入れたほうがいいのかもしれませんね。

by tera

コメント欄でのご指摘を受け、以下のコードを追記しています。

                "[object Boolean]":  0x40,  // uu.type(new Boolean(false)) 2009-10-18追記
                "[object Number]":   0x80,  // uu.type(new Number(0)) 2009-10-18追記
                "[object String]":   0x100, // uu.type(new String('')) 2009-10-18追記

tera さん フォローありがとうございます。


本文

js は型を判断する簡単な方法が用意されていません。

ブラウザが typeof と Object.prototype.toString.call で返す値を調べ、そこから型を判断する方法を模索してみます。

ブラウザ戻り値を調べてみる

<script>
window.onload = function() {
  show("undef", void 0);
  show("null",  null);
  show("bool",  false);
  show("num",   1);
  show("str",   "a");
  show("regexp", /^a/);
  show("ary",   []);
  show("func",  show);
  show("date",  new Date);
  show("node",  document.body);
  show("nodelist", document.getElementsByTagName("body"));
  show("arguments", arguments);
}
function show(title, mix) {
  alert(title + "=" + typeof mix + "," + Object.prototype.toString.call(mix));
}
</script>
結果

以下の気になるポイントに色をつけておきました。

  • RegExp
    • Firefox2(function) → Firefox3(object)
    • Google Chrome3(object) → Google Chrome4(function)
  • NodeList
    • Safari3+(function)
  • typeof mix で正しく判断できるのは、undefined, boolean, number, string のみ
  • typeof mix === "function" は function, RegExp, NodeList で true になる可能性がある
  • Object.prototype.toString.call(undefined) や call(null) は call(window) と同じ結果になっている
    • ECMAScript で明文化されてるか分からないけど、現状そうなってるので、そういう仕様なんだと思う。
Chrome 3+ typeofObject.prototype.toString.call
undefined "undefined""[object builtins]"
null "object""[object builtins]"
false "boolean""[object Boolean]"
1 "number""[object Number]"
"a" "string""[object String]"
/^a/ (Chrome 3)"object""[object RegExp]"
/^a/ (Chrome 4)"function""[object RegExp]"
/^a/ (Chrome 14)"object""[object RegExp]"
[] "object""[object Array]"
show "function""[object Function]"
new Date "object""[object Date]"
document.body "object""[object HTMLBodyElement]"
document.getElementsByTagName("body")"object""[object NodeList]"
arguments "object""[object Object]"
Safari3.1+ typeofObject.prototype.toString.call
undefined "undefined""[object DOMWindow]"
null "object""[object DOMWindow]"
false "boolean""[object Boolean]"
1 "number""[object Number]"
"a" "string""[object String]"
/^a/ (Safari 3.1)"function""[object RegExp]"
/^a/ (Safari 5.1)"object""[object RegExp]"
[] "object""[object Array]"
show "function""[object Function]"
new Date "object""[object Date]"
document.body "object""[object HTMLBodyElement]"
document.getElementsByTagName("body")"function""[object NodeList]"
arguments "object""[object Arguments]"
Firefox2+ typeofObject.prototype.toString.call
undefined "undefined""[object Window]"
null "object""[object Window]"
false "boolean""[object Boolean]"
1 "number""[object Number]"
"a" "string""[object String]"
/^a/ (Firefox 2)"function""[object RegExp]"
/^a/ (Firefox 3)"object""[object RegExp]"
/^a/ (Firefox 3.5)"object""[object RegExp]"
/^a/ (Firefox 5)"object""[object RegExp]"
[] "object""[object Array]"
show "function""[object Function]"
new Date "object""[object Date]"
document.body "object""[object HTMLBodyElement]"
document.getElementsByTagName("body")"object""[object HTMLCollection]"
arguments "object""[object Object]"
IE6+ typeofObject.prototype.toString.call
undefined "undefined""[object Object]"
null "object""[object Object]"
false "boolean""[object Boolean]"
1 "number""[object Number]"
"a" "string""[object String]"
/^a/ "object""[object RegExp]"
/^a/ (IE9) "object""[object RegExp]"
[] "object""[object Array]"
show "function""[object Function]"
new Date "object""[object Date]"
document.body "object""[object Object]"
document.getElementsByTagName("body")"object""[object Object]"
arguments "object""[object Object]"
Opera9.5+ typeofObject.prototype.toString.call
undefined "undefined""[object Window]"
null "object""[object Window]"
false "boolean""[object Boolean]"
1 "number""[object Number]"
"a" "string""[object String]"
/^a/ (Opera 9.5) "object""[object RegExp]"
/^a/ (Opera 11.50) "object""[object RegExp]"
[] "object""[object Array]"
show "function""[object Function]"
new Date "object""[object Date]"
document.body "object""[object HTMLBodyElement]"
document.getElementsByTagName("body")"object""[object NodeList]"
arguments "object""[object Object]"

型を判断する順番

順番も重要になります。

型が不明な変数 mix の型を判断するには、

  1. typeof mix で undefined, Boolean, Number, String を検出
  2. Object.prototype.toString.call(mix) で RegExp, Array, Function, Date を検出
  3. !mix で null を検出
  4. "length" in mix で 擬似配列(NodeList や arguments) を検出
  5. これ以外は Object 型

とやればよさそうです。

typeof mix の次に Object.prototype.toString.call(mix) するのは call(mix) が重いからです。

コード化してみる

Google Code Archive - Long-term storage for Google Code Project Hosting.

var uu = {}; // library namespace

(function() {
var _TYPE   = { "undefined":         0x20,
                "boolean":           0x40,
                "number":            0x80,
                "string":            0x100,
                "[object Boolean]":  0x40,  // uu.type(new Boolean(false)) 2009-10-18追記
                "[object Number]":   0x80,  // uu.type(new Number(0)) 2009-10-18追記
                "[object String]":   0x100, // uu.type(new String('')) 2009-10-18追記
                "[object RegExp]":   0x200,
                "[object Array]":    0x400,
                "[object Function]": 0x800,
                "[object Date]":     0x1000 },
    _TYPE_DETECTOR = Object.prototype.toString;

Google Code Archive - Long-term storage for Google Code Project Hosting.

uu = {
  // --- type ---
  type:         uutype,         // [1] uu.type("str") -> 0x100(uu.STR)
                                // [2] uu.type("str", uu.STR | uu.NUM) -> true
  HASH:         1,              // Object(Hash)
  NODE:         2,              // Node
  FAKE:         4,              // FakeArray, "length" in mix(NodeList, arguments)
  RGBA:         8,              // { r, g, b, a }
  NULL:         0x10,           // null
  UNDEF:        0x20,           // undefined
  BOOL:         0x40,           // Boolean
  NUM:          0x80,           // Number
  STR:          0x100,          // String
  REGEXP:       0x200,          // RegExp
  ARRAY:        0x400,          // Array
  FUNC:         0x800,          // Function
  DATE:         0x1000          // Date
};

Google Code Archive - Long-term storage for Google Code Project Hosting.

// --- type ---
// uu.type - type detection
// [1] uu.type("str") -> 0x100(uu.STR)
// [2] uu.type("str", uu.STR | uu.NUM) -> true
function uutype(mix,     // @param Mix:
                match) { // @param Number(= 0): match types
                         // @return Boolean/Number: true = match,
                         //                         false = unmatch
                         //                         number is matched bits
  var rv = _TYPE[typeof mix] || _TYPE[_TYPE_DETECTOR.call(mix)] ||
           (!mix ? 16 : mix.nodeType ? 2 : "length" in mix ? 4 :
            ("r" in mix && "g" in mix && "b" in mix && "a" in mix) ? 8 : 1);
  // 16: NULL, 2: NODE, 4: FAKE, 8: RGBA, 1: HASH
  return match ? !!(match & rv) : rv;
}
})();

定数ではなく 16 や 4 などのマジックナンバー(即値)を使ってるのは速度的な理由から。

三項演算子も多用してたりと、ちょっと行儀が悪いコード片です。


まるで

バッドノウハウの塊ですね。将来大丈夫かこれ…

このページみせれば誰でもメンテできるだろうから大丈夫だね

他のライブラリってどうしてるの?

「この考え方であってんのかなー」と不安になったので有名どころを、ざーっと見てみました。

YUI 3.0.0
/*
Copyright (c) 2009, Yahoo! Inc. All rights reserved.
Code licensed under the BSD License:
http://developer.yahoo.net/yui/license.txt
version: 3.0.0
build: 1549
*/

var L     = Y.Lang, 
ARRAY     = 'array',
BOOLEAN   = 'boolean',
DATE      = 'date',
ERROR     = 'error',
FUNCTION  = 'function',
NUMBER    = 'number',
NULL      = 'null',
OBJECT    = 'object',
REGEX     = 'regexp',
STRING    = 'string',
TOSTRING  = Object.prototype.toString,
UNDEFINED = 'undefined',

TYPES     = {
    'undefined'         : UNDEFINED,
    'number'            : NUMBER,
    'boolean'           : BOOLEAN,
    'string'            : STRING,
    '[object Function]' : FUNCTION,
    '[object RegExp]'   : REGEX,
    '[object Array]'    : ARRAY,
    '[object Date]'     : DATE,
    '[object Error]'    : ERROR 
},

/**
 * Returns a string representing the type of the item passed in.
 * @method type
 * @param o the item to test
 * @return {string} the detected type
 */
L.type = function (o) {
    return  TYPES[typeof o] || TYPES[TOSTRING.call(o)] || (o ? OBJECT : NULL);
};

(・∀・)人(・∀・)ナカーマ発見。

Object.prototype.toString(o) で Error オブジェクトも取れるんですね。

# Errorを検出しても、ちょっと使い道がわからないので、実装は控えます。

おまけ

undefined かどうか調べる方法

  1. if (typeof mix === "undefined") {}
  2. if (mix === undefined) {}
  3. if (mix === void 0) {}

一番下がお勧めです。

Infinity や NaN は "number", "[object Number]" になります。

  • show("Infinity", 1 / 0);
  • show("NaN", 0 / 0);

ついったー

速度的なものも含めて色々と調べてたら、記事にするまで2日もかかってしまいました。

レスの機会を見事に逃したので、ここで感謝を

os0x さん、いつもアドバイスありがとうございます。

Safari の typeof NodeList === "function" は不具合っぽいですね。

teratera 2009/10/16 12:39 uu.type(new String('hoge'))とか渡した時4が帰ってきたりするので、プリミティブラッパー(ってJSで言うのか?)の判定を_TYPEに入れたほうがいいのかもしれませんね。

uupaauupaa 2009/10/19 03:30 tera さん。フォローありがとうございます。

uu.type(new String('hoge')) → 4 (uu.FAKE) が返ってきてしまっていました。
配慮不足ですね。

new Boolean(false), new Number(0), new String("") もカバーするように対策したコードを本文に追記しました。

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト

コメントを書くには、なぞなぞ認証に回答する必要があります。

トラックバック - http://d.hatena.ne.jp/uupaa/20091006/1254926477