typeof演算子から学ぶJavaScriptのデータ型の概念と関係する考察のまとめ

まずはtypeof演算子について。JavaScript Gardenというウェブページがありまして。こういう一文がありまして。

The typeof operator (together with instanceof) is probably the biggest design flaw of JavaScript, as it is near of being completely broken.

http://bonsaiden.github.com/JavaScript-Garden/#typeof

訳)http://efcl.info/adiary/Javascript/JavaScriptGarden#k95p17


要するにtypeof演算子は「ヒャッハーtypeof演算子ぶっ壊れてるぜー」ってことらしい。理由が下記の表。

[表1]
Value               Class      Type
                                                                        • -
"foo" String string new String("foo") String object 1.2 Number number new Number(1.2) Number object true Boolean boolean new Boolean(true) Boolean object new Date() Date object new Error() Error object [1,2,3] Array object new Array(1, 2, 3) Array object new Function("") Function function /abc/g RegExp object (function in Nitro/V8) new RegExp("meow") RegExp object (function in Nitro/V8) {} Object object new Object() Object object

比較するとこの表からは一貫性が感じられない。よって、typeof演算子が使える唯一のケースはtypeof foo === 'undefined'だけ、ということらしい。というかキモいらしい。う◯こらしい。巻◯糞らしい。実はピーでピーなピーらし(自主規制

本当にそうなのか?

私は「ぶっ壊れてないと思うし使えるけど扱いがややこしい演算子」だと思う。でも使うなってほどbadな演算子でもない。


この表から読み取れる「一貫性が感じられない」ということを一番伝えたい部分は、

[表2]
Value               Class      Type
                                                                        • -
"foo" String string new String("foo") String object 1.2 Number number new Number(1.2) Number object true Boolean boolean new Boolean(true) Boolean object

この部分だと思います。

他にもFunctionだけTypeがfunctionなのはなんでだとか、new Array(1,2,3)がobjectなのはなんでだとか、v8のRegExpがfunctionとかどないやねんとか、nullやundefinedどこいったとか人によって思うことはまちまちあるでしょう。

でも内容が複雑になるのでここに要点を絞って考えてみます。

もう一度言うけれど、私はtypeof演算子はぶっ壊れていると言われるほど破滅的なものではないと思う。でも人によっては直感的でない部分はあるかもしれない。

細かいことでどうでもいいといえばどうでもいいことなのだけど、それぞれ役割というものがありできることとできないことがあります。typeof演算子は何のために存在するのかをはっきりさせ、また、JSにおけるデータ型の概念の本質を探ってみたいと思います。

JSのデータ型を復習

基本的なことですがJSにおけるデータ型は、String型、Number型、Boolean型、Null型、Undefined型、Object型とあり、それぞれ基本型(プリミティブ値や基本データ型とも称される)と参照型(オブジェクト型とかオブジェクトとも称される)に分けることができます。

分け方はString型、Number型、Boolean型、Null型、Undefined型が基本データ型にあたり、Object型が参照型です。配列、関数、オブジェクトなどはObject型に含まれます。(言葉の定義はそれぞれ違うのであれですが、概念的に整理しやすい言葉を選んでます)

全てのデータがオブジェクトというわけではない

というわけでJSにはプリミティブ値があります。全てのデータが何かのオブジェクトというわけではありません。

"foo".lengthとアクセスすることができるのでStringオブジェクトに見えますが、これは内部的に(暗黙的に)ラッパーオブジェクトを生成している(厳密には内部でToObjectを呼び出してオブジェクトに変換したもの)のでプロパティやメソッドにアクセスできるだけで、アクセスが終了すれば廃棄され文字列に戻ります。

オブジェクトのように振る舞うだけで本質的なオブジェクトではありません。

これはサイ本など熟読している人には当たり前のことですが、案外伝わっていないことだったりしますね。

そしてこれらの現象はNumber型、Boolean型も同じ。つまりプリミティブ値のうちnullやundefinedを除くラッパーオブジェクトとしてStringオブジェクト、Numberオブジェクト、Booleanオブジェクトが用意されていて、必要な場合に暗黙的に利用されるわけです。

プリミティブ値以外のデータ型はObject型

基本的にプリミティブ値以外のデータ型はObject型となります。プリミティブ値以外全てオブジェクトなんで当然といえば当然です。

様々なオブジェクトのデータ型は全てObject型としてまとめられているので、Array型やRegExp型といったようなオブジェクトのデータ型は実は存在しません。new Array()もnew RegExp()で生成されたオブジェクトのデータ型もObject型となります。

ラッパーオブジェクトでも同じこと

当然ラッパーオブジェクトも例外ではありません。ラッパーオブジェクトは暗黙的に利用されることを基本としていますが、他の組み込みオブジェクトと同じく明示的にコンストラクタによってオブジェクトを生成することができます。

その場合も当然生成されるのはオブジェクトなのでデータ型はobject型です。

[表3]
Value               Class      Type
                                                                        • -
[プリミティブ値] "foo" String string 1.2 Number number true Boolean boolean [ラッパーオブジェクト] new String("foo") String object new Number(1.2) Number object new Boolean(true) Boolean object

そういった背景から考えると、表のようにコンストラクタで生成されたデータ型(Type)がobjectになるのは健全だと言えます。

それでも納得できないのであれば、ここでもし仮にコンストラクタによって生成されたオブジェクトのTypeが直感的に正しいと思われるstringやnumberを返すことにしましょう。

[表4]
Value               Class      Type
                                                                        • -
[ラッパーオブジェクト] ↓ohh!yes!yes!( ´,_ゝ`) new String("foo") String string new Number(1.2) Number number new Boolean(true) Boolean boolean


もちろんString型やNumber型はJSではプリミティブ値のデータ型です。なので、new演算子を用いてコンストラクタで生成されたオブジェクトは「プリミティブ値」ということになりますね。










・・・Pardon?( ´,_ゝ`)

データ型とオブジェクトの種類は違う

とまあそういうことになれば、JSとしては言語的にも仕様的にもかなりいかがわしいものになってしまいます。

当たり前ですが、String型やNumber型というのはオブジェクトのデータ型ではなくプリミティブ値としてのString型やNumber型です。まさにドがつくほど当たり前のことなのです。

でもどこかでオブジェクトとしてのString型やNumber型といったように、無意識にクラス型のような概念と混じって解釈しているとやっかいです。

何度も言いますが、typeof演算子が返すString型やNumber型は「プリミティブ値としてのデータ型」です。クラスに属するオブジェクトの型を示しているわけではありません。

そして表で言うClassとはオブジェクトの内部プロパティにあたる[ [Class] ]というプロパティです。この内部プロパティは主に組み込みオブジェクトの種類を区別するためのプロパティであって、一般的な意味でいうClassとは違います。

だからデータ型と[ [Class] ]を比較するのはおかしい

こういった理由から表のようにTypeと[ [Class] ]と比較するのはそもそもおかしいのです。Typeとはデータ型のことで、[ [Class] ]とは上述した通りデータ型のうちObject型にあたるオブジェクトの種類を示す内部プロパティだからです。

一見すると同じような性質を持ったものに見えるものですが、このように考えると別ものだということが分かります。

typeof演算子の役割とはデータ型を返す演算子

見出しのような説明はどのリファレンスでもだいたい書かれていますが、少し深く考えないとその意味が読み取れないというのはどういうことなんでしょう。まあいいや。まとめ。

問題として挙げた表を見てみると、

[表2]
Value               Class      Type
                                                                        • -
"foo" String string new String("foo") String object 1.2 Number number new Number(1.2) Number object true Boolean boolean new Boolean(true) Boolean object

Typeと[ [Class] ]で比較すると一貫性がないように思えます。しかし、そもそもそれらは比較することのできないものです。

なぜなら、typeof演算子はデータ型がString型、Number型、Boolean型、Undefined型、Object型のうちどの型に当てはまるのかを返す演算子で、[ [Class] ]とはオブジェクトの内部にあるオブジェクトの種類を区別するためのプロパティだからです。

表の[ [Class] ]の項目はObject.prototype.toStringを汎用的に呼び出すことでオブジェクトから取得できます。つまり、次のようにそれぞれTypeと[ [Class] ]を取得したと思われます。

var foo = "bar";

// Type
typeof foo;                          // string

// [[Class]]
Object.prototype.toString.call(foo); // [object String]

そしてコンストラクタの場合。これが重要ですが、

var foo = new String("bar");

// Type
typeof foo;                          // object

// [[Class]]
Object.prototype.toString.call(foo); // [object String]

こうなります。typeof演算子が返しているのはデータ型としてのobjectです。

そしてObject.prototype.toStringが返しているのはデータ型がObject型のオブジェクトのうち、Stringオブジェクトだというオブジェクトの種類を意味する文字列です。([object String]って思いっきり書かれているのでそのままなのですが)

この通り2つの値が意味するものは異なります。


そういうわけでtypeof演算子とはオブジェクトの種類ではなくデータ型を返す役割を果たすという、ごく当たり前な演算子です。オブジェクトの種類を返すと思って利用しないよう注意したいですね。

ここで、一番最初の表をtypeof演算子向けにいくつか付け足して並べ替えます。実はあれは横に比較してみるのではなく、縦にみるべきです。あとtypeof演算子向けの表としていくつかつけたします。

[表4]
Value               Class      Type
                                                                        • -
[基本的なプリミティブ値] "foo" String string 1.2 Number number true Boolean boolean [コンストラクタ] new String("foo") String object new Number(1.2) Number object new Boolean(true) Boolean object new Date() Date object new Error() Error object new Array(1, 2, 3) Array object new RegExp("meow") RegExp object (function in Nitro/V8) new Object() Object object new Function("") Function function [オブジェクトのリテラル] [1,2,3] Array object /abc/g RegExp object (function in Nitro/V8) {} Object object function(){} Function function (つけたし) [nullとundefined] null object (つけたし) undefined undefined (つけたし)

縦に見ればなんらかの一貫性があるということが確認できるはず。あと具体的にtypeof演算子がどのようなデータ型を返すのか対応表も載せておきます。

Type Result
Undefined "undefined"
Null "object"
Boolean "boolean"
Number "number"
String "string"
Object (native and doesn't implement Call) "object"
Object (native and implements Call) "function"
Object (host) Implementation-dependent

以上のことからtypeof演算子はデータ型を調べる演算子で、もっといえば「データ型がプリミティブ値かどうかを調べる演算子」と言って良いでしょう。

そもそもオブジェクトはObject型と括っているわけですから、オブジェクトなんてアウト・オブ・眼中です。typeof演算子からすればデータ型なんて「プリミティブ値とその他の愉快なオブジェクトたち」なのです。偉そうですこいつ。

何はともあれオブジェクトの種類を知りたいときはObject.prototype.toStringを利用しましょう。


これらの使い分けが面倒ととらえるかどうかは人それぞれです。考えるのが面倒であればObject.prototype.toStringだけでも良いと思います。これらの言い分に関しては自由ですからね。

そんなわけで、評価対象のデータ型を返すという本来の役割から考えればtypeof演算子はぶっ壊れていないと思います。プリミティブ値への利用なら使えるでしょう。

ちなみに表を見ても分かる通り少しおかしい点がいくつかありますが、ここではそこが論点ではないので後述しています。


そして私はtypeof演算子が好きではないという結論が出たところで終わり( ´,_ゝ`)

今回は視点の発想をtypeof演算子からJSにおけるデータ型の概念をとらえてみようということで考えてみました。以上ですが、おかしなとこあればコメントください。

その他いろいろ気になったこととか補助的なこととか適当に考えてまとめておきます。分けるのめんどくさい。

型について

http://www2u.biglobe.ne.jp/~oz-07ams/prog/ecma262r3/8_Types.html

この定義を使いました。本やウェブサイトだと関数や配列もデータ型として扱っており、参照型に含ませている場合が多いのですが、今回はtypeof演算子の本質というわけで厳密な定義を使いました。

配列も関数もデータ型と認識した方が分かりやすいとは思いますが、今回ばかりはしょうがないです。

ちなみにこの記事はES3ベースです。一応ES5も見出し程度で確認したところデータ型に関しては大きな変更はなかったと思います。(こういう変化はある。http://d.hatena.ne.jp/Constellation/20101205/1291564928

http://people.mozilla.org/~jorendorff/es5.html#sec-8

データ型とオブジェクト

これまでの文章を見て頭痛くなったJSに慣れていない人用に、データ型とオブジェクトの関係を図にしてみました。

早い話が左側のデータ型がtypeof演算子が返す型の名前です(functionは定義の都合上含めていません)。そして汎用的なtoStringメソッドは、データがどのオブジェクトに属するかを返すわけですが、それがビルトインオブジェクトのそれぞれオブジェクト名です。(厳密にはそのオブジェクトの[ [Class] ]プロパティにある値)

文章だといろいろ芋臭くなりましたがこのようにまったく性質が違うということが分かると思います。

まあデータ型とオブジェクトの種類の関係性が不透明というか整理されていないから、ややこしい印象を与えてしまっているんじゃないかなと思います。

あと文字については精一杯でした。勘弁してください。

typeof演算子とObject.prototype.toStringの使い分け

プリミティブ値かどうかのときは素直にtypeof演算子。Object.prototype.toStringは対象のオブジェクトが不明なときとか使うと良いと思う。

var type = (function(toString, types){
  var l = types.length, _types = {};
  while((--l) >= 0){
    (l < 8) ? 
      (_types['[object ' + types[l] + ']'] = types[l].toLowerCase()) :
      (_types[types[l]] = types[l].toLowerCase());
  }
  return function(obj) {
    if(obj == null) { return obj + ''; }
    if(obj.nodeType) { return _types['Node']; }
    return _types[toString.call(obj)] || 'object';
  }
})(Object.prototype.toString,
  'Boolean,Number,String,Function,Array,Date,RegExp,Object,Node'.split(','));

type([]); // 'array'
type(new RegExp()); // 'regexp'
type(1); // 'number'
type(null); // 'null'
type(document); // 'node'

プリミティブ値以外はじくといったときにわざわざtoStringを使うことはないと思うけど。面倒だったら上の方法だけ使っても別に良いでしょう。

明示的にコンストラクタで生成するケースはあまりない

少なくとも文字列、数値、真偽値のコンストラクタはまず使わない。

// オブジェクトとして生成してもほとんど意味がないので使わない
new String("foo")
new Number(1.2)
new Boolean(true)

// Errorオブジェクトはほぼ触らない
new Error()

// new Array(1000)とかならあるかも
new Array(1, 2, 3)

// new Function("return window;")()みたいな黒魔術が
new Function("")

// 通常は/meow/のようにリテラル使うと思う
new RegExp("meow")

// これだけなら{ }を使う
new Object()

利用するにしても目的を持って利用します。なんとなくで使わないでしょう。

ちなみにプリミティブ値の場合オブジェクトとして生成するとどうなるのかというと、

console.log(new String('abcde')); // abcde { 0="a", 1="b", 2="c", 3="d", 4="e"}

みたいなオブジェクトになってます。でも、文字列として扱うときは文字列になってくれます。

console.log(new String('abcde') + 'fg'); // abcdefg

valueOfやtoStringが呼び出されて文字列として変換してくれてるんですね。これは不思議でたまらんかった。

valueOfとtoStringメソッドの水深43cmぐらいの深さの話 - 三等兵
valueOfとtoStringとToPrimitive - os0x.blog


このようにStringオブジェクトでも扱いやすいよう配慮してくれているんですが、何があるか分かったもんじゃないので意味なく使わない方が良いです。

そういうわけでjQueryとかで、

$.each(['abc', 'def', 'ghi', 'jel'], function() {
    console.log(this); // abc { 0="a", 1="b", 2="c"}...
});

thisを使うと要素の文字列がStringオブジェクトになるので注意。callでcallback呼んでるのでそりゃこうなりますね。あとnewを使用しないコンストラクタの場合、というか関数として実行した場合そのコンストラクタに依存します。

String(1234); // '1234'
Number('1234'); // 1234
Boolean(1234); // true

といったように型変換の形になります。速度重視だと演算子で済ますケースが多いと思いますが、使ってもいいと思います。でも、

Boolean('false'); // true

みたいなトラップが潜んでいるので気をつけてください。

データを全てオブジェクトと解釈することの障害

JSはデータが全てオブジェクトだという認識でもだいたい大丈夫。そのように配慮されている。正確にはJSがではなくECMAScriptがですが。

そしてJavaScript Gardenでの扱いを見る限りでもそう解釈し操作するように最適化しているように見受けられます。しかし、例えばそういった認識だと演算子を誤った方法で利用してしまうことがあります。

new String('foo') instanceof String; // true
new String('foo') instanceof Object; // true

// ↓誤った使い方
'foo' instanceof String; // false
'foo' instanceof Object; // false

プリミティブ値なんてしらねーよという意識だとこのような使い方をしてしまいます。プロパティやメソッドにアクセスしていないとただの文字列値だからfalseになるのは当たり前なのですが。

そういうわけでプリミティブ値に対してinstanceof演算子使うというのは結構恥ずかしいことです。そして私はこの恥ずかしい思いをしたことがあるわけで、誰しも通ることがある道なんだと気をつけてください。




べ、別にあんたのために忠告したわけじゃないんだから勘違いしないでよねっ///

typeof演算子は式を評価する演算子である

たとえば、

typeof(new String('foo'));

といったように関数みたい使えるのだけれど、そもそも演算子なので通常はいりません。そして関数みたいに使っているわけでもありません。

結局どういうことになっているのか他の演算子で例えるなら、

+(new String('100'));

こんなイメージだと思います。

でもtypeof演算子は式を評価するわけですから、

typeof (new String('100') + new Number(100) + 100); // 'string'

こういった形はあると思います。実践的なコードではありませんが。たとえば括弧の位置をかえると、

typeof (new String('100') + new Number(100)) + 100; // 'string100'

こんな感じになります。括弧使いたいです姉さん。演算子と散々言ってきて今更ですが、関数じゃないってことだけおさえておきたいですね。

nullとfunctionとNitro/V8でRegExpがfunctionな件

typeof null === 'object'のわけ

どっかの高尚な人が答えを出しているのかもしれませんが、なぜこのような仕様なのでしょう。typeof演算子はデータがReference Typeかどうかでobjectを判断しているようですが、nullの場合どうでもええからobjectにしとけって感じなんですね。

概念的な面での配慮かなーというのが今のところの結論なのですが。あ、いやいや、だったらnullじゃないか。そっちの方が筋が通っている。

ES5もnullはobjectなんですが互換かなこれは。今更変えられないよなあ。

typeof function(){} === 'function'のわけ

functionはObject型なんですがtypeof演算子を使うと特別にfunctionと返してくれます。しかしながら型は定義上Object型です。Function型はありません。でもfunctionと返します。

なんでこんな特別扱いなんでしょう。それじゃあArrayもarrayって返してくれてもいいんじゃないか!ケチッ!と思います。ここでES3のtypeof演算子が返す値の表をもってきます。

Type Result
Undefined "undefined"
Null "object"
Boolean "boolean"
Number "number"
String "string"
Object (native and doesn't implement Call) "object"
Object (native and implements Call) "function"
Object (host) Implementation-dependent

Object (native and implements Call)であればobjectのうちfunctionとして返すらしい。[ [Call] ]プロパティを保有しているオブジェクトがfunctionで、そうでなければobjectなんですね。

それだけで区別できるなら分けた方が良いとそういう判断だったのでしょう。というか関数だからそりゃ判定したいよね。こうしたことが正解かどうか知りませんが。

objectとfnctionの違いをリテラルで判断しているとこういう発想には辿りつけないですね。概念的な理解は厳しい。

v8のRegExpインスタンスがfunctionなわけ
typeof new RegExp(); // function (Nitro &v8)

変な感じがしますがこれはあながち間違ってはいません。

var r = new RegExp(/([あ-る])あ?([あ-る])う?.+さ([あ-る])す.+つ([あ-る])([あ-る])/);
var str = "あいうえおかきくこさしすてそたちつてる";

r(str); // ["あいうえおかきくこさしすてそたちつてる", "あ", "い", "し", "て", "る"]

*「え、なにこの暗号?」
_「いいから実行してみなよ」
*「えーなにもう。じゃあ実行ボタンをポチっと」
_「・・・」
*「なんかでてきたけどこの”あいうえおかき・・・”って?意味分かんないよもう」
_「それのうしろみてみなよ」
*「え?うん。んーと、"あ", "い", "し", "て", "る"・・・、あいしてる?」
_「うん、結婚しよう」
*「きゃ。ポッ」


ポッ、じゃねーよぼけええええ!


というわけでデフォルトでexecメソッドが呼び出されるようになっています。

型に関してはFirefoxも以前はfunctionと返していたけれど、今はobjectになっていますね。ちなみにRegExpオブジェクトには[ [Construct] ]という内部プロパティがないのでnewできません。

new /po/(); // TypeError: function is not a function (v8)
new /po/(); // TypeError: /po/ is not a constructor (firefox)

関数であって、関数ではない・・・か。v8は哲学的思想をお持ちのようだ。深い。

関数は関数じゃないもん!ポッ。とおっしゃっていますので関数は関数じゃない!と怒られないようにしたいですね。むしろ怒られたいですね。よくわかんないですね。ちなみにregexp.execと使った方が全然分かりやすいのでこっちをおすすめします。


あ、間違っても無許可で正規表現プロポーズなんてしようとしないでくださいよ!なんかどうでもいい文章を長々とかいて、「なにこれいみわかんない」とかつって今度は「これおしてみなよ」とかつって押したら文字がどんどんあらわれてきてそうなってこうなってああなんて「お父さんお母さん今までお世話になりました」的なロードは私に著作権があるので許可をとってください!著作権レッドブルをいただくからねっ!w

Object.prototype.toStringの引数にプリミティブ値が渡せる理由

引数に渡されたオブジェクトの[ [Class] ]を返すのがObject.prototype.toStringの役目です。

というわけで通常はプリミティブ値だといけないように思えますが、[ [Class] ]の取得のためにToObjectという内部でオブジェクトに変換できる関数を使ってオブジェクトに変換し、[ [Class] ]を取得します。

このようにプリミティブ値を引数として渡すと内部でオブジェクトが生成されその[ [Class] ]を返すもんですから、Stringオブジェクトの[ [Class] ]とデータ型としてのString型が同じように見えてしまいます。

実際はオブジェクトとしてのStringと、プリミティブ値としてのStringという質の違うものなんですが。

変数に型がないんだからいっそのことRubyみたいになってたら良かったのにね。Javaっぽくしなきゃいけなかった理由でもあるのか。Rubyがあるとかないとか以前にその当時はそれが普通だったのでしょうか。歴史を知らないので分かりませんが。

AS3.0でtypeof new String("foo") === "string"なわけ

気になってAS3.0を少し調べたんですが、見出しのとおりになるそうです。これが通るならデータは全てオブジェクトなのでしょうね。"string"とはStringクラスのオブジェクトということになるのかな。

ES4に準拠しているようなんでデータ型にプリミティブ値はあるでしょうが、事実上はオブジェクトと同じなのでしょう。

JavaScriptとは何か

typeof演算子が生きるための言語です。JavaScriptがあるからtypeof演算子があるんじゃない。typeof演算子があるからJavaScriptが存在するのです。

お互いが活かされているだなんて啓発的なことを言ってるinstanceof演算子とは違うのです。

「instanceof演算子ェ、つながりなんて大切なんじゃねえんだよ。おれがいることが大切なんだ!」と、typeof演算子はおっしゃっています。半端ないほどブリリアントです。

そいうわけでtypeof演算子は「半端ないほどブリリアント」という真の結論に達したところで終わり。