プリミティブはオブジェクトじゃないし、プリミティブと参照なんて区別はない
404 Blog Not Found:javascript - にはクラスはないの重箱の隅をつつきます。「JavaScriptにクラスはない」という表題の主張自体は合っていると思うので、重箱の隅。
最初気になったのは「参照」の間違いだったんですけど、調べるうちにいろいろと…
「JavaScriptにおけるほとんど全てのデータはオブジェクト」ではない
4 概要 (Overview)によると、プリミティブ値は
プリミティブ値 (primitive value) は Undefined, Null, Boolean, Number, String 型のうちの一つ
とあり、オブジェクト
オブジェクトは Object 型の構成要素である。序列のないプロパティの集合体で、それぞれのプロパティがプリミティブ値やオブジェクト、関数を含む
とは明確に区別されています。プロパティ(メソッドも含む)を持つのはオブジェクトだけで、プリミティブ値はプロパティを持ちません。
じゃあ、なぜ
p(true.toString()); p("String".toString()); p((1).toString()); p((3.14159265).toString()); p([0,1,2,3].toString()); p({q:"answer to life, the universe, and everything", a:42}.toString());
とプリミティブ値のtoString()メソッドを呼べる(ように見える)のか、というと…
11 式 (Expressions)
- MemberExpression を評価。
- GetValue(Result(1)) を呼出す。
- Expression を評価。
- GetValue(Result(3)) を呼出す。
- ToObject(Result(2)) を呼出す。
- ToString(Result(4)) を呼出す。
- 基準オブジェクトが Result(5) でプロパティ名が Result(6) である Reference 型の値を返す。
と、(MemberExpression[Expression]と解釈される)MemberExpression.ExpressionではMemberExpressionはオブジェクトに変換される、という仕様になっているからです。
つまり、実は暗黙にオブジェクトに変換されて
p(new Boolean(true).toString()); p(new String("String").toString()); p(new Number(1).toString()); p(new Number(3.14159265).toString());
とやるのとおんなじ意味になってるんですね。暗黙にオブジェクトに変換されるからメソッド(プロパティ)にアクセスできるだけで、プリミティブ型がメソッド(プロパティ)を持ってる訳では有りません。
ちなみに、プロパティアクセス式にあるtoObject(やtoString)は9 型変換 (Type Conversion)で説明用に定義された物で、実際にそういう関数が有る訳ではありません。
「プリミティブと参照」なんて分類はない
実際に型として「Reference 型」も登場するのでまたややこしいんですが…「プリミティブと参照」なんて分類はありません。
8 型 (Types)
値は実体であり、9 つの型のうちの 1 つを取る。型は 9 つ (Undefined, Null, Boolean, String, Number, Object, Reference, List, Completion) ある。Reference 型, List 型, Completion 型の値は式評価の中間結果としてのみ使われ、オブジェクトのプロパティに蓄積はできない。
まぁ内部的に使われるだけのReference, List, Completionおよび特殊な値であるUndefinedとNullは置いといたとして、「値には二種類ある - プリミティブとオブジェクト」と言うべきなのです。
ちなみに、Reference 型はまた少し違った話になります。これはプロパティ(や変数)に代入可能な物ではないので、後ほど。実はこれが凄く面白かったんで調べこんだんですが;-p
閑話休題。
で、その「参照」の話なんですが、
a = bとしてから、bにだけ改変を加えた場合、プリミティブではbだけが変わっていますが、参照ではaも変わってしまっています
と言っている例が変なんです。これが読んですぐに気がついた間違いで、Javaにおいて「Stringだけ参照渡しじゃない、同じオブジェクトなのに特別扱いだ」なんて言う勘違いと本質的には一緒だと思います。
はしょりつつ例を引用すると…
プリミティブの例:
var a = 0; var b = a; p('a is ', a, ', b is ', b, '.'); b++; p('a is ', a, ', b is ', b, '.');a is 0, b is 0. a is 0 + b is 1.
参照の例:
var a = {x:'foo', y:'bar'}; var b = a; p('a.x is ', a.x, ', b.x is ', b.x, '.'); b.x = 'baz'; p('a.x is ', a.x, ', b.x is ', b.x, '.');a.x is foo, b.x is foo. a.x is baz, b.x is baz.
と、「プリミティブはbを変更した後にaを見ても変わってないけど参照はbを変更すると変わってるよね」とやってるのですが大きな間違いです。
プリミティブの例と参照の例で違う事をやっているだけです。
プリミティブの例でやっている事をオブジェクトでやってみましょう。(プリミティブと参照という区別は間違いで、プリミティブとオブジェクトです。)
var a = new Number(0); var b = a; p('a is ', a, ', b is ', b, '.'); b++; p('a is ', a, ', b is ', b, '.');
a is 0, b is 0. a is 0 + b is 1.
プリミティブのときと全く同じ結果です。++演算子は左辺値から値を取得し、プリミティブのNumberに変換し、それに1を加算し、その結果を左辺に再代入する物です。値そのものを変化させる訳ではなくて、左辺にある変数なりプロパティを変更するだけです。
で、今度は「参照の例」でやっている事をプリミティブに対してやってみる…事が出来ると良いのですが、プリミティブにはプロパティがないので、出来ません。プロパティを持っているのはオブジェクトだけですから。
プロパティを変更すると同一のオブジェクトを持つ別の変数にも伝播する事からオブジェクトはいわゆる「参照」と同じ振るまいをしますが、プリミティブの場合プロパティが(破壊的な操作も)ないのでプリミティブがいわゆる「参照」かどうかを調べる事は出来ません。(Reference型は存在するのでちょっと迂遠な表現になってます^^;)
が、実は少なくともString(javascriptではプリミティブです)も「参照」じゃないかと思います。可変長の文字列の配列を作れますから。実際、以下のような実験をしてみると
<div id="out"></div> <script type="text/javascript"> function bench() { var str = "abcdefgijklmnopqrstuvwxyz" for (var i = 0; i < 20; i++) {str += str} document.getElementById("out").innerHTML = "String:" + getPutTime(str) + ", Object:" + getPutTime(new String(str)) } function getPutTime(val) { var l = new Array(1000000) var start = new Date().getTime() for (var i = 0; i < l.length; i++) {l[i] = val} return new Date().getTime() - start } </script> <button onClick="bench()">test</button>
プリミティブのStringとStringオブジェクトでコピーの時間がほぼ一緒です(safari4とcamino1.6.7とchromium 3.0.184の場合)。
26*(2^20)文字で27262976文字あるので、もし値だとすると結構なコストになると思います。まぁ一定の範囲を一気にコピーするのは凄く速かったはずなので、一概には言えないんですが…
さらに、safariではstrを作るところのループは20から24に変える(2^4で16倍の長さ)だけでout of memoryになりますが、ベンチマークのArrayはサイズを100000000(100倍)にしてもout of memoryにはなりません。少なくとも配列にはプリミティブの文字列も参照が入れられている事になります。
変数やプロパティについても、(たぶん)サイズの違うBooleanやStringやNumberが入れられるので、おそらく即値が入っている訳ではないでしょう。
おまけ:JSのReferenceはちょっと違う
さて、おまけで面白かったReference型について、ちょっと書いておきます。
8 型 (Types)
Reference は、オブジェクトのプロパティへの参照である。Reference は基準オブジェクト(base object) とプロパティ名の 2 つの成分から構成される。
メモリ上のどこにある値、とか、そんな定義じゃないんですね。「どのオブジェクトのなんてプロパティ」がECMAScriptにおける「参照」なんです。ちなみにメソッドもオブジェクトのプロパティでしかないので、参照に入れる事が可能です。
じゃあグローバル変数や関数や変数はどうなるの?と調べてみると…10 実行コンテキスト (Execution Contexts)や10 実行コンテキスト (Execution Contexts)を見るとスコープもオブジェクト、グローバルもオブジェクトでグローバル変数や関数やローカル変数もそれらのプロパティなんですね。
というわけで識別子は実は全てオブジェクトのプロパティになってる訳です。違う意味で「全てオブジェクト」なのでした。
(与太話)JavaScriptにもClassは有る!
プロパティ パラメータ 説明 [[Prototype]] 無し このオブジェクトのプロトタイプ。 [[Class]] 無し このオブジェクトの種類。 (以下略)
なんと!内部プロパティにはClassが有るじゃないですか!…
Host オブジェクトの[[Class]] プロパティの値は任意の値でよく、組込み オブジェクトからその [[Class]] プロパティのために使用される値でも例外ではない。 この仕様はプログラムに [[Class]] プロパティの値へのアクセスを意味するものは何も提供しないことに注意; これは組込み オブジェクトの種類の違いの区別に内部的に使用される。
て組込みのオブジェクト専用の内部プロパティですか…たぶんC++(やその他の実装コード)のクラスとひもづけて、動作を高速化するための物なんでしょう。
ちなみに、Hostオブジェクトとはユーザ定義のオブジェクトとかDOMのオブジェクトとか、組込みオブジェクト以外のオブジェクト全部を指すみたいです。そのその他大勢についてはClassは適当でかぶっても良い、て事…なんでしょうか??
少なくとも、「Classはない」と言ってるClassとは少し毛色が違うポイですし、JavaScript言語には現れない物です。「Classは有ったんだ!」なんて真に受けないでくださいね!-)