Subterranean Flower

JavaScriptの変数と配列と参照について

Author
古都こと
ふよんとえんよぷよぐやま!!!

配列(Array)はJavaScriptにおける基本的な要素のひとつです。しかし、変数と組み合わせると、初学者にとって少し不可解な挙動をすることがあります。この記事では、配列と変数の組み合わせで発生する不思議な挙動の理由と、その解決方法について説明します。

基礎と基礎を組み合わせると……バグ?

私たちはこう習いました。「変数は値を入れる箱である」と。そして実際そのように変数は機能します。これは問題ありません。

そしてこういったことも習いました。「配列は値を入れる箱を複数並べたものである」と。配列は実際そのように機能します。これも問題ないでしょう。

しかしこれらふたつを組み合わせると、よくわからないことが起こります。以下のコードを見てください:

let array1 = [1, 2, 3]; // 配列[1, 2, 3]
let array2 = array1; // array1のコピー

// array1に4を追加
array1.push(4);

// 両方の内容を表示
console.log(array1);
console.log(array2);

これは簡単なコードです。まず、配列[1, 2, 3]である変数array1と、そのコピーであるarray2を用意します。次にarray1に4という値を追加し、両方の変数を値を表示するだけのものです。

このコードの実行結果は以下のようになることが期待できます:

[1, 2, 3, 4]
[1, 2, 3]

コードを実際に実行してみましょう。どうなるでしょうか。

[1, 2, 3, 4]
[1, 2, 3, 4]

おおっと!これはどういうことでしょう?array1には4を追加しましたが、array2には何もしていないはずです。しかしarray2にも4が追加されてしまっているようです。

不思議な挙動ですが、これはバグではありません。正しい挙動です。しかし、どうしてこういうことが起こるのでしょうか?

変数と参照

この動きを理解するためには、まず、変数について、ひとつの誤解を解かなければなりません。

問題の箇所は、以下の2行です:

let array1 = [1, 2, 3]; // 配列[1, 2, 3]
let array2 = array1; // array1のコピー

このとき、私たちの素朴な理解では、以下の図のようなことが起こっていると考えていました。

変数から変数へ配列をコピーする、とても簡単なことです。しかし、実はこの図の理解は誤りなのです。実際にはこのようなことは起こっていません。

実際にはどうなっているのでしょうか。実は、変数の中に直接配列が入っているわけではありません。変数の中には、「参照」と呼ばれる「配列の場所」が記録されていて、配列の実体は別の場所(メモリ上のどこか)にあります

つまり、変数array1は以下の図のようになっています。

変数array1には、例えば「番地1」という参照が入っており、配列の実体である[1, 2, 3]は別の場所(番地1)に存在します。さて、これを変数array2に代入してみましょう!array1の内容をそのままarray2に代入するわけです。すると……

ああっと!配列ではなく参照がコピーされてしまいました!そうです、配列自体はコピーされません。参照の値がコピーされるのです。結果として、ふたつの変数は全く同じ配列を指している、ということになります。

この状態でarray1を操作してみましょう。array1に4を加えてみます。

array1の指し示す配列に4を加えました。しかしarray2も同じ配列を指し示しています。つまり、どちらの変数からでも全く同じ[1, 2, 3, 4]が見えている、というわけです。これがarray1を操作するとarray2まで変わってしまった現象のカラクリです。

まとめると、array1には参照が入っており、array2に代入すると配列ではなく参照がコピーされ、結果としてarray1とarray2は全く同じものを指し示していた、ということになります。

関数の引数にも注意

これは単なる変数の場合だけではありません。関数の引数にも注意しましょう。例えば以下のようなコードがあるとします:

function pushValue(array, value) {
  array.push(value);
  return array;
}

let a = [1, 2, 3];
let b = pushValue(a, 4);

console.log(a);
console.log(b);

このとき関数の引数arrayに渡るのも参照の値です。渡された配列の参照をそのまま操作してしまうと、呼び出し元の配列の値まで変わってしまいます。つまり、このコードの実行結果は以下のようになります:

[1, 2, 3, 4]
[1, 2, 3, 4]

関数の中では、むやみに配列を書き換えないようにしましょう。

しかしなぜこんなことを?

配列が直接変数に入っているわけではないということはわかりました。すると次に疑問に思うのが「なぜこんなことをするのか」でしょう。

JavaScriptの値には2種類あります。プリミティブ型とオブジェクト型です。プリミティブ型は数値や文字列といった基本的なもののことで、オブジェクト型は配列(Array)などのその他の値のことです。

Arrayオブジェクトに代表されるオブジェクト型は、小さくまとまっているプリミティブ型とは異なり大きな構造を持ちます。例えばArrayオブジェクトで言えば、複数の値を持てる、多数のメソッドが存在する、多数のプロパティが存在する、などです。そういった大きな構造を持つ値を毎回コピーしていると、どうしても処理に時間がかかり、遅くなってしまいます。

しかし参照の値を受け渡しするのならば、簡単な値のコピーだけで済むので、短時間で処理を終わらせることができます。つまり、この方式の方が、無駄が少ないのです。

そしてこの参照の値を渡す手法は、Arrayオブジェクトだけでなく、オブジェクト型全てで適用されます。プリミティブでない値を扱うときは気をつけましょう。

それでも配列をコピーしたいんだけど……

そうは言っても配列をコピーしたい場合というのはよくあります。配列を複製する方法を、いくつか紹介しましょう。

ひとつは、Array.fromメソッドを使うことです。これは、他の配列などから、新しい配列を作るメソッドです。Array.fromに配列を渡すことで、内容をコピーした、新しい配列を作ることができます。

もうひとつは、スプレッド演算子を使うことです。スプレッド演算子を使えば、新しい配列の中に、他の配列の値を展開することができます。

let array = [1, 2, 3];

let array_copy1 = Array.from(array);
let array_copy2 = [...array];

これらの方法を使えば、全く新しい、参照の値も異なる配列を作ることができます。どちらも大した違いはないので、好きな方を使ってください。