Kokudoriing

技術系与太話ブログ

typeof やら instanceof やら toString.apply やら

jQuery や underscore.js では isFunction とか isArray とかの型判定関数的なものがあります。
え?ライブラリ使わないとJavaScriptはろくに型すら判別できないの?

半分YESで半分NO。
そもそも typeof とはなんぞやというお話し。

よく typeof と instanceof の違いについて、
「typeof は型の文字列表現を返して、instanceof は型から派生されたかどうかを返すから戻り値は違えどやってることは一緒」
的な事を耳にしますが全くもって違います

まず、JavaScript で言う型とは何か?
JavaScript はクラスを持ちません。よって型を作れません。

え? new Object() とかできるけど?
var obj = new Object() として時の obj は確かにObject型のインスタンスオブジェクトです。
しかし、var array = new Array(100) とした時の array もObject型のインスタンスオブジェクトです。

(function(undefined) {
  console.log(typeof new Object()); // output 'object'
  console.log(typeof new Array(100)); // output 'object'
}).apply(this);

!?

JavaScript 上に存在する型は全部で6種類です。
Number, String, Boolean, Null, Undefined, Object の計6種類。
つまり typeof 演算子は型の文字列表現を返しているわけなので、
typeof 演算子が 'array' と返すはずがないわけですね。(JavaScript に Array なんていう型はない。)

さて、ますます混乱してきました。ますます JavaScript が嫌いになってきました。
次に instanceof 演算子を見てみましょう。

(function(undefined) {
  console.log(new Object() instanceof Object); // output true
  console.log(new Array(100) instanceof Array); // output true
}).apply(this);

そう!その反応が見たかった!
恐らくこの挙動は想定していた挙動と同じ挙動だと思います。非常に自然ですね。

じゃあ instanceof は何を判断するための演算子なのか。
instanceof はインスタンスオブジェクトがどのコンストラクタから生成されたかを判断するための演算子です。
つまり、instanceof 演算子と型には直接的な関係性はありません。


つまり、JavaScript において型とコンストラクタは直接的に関係しないわけです。
そして、型はユーザーが自由に定義できないが、コンストラクタはユーザーが自由に定義可能です。

ちょっと複雑なのでいくつか例を出してまとめてみます。

new Object() はObject型でObjectコンストラクタから生成されたインスタンスオブジェクト。
new Array(100) はObject型でArrayコンストラクタから生成されたインスタンスオブジェクト。
new RegExp('hoge') はObject型でRegExpコンストラクタから生成されたインスタンスオブジェクト。
new String('piyo') はObject型でStringコンストラクタから生成されたインスタンスオブジェクト。

えっ

new String('piyo') はObject型?
でも JavaScript には String 型があります。なのにObject型?

(function(undefined) {
  //typeof
  console.log(typeof new String('hogehoge')); // output 'object'
  console.log(typeof 'hogehoge'); // output 'string'

  //instanceof
  console.log(new String('hogehoge') instanceof Object); // output true
  console.log(new String('hogehoge') instanceof String); // output true
  console.log('hogehoge' instanceof Object); // output false
  console.log('hogehoge' instanceof String); // output false
}).apply(this);

つまり、'hogehoge' と new String('hogehoge') は別物です。
これは恐らく Java の経験がある方はピンと来るかと思いますが、
new String('hogehoge') はボクシング(boxing)のためのラッパオブジェクトです。

ボクシングについてここでは詳細に説明しませんが簡単に説明しますと、
'hogehoge'.toString() は new String('hogehoge').toString() と自動変換され、これをオートボクシングと言います。
JavaScript ではObject型以外はメソッドやプロパティを持つことが出来ません。
なので、Object型以外(とUndefined, Null以外)の型はそれぞれ対応するObject型のラッパコンストラクタを持ちます。
これらがバックグラウンドで自動的に行われるので、何も考えずに "hogehoge".toString() を呼び出せるというわけです。

さて、これでラッパコンストラクタによって生成されたインスタンスオブジェクトの方はなんとなくわかったかと思います。
そして、'hogehoge' instanceof String は失敗することもなんとなく理屈が通るような気がします。
では 'hogehoge' はどのコンストラクタなら instanceof が true を返すのでしょうか?
つまり、'hogehoge' はどのコンストラクタから生成されたのでしょうか?

