Javascript オブジェクト判定あれこれ

JSRubyここらへんのことをやるのに、Javascript でのオブジェクト判定をあれこれ試したので、TIPS 的に書いてみる。
といっても、教えてもらったり、blog で読んだりしたことを試してまとめてみただけなのだが。実はもっと良い方法があると言うことであれば教えてもらえると嬉しい。

オブジェクト判定

typeof とか instanceof があるので、ここらへんを使えば済みそうなのだが、ちょっと問題が。

typeof "abc" // => "string"
typeof new String("abc") // => "object"

"abc" instanceof String // => false
new String("abc") instanceof String // => true

cho45 さんが書かれているように「 Javascript では全てがオブジェクトではない」からこんなことになるのだが、解決方法はとりあえず2つ。

// 方法1
typeof "abc".valueOf() // => "string"
typeof (new String("abc")).valueOf() // => "string"

// 方法2
Object.prototype.toString.call("abc") // => "[object String]"
Object.prototype.toString.call(new String("abc")) // => "[object String]"

"typeof obj.valueOf()" の方はお手軽だが、primitive しか判定できず、null や undefined を入れたらエラーになる。
Object.prototype.toString.call() は Array や HTMLDocument オブジェクトも判定可能だ。null や undefined でもエラーにはならない(でも "[object Window]" なんてのが返ってくるので、どちらにしても別途判定は必要なのだが)。

プロトタイプ判定

上記オブジェクト判定では obj が new Foo() したものなのか new Bar() したものなのかを知ることは出来ない

function Foo() {}
function Bar() {}
obj = new Foo()
Object.prototype.toString.call(obj) // => "[object Object]"
obj = new Bar()
Object.prototype.toString.call(obj) // => "[object Object]"

この場合は isPrototypeOf() を使う。

obj = new Foo()
Foo.prototype.isPrototypeOf(obj) // => true
Bar.prototype.isPrototypeOf(obj) // => false

ちなみに JSRuby では、Ruby のオブジェクトの振る舞いを RubyEngine.RubyObject.String とかに記述し、var obj = new RubyEngine.RubyObject.String("abc") のように生成しているのに対し、逆に obj がどの RubyObject.* を new したものなのかを判定したいときや、構文木のノードの種類の判定などにこれを使っている。

配列っぽいオブジェクト ( Collection ) 判定

Array かどうかなら上記オブジェクト判定でできるが、「配列操作ができるオブジェクトかどうか」(例: document.getElementsByTagName() の返値) かどうかは一概にはわからない。

obj = [1,2,3]
Object.prototype.toString.call(obj) // => "[object Array]"

obj = document.getElementsByTagName("body")
Object.prototype.toString.call(obj) // => "[object HTMLCollection]"

あまり完全な方法はないらしいのだが、Collection はだいたい item と length の両プロパティを持つので、その有無で判定するというのが一番一般的であるそうな( thanks id:ZIGOROu )。

// 簡易版
if (typeof obj=="object" && "length" in obj) {
  // 配列っぽい場合の処理
}

// もう少し厳密に
if (Object.prototype.toString.call(obj)=="[object Array]" || 
    (typeof obj=="object" && "length" in obj && "item" in obj)) {
  // 配列っぽい場合の処理
}

本家 Array が item プロパティを持たないので、少し厳密さが欲しい場合は Array とそれ以外で判定方法を分ける必要がある。


JSRuby ではこれら Collection 類を配列に変換したくてこのあたりのことを試したのだが、DOM Element の style 属性 ( CSSStyleDeclaration ) もこの判定に引っかかって配列に変換されてしまい……
仕方なく今のところは Object.prototype.toString.call() が「この値とこの値の時は配列と見なす」という列を持たせてしまっている。ううむ、弱い……