JavaScriptは「型のない言語だ」と言われています。これは、変数に型を宣言する必要がない(現状ではできない)ことです。しかし、データには型があります。よって、データの分類体系としての型システムもあるはずです。とはいえ、ホレッ、JavaScriptのことですから、型システムも“どうとでもとれるような(解釈の任意性がある)”曖昧な分類体系ですね。よくいえば融通無碍<ゆうずうむげ>、有り体<ありてい>にいえばエエカンゲンですわ。
●とっかかりはtypeof演算子
([追記]: typeof演算子をtypofと書き間違えていた(で、直した)。こういうのはtypoofと言うのだろう:-) )
typeof演算子(関数ではありません)の引数に、定数リテラル、変数、式(expression)などを指定するとその型(の名前である文字列)を返してくれます。
js> typeof 123
number
js> typeof ("Hello" + "World")
string
js> typeof new Date()
object
js> typeof function(x) {return x * x;}
function
js> typeof [1, 2, 3]
object
js> typeof typeof (2 * 3)
string
js>
typeofが返す種別は、 number
, boolean
, string
, object
, function
, undefined
の5つ6つ*1です。
「なーんだ、簡単じゃん、6つの型があるだけ」 -- でもないんです。
●型階層をとりあえず描いてみる
上の6種類では、いくらなんでも粗<あら>っぽすぎるでしょう。それに、typeof演算子が返す結果が、自然であるとも僕には思えないし。
型システムが言語仕様でキチンと定義され、型階層の絵(ツリー状、または網状)が描ける言語もありますが、JavaScriptでは、「これで決まり」という型階層はどうもないようです。で、とりあえず僕の主観で描いたのが下のツリー。つっこみどころはいくらでもあるんですが、その言い訳や説明は後(別な回も含む)でします。
JavaScriptデータ型
+--基本型
+--number型
+--boolean型
+--string型
+--オブジェクト的型
+--object型
+--Null型
+--配列型
+--狭義ブジェクト型
+--標準組み込みオブジェクト型
+--実装依存組み込みオブジェクト型
+--ユーザ定義(カスタム)オブジェクト型
+--function型
+-特殊な型
+--undefined型
typeof演算子に敬意を払って、“6つの種別”はそのままこの型階層に埋め込んでいます。その他の(日本語でラベル付けした)ノードは概念的な分類のために導入したものです。
●おまえ達はコウモリか? JavaScriptのデータよ
※注:コウモリ -- あるときは自分は鳥だと言い、あるときは獣だと主張した、という物語がある。
number型、boolean型、string型を基本型としてまとめることに異論はないでしょう。これらに共通する特徴は、(概念的には)値渡しであり、イミュータブル(変更不可能)なことです。文字列(string)に関しては、実際には参照(ポインタ)渡しをするでしょうが、文字列データを変更する手段がないので、理屈としては、値渡し(常に全バイトイメージがコピーされる)だと理解していいはずです。
文字列は変更不可能(イミュータブル):
js> var s1 = "Hello"
js> var s2 = s1.toUpperCase()
js> s1
Hello
js> s2
HELLO
js> s1[0]
H
js> s1[0] = 'h'
h
js> s1
Hello
js>
さて、ところで、[オブジェクト的型>object型>狭義ブジェクト型>標準組み込みオブジェクト型]をさらに細分すると、そのなかにNumber型、Boolean型、String型ってのが現れます。
string型とString object型:
js> var s1 = "Hello"
js> var s2 = new String("Hello")
js> typeof s1
string
js> typeof s2
object
js> s1 == s2
true
js> (typeof s1) == (typeof s2)
false
js>
リテラルとして書いた文字列はstring型なんですが、コンストラクタ関数Stringで生成したモノはobject型になります。s1とs2は型が違うのです。けど、等値比較演算子==で比較すると、s1とs2は等しいのです。ウーム、味わい深い。
さらに、Stringって関数はコンストラクタ(つまり、newと共に)ではなくて、単なる関数呼び出しもできまして、次のようなことになります。
単なる関数としてのString:
js> var s3 = String(s2)
js> typeof s3
string
js>
String関数の戻り値は、object型ではなくてstring型です。このような現象は、number, booleanについても同様に起こります。
●種明かし:君達はコウモリなんですね
上のような、“普通の”プログラマには不可思議な現象は、ラッパー・オブジェクトにより解釈できます。基本(プリミティブ)型のラッパー・クラスはJavaではお馴染みです。int型に対してInteger型、boolean型に対してBoolean型などです。Javaでは、String型は最初からオブジェクト型(参照型、クラスを持つ)なので特にラッパーを持ちませんけどね(これは重要な違い!)。
コンストラクタ関数Number、Boolean、Stringは、ラッパー・オブジェクトを生成するものです。ラッパー・オブジェクトから値を取り出すにはvalueOf()メソッドを使います。
ラッパー・オブジェクト:
js> var i = new Number(123)
js> typeof i
object
js> i.valueOf()
123
js> typeof i.valueOf()
number
js>
一方、単なる関数としてのNumber、Boolean、Stringはデータの型変換機能を提供します(他の言語のキャストに相当)。
データの型変換:
js> Number("123")
123
js> Number("")
0
js> Number(true)
1
js> Boolean(123)
true
js> Boolean("")
false
js> Boolean("false")
true
js>
さらに、「型が異なっていても値が等しくなる」ことは、JavaScriptでは日常茶飯事<さはんじ>。なぜなら、式や文のそれぞれの場所に“期待される型コンテキスト”があり、そこに期待されないデータがやって来ると、JavaScriptは勝手に、かなり強引に型変換を実行します(この点はPerlなんかと似てます)。この自動型変換が、演算子==の左と右に対してさえも実行されます。
ゆるい等値性:
js> 123 == "123"
true
js> false == ''
true
js>
もっときびしい等値性判定には===が使えます。この===を使って、==を解釈すると:
js> 123 === "123"
false
js> 123 === Number("123")
true
js> false === ''
false
js> false === Boolean('')
true
js>
いちおうツジツマがあっているでしょ。でも、JavaScriptのバージョンや方言により挙動が変わってしまうみたい(そこがまたJavaScriptの味わい)。
Javaでオートボクシング/アンボクシングが導入されたのは最近(J2SE5)ですが、JavaScriptでは、もっと以前から/もっと暗黙に/もっと強力に(迷惑なくらい強力に)自動型変換をしていたのです。よって、JavaScriptのデータは、あるときは鳥だと言い、あるときは獣だと主張するコウモリのようなモノですね。
●今回のまとめ
- typeof演算子は、
number
,boolean
,string
,object
,function
,undefined
のいずれかを返す。 - number型、boolean型、string型は、概念的に値渡しでイミュータブルだから基本型と考えてよい。
- number型、boolean型、string型に対応するラッパー・オブジェクト型として、参照型であるNumber型、Boolean型、String型がある、と考えてよい。
- 関数Number、Boolean、Stringは、ラッパ・オブジェクトのコンストラクトとしても、各基本型への型変換関数としても使える。
- JavaScriptは強力な自動型変換機能を持ち、それは、==の両辺に対してさえも実行される。
- 結果として、JavaScriptのデータには型があるような/ないような? コウモリ的なモノである。
データ型、型システムについてはもう少し言いたいことがあるので、たぶん、次回もこのネタです。
*1:数を間違えたのは、undefinedは型じゃないという印象があったからです。でも、特殊な型と考えることにします。