結論を言うと、'hogehoge' はどのコンストラクタとも instanceof が true になることはありません。
'hogehoge' はコンストラクタから生成されていないという事になります。
これは Number と Boolean にも同様に言えます。

つまり、Object型以外のインスタンスオブジェクトは全てコンストラクタから生成されていません。
よって、Object型以外のインスタンスオブジェクトに instanceof を使用してももれなく false が返されます。

これで、typeof が型を、instanceof がコンストラクタを調べる演算子だという事を意味がわかったかと思います。

ちなみに Undefined と Null にはラッパコンストラクタがありません。
これは null.toString() や undefined.toString() といった事が出来る事を禁止したい目的のためです。
これを実際に動かすと TypeError が投げられます。
これがもし何も例外を発生させなければ、恐ろしくデバッグが難しかったでしょう。

ということは、String, Number, Boolean 以外のラッパコンストラクタっぽいものの存在意義は何でしょう?
new Array() なんてしなくても [] というリテラル表現があるわけです。
そして、これは現在ほとんど意味をなしていません。
JavaScript パターン」でもリテラル表現があればコンストラクタではなくリテラル表現を使用するように言っている。

JavaScriptパターン ―優れたアプリケーションのための作法

JavaScriptパターン ―優れたアプリケーションのための作法

この場合のコンストラクタを避けると言う意味は String, Number, Boolean も含んでいる。
new String('hoge') が役に立つのはボクシングの時のみであり、それはJavaScriptが勝手にやってくれる。
我々が文字列を作りたいのであればおとなしくリテラル表現を使用するべきです。

さて、一番最初の質問。
なぜJavaScriptには typeof やら instanceof やらがあるのに各種ライブラリは独自の型判別メソッドを用意しているのか。

まず第一にプログラマが予想する型とJavaScript上での型には明確な乖離があるということ。
大抵のプログラマは配列かどうかを調べることと数値がどうかを調べることの意味を区別しない。
しかしJavaScript上ではそれらはコンストラクタと型という明確な区別を行なっているのでわかりにくい。

わかりにくいのでJavaScriptではそれらを一緒くたに扱える方法が存在する。

(function(undefined) {
  var toString = Object.prototype.toString;
  console.log(toString.apply(new String('hogehoge'))); // output '[object String]'
  console.log(toString.apply('hogehoge')); // output '[object String]'
  console.log(toString.apply(new Array(1,2,3))); // output '[object Array]'
  console.log(toString.apply([1,2,3])); // output '[object Array]'
}).apply(this);

Object.prototype.toString.apply(obj) とすると、
'[型名 コンストラクタ名]'という形式の文字列が返ってくる。

ここで、Object.prototype.toString.apply('hogehoge') も型名として object を返している。
しかし、これは先程のオートボクシングが影響しているだけだ。
つまり、Function.prototype.apply(もちろんcallも) はObject型を期待しているので、
Object型以外の型のインスタンスオブジェクトがくるとオートボクシング機能が働く。
つまり、Object.prototype.toString.apply('hogehoge') と
Object.prototype.toString.apply(new String('hogehoge')) の差は殆ど無い。

これで型を気にせずにコンストラクタのみを気にすることが出来、
プログラマにとって自然な結果が返ってくるようになる。


が、typeof 演算子はかなり謎な挙動も数多く起こしている。
まず、 typeof null が 'object' を返してくる。
null はNull型なのでこれはどう考えてもおかしい。
そして、これはただの仕様バグという悲しいオチ。

次に、typeof function() {} は 'function' を返してくる。
function() {} はFunctionコンストラクタから生成されたObject型のインスタンスオブジェクトだからこれはおかしい。
ただ、何故こうなったのかはちょっとわからないのでご存知の方がいらっしゃれば教えて頂ければ幸いです。

また、typeof new RegExp('hoge') が 'function' を返す実装もあるそうです。
本当にやめて欲しいです。
これも理由がわからないので教えて頂ければ幸いです。

ちなみに instanceof はコンストラクタを指すので、
new をラッパして Factory パターンとかする際には new する時のコンストラクタにしか反応しません。
この時にそのコンストラクタをクロージャで完全に隠してしまうと instanceof がほとんど意味なくなります。


結論。
typeof 糞だわ。
Object.prototype.toString.apply 使いましょう。
jQuery 使ってる人は何も考えずに $.isXxx 系の関数使いましょう。