今回は値と参照について取り上げます。
突然ですが問題です。以下のJavaScriptプログラムを実行すると、何と表示されるでしょうか?
- プログラム:
- 出力:
- エラー:
答えは、"zero, 1, 2, 3"です。しかし、なぜaを直接変更していないのにaの中身が変わっているのでしょうか?
ここで、二行目に注目してみます。ここでは変数bに変数aを代入しています。変数aは配列です。ここだけ見ると、内部で起こっているのは以下のようなことに見えなくもありません。
array b -+ array b -+ | 0 | | 0 | | 1 | = | 1 | | 2 | | 2 | | 3 | | 3 | +--------+ +--------+
しかし、これは今観測した事実と明らかに異なります。実際はこうです。
a --------> array----+ = | 0 | b --------> | 1 | | 2 | | 3 | +--------+
すなわち、変数aも変数bも、配列そのものではなく、メモリーのどこかにある配列を参照している何か、ということになります。この「何か」を、プログラミングでは「参照(reference)」と呼びます。
これに対し、以下の例ではプログラムは「期待どおり」に動きます。
- プログラム:
- 出力:
- エラー:
ここにおけるaやbは、「値(value)」そのものに対応しているからです。
JavaScriptにおける値と参照の違い
このように、変数の中身が値なのか参照なのかというのは、プログラミング言語によって大いに異なります。JavaScriptでは、何が値として扱われ、何が参照として扱われるのかは、以下の表のとおりとなります。
値 | Boolean, Number, String, RegExp |
---|---|
参照 | Object, Array, Function, Date, その他すべて |
基本的には、内部構造を持つもの、すなわち変更可能なプロパティを持つもの全てが参照で、内部構造を持たない(atomic)ものは値、ということになります。例えばRegExpオブジェクトにはglobalなどのプロパティがありますが、そこに値を代入しても無視されます。Stringオブジェクトのlengthプロパティも同様です。
なぜ内部構造を持つデータは参照を通して扱うのか
これは、なんとも納得しづらい結果です。Cを知っている人であれば、文字列が参照(ポインター)でないことに首を傾げるでしょうし、Perlを知っている人であれば配列が値扱いでないことに面食らうかも知れません。
しかし、これらのこと「JavaScriptには、参照を参照として直接アクセスする手段がない」ことを考えれば、充分納得が行くことです。
え?納得していない。そうですよね。それではなぜ内部構造を持つデータは、JavaScriptに限らずほとんどの言語で参照として扱われるかを、アルゴリズムの観点から説明してみましょう。
ここで、あえて値として扱われる配列というものを考えてみます。この配列の中に配列を入れた場合、どうなるでしょうか、こんな感じになるでしょう。
[[0,1,2],[3,4,5],[6,7,8]] array ----+ | array--+| | | 0 || | | 1 || | | 2 || | +------+| | array--+| | | 3 || | | 4 || | | 5 || | +------+| | array--+| | | 6 || | | 7 || | | 8 || | +------+| +---------+
この配列の最初の要素である配列、[0,1,2]を、[0,1,2,-1,-2,-3]に変更したい場合、上の実装だと何がおこるでしょうか。単に最初の要素を入れ替えるだけではなく、他の要素までメモリーの別の場所に移動させなければなりません。
しかし配列が、以下のように参照として実装されている場合には、様相は異なります。
[[0,1,2],[3,4,5],[6,7,8]] array --+ | *-------------------------------> array--+ | *-------------------> array --+ | 0 | | *--------> array--+ | 3 | | 1 | +-------+ | 6 | | 4 | | 2 | | 7 | | 5 | +-------+ | 8 | +-------+ +------+
こうなっていれば、最初の要素を入れ替える時にも、新しく別の配列を作って、参照をその新しい配列に向けるだけで済みます。効率がずっとよいのです。
それでも困る時には
現代のいわゆるLightweight Languageの多くは、このように内部構造のあるデータを参照を使って実現しつつも、参照そのものは直接ユーザーが触れないようになっています。例外はPerlぐらいでしょうか。これは便利な一方、時には困ったことも起こります。
その一例が、sortです。JavaScriptのsortは「破壊的(destructive)」で、一度このメソッドを使うと配列の内部構造が変化してしまいます。元の配列を残したままで、ソートされた配列が別に欲しいといった場合、これでは困ってしまいます。
配列の場合には、それでも以下の方法で「配列のコピー」を作ることが出来ます。
var copy_of_array = Array.apply(null,original_array);
しかし、これも完全ではありません。配列の中身がまた配列だったりオブジェクトだったりした場合には、元となる配列は別でも、その中身はやはり参照のままです。
参照によって実現されたデータ構造と独立したコピーを作ることを、deep copyと言いますが、JavaScriptの場合それを実現するにはどうしたらよいでしょうか。
FireFoxであれば、Object
のtoSource()
ないしグローバル関数のuneval()
を使って、一端データ構造を文字列化してからそれをeval()
するという手が使えます。
var copy_of_data = eval(uneval(original_data)); var copy_of_data = eval(original_data.toSource());
残念ながら、他のブラウザーにはuneval()
がないので、自作するかライブラリーに頼るかということになります。ネットで"javascript uneval"で検索すればいくつか実装例が出てくるのですが、ここでは私が作ったものを紹介するに留めておきます。
<script language="JavaScript" src="http://blog.livedoor.jp/dankogai/js/uneval.js"></script>
としてお使いください。
Dan the Referr(ed|ing) Man
この程度のチャート表示・編集できるしくみまず作ったら?