uhyohyo.net

JavaScript初級者から中級者になろう

一章第三回 配列のコピー

配列のコピーとは

配列のコピーとは、読んで字のごとく、配列を複製することです。

しかし、前回解説した通り、配列のオブジェクトの一種なので、普通に配列変数をコピーしただけでは、結局同じオブジェクトを参照してしまいます。

そこで、ちゃんと別々のオブジェクトを参照するようにしてやらないと複製したとはいえません。例えばこんな感じです。

var a = [0,1,2,3,4];
b=a;
a[5]=5;
alert(a);
alert(b);

bにaを代入して、aを変更したはずに、bも同じように変更されてしまっています。

今回は、ちゃんと別々のオブジェクトを参照するように配列をコピーしてみましょうという、今までの知識を利用した演習的な回です。

とりあえず、配列をコピーする関数を作りましょう。引数がコピー元の配列で、戻り値が複製された配列というようにします。

function copyArray(arr){
}

「copyArray」が関数名で、引数が「arr」ですね。「Array」とは、配列のことです。ちなみに、関数呼び出しのときに引数としてオブジェクトを渡した場合も、渡されるのはあくまで参照です。

最終的に別々のオブジェクトを参照するようになればいいので、とりあえず配列をもう1つ作ります。 function copyArray(arr){ var newarr = []; }

結論からいうと、この新しい配列に要素を1つずつコピーしていけばいいのです。

1つずつの要素はオブジェクトではなくプリミティブ値なので、そのようにしていけば同じ中身を持った別々の配列ができるというわけです。

今具体的な方法を解説しました。これで自分でコードを書くことができれば、結構JavaScriptの力がついているといえます。

まず、配列の要素1つずつに処理するときはどのようにしましたか?そうです。繰り返しの構文を使います。

function copyeArray(arr){
  var newarr = [];
  for(var i = 0;i<arr.length;i++){
  }
}

このようにfor(やwhile)を使うのでした。この形で、iが0から最後の要素の添字まで増加するのを利用してひとつずつ処理します。

処理の中身は簡単です。元の配列のその番号の要素を、新しい配列の同じ番号の要素にコピーすればいいだけです。

function copyArray(arr){
  var newarr = [];
  for(var i = 0;i<arr.length;i++){
    newarr[i] = arr[i];
  }
}

最後に、できた配列を戻り値として返します。

function copyArray(arr){
  var newarr = [];
  for(var i = 0;i<arr.length;i++){
    newarr[i] = arr[i];
  }
  return newarr;
}

できたら、実際に使ってみましょう。

function copyArray(arr){
  var newarr = [];
  for(var i = 0;i<arr.length;i++){
    newarr[i] = arr[i];
  }
  return newarr;
}
var a = [0,1,2,3,4];
alert(a);
var b = copyArray(a);
alert(b);

a[5]=5;
alert(b); 

アラートが3回表示されます。

まず最初にcopyArray関数をつくり、次の行でaに[0,1,2,3,4]を代入しています。

そして、次の行でそのaを表示します。ちなみに、アラートで配列変数を直接表示すると、丁寧に要素が「,」で区切られて表示されます。

そして、次の行でbにcopyArray関数でaと同じ配列を代入しています。アラートでbを表示すると、正常にコピーされていることがわかります。

その後、a[5]に5を代入されています。この時点でaは、[0,1,2,3,4,5]になることが分かると思います。その後bを表示すると、aを変えてもbには影響がないことが分かります。

ちなみに、これを

function copyArray(arr){
  var newarr = [];
  for(var i = 0;i<arr.length;i++){
    newarr[i] = arr[i];
  }
  return newarr;
}
var a = [0,1,2,3,4];
alert(a);
var b = a;
alert(b);

a[5]=5;
alert(b);

のようにした場合、bにはaの参照が代入されるだけで、オブジェクトそのものは複製されません。

その結果、a[5]に5を代入したときに、bを表示してもaへの変更が反映されてしまっています。aとbは同じオブジェクトを指しているのだから、当然のことです。

多次元配列

さて、上で解説した配列のコピーですが、まだ完璧ではない部分があります。例えば、配列の要素にプリミティブではなくオブジェクトが代入されていたら、結局その要素は参照になってしまいます。まあ、それで困るかどうかは場合次第ですし、オブジェクトのコピーは配列ほど簡単ではないのでここでは解説しません。

しかし、配列の要素にまた配列が代入されているという場合があります。これくらいは対処したいものです。ちなみに、このように配列の要素にまた配列が代入されているものを、多次元配列ということがあります。

var a = [
  [0,1,2],
  [3,4,5],
  [6,7,8]
];

alert(a[0][1]);
alert(a[2][0]); 

このサンプルでは、1〜5行目でaに配列を代入しています。全体が[ 〜 ]で囲まれているのが分かります。しかし、そのひとつひとつの要素ですが、[0,1,2]が0番目の要素、[3,4,5]が1番目の要素、[6,7,8]が2番目の要素となっています。今回は分かりやすく改行を入れています。

これらの要素もそれぞれ配列であることが分かります。つまり、配列の要素がまた配列というわけです。

それをアラートで表示するわけですが、なんと添字が2個ついています。

a[0][1]の場合、左から処理されます。まず、a[0]が処理されます。これは、[0,1,2]の配列でしたね。つまり、a[0][1]は「([0,1,2]の配列への参照)[1]」のようになり、[0,1,2]の1番目の要素を表しています。つまり1ですね。

同様にa[2][0]の場合、a[2][6,7,8]で、その0番目の要素ですから6ということになります。

ちなみに、「配列の中の配列の要素がまた配列」なんていう場合もあると思います。その場合もa[0][0][0]のように添字の数を増やすことで対応できます。もっとも、よく使われるのはせいぜい2つくらいまでです。

多次元配列のコピー

さて、それではcopyArrayを作りなおしましょう。さっきのものだと、配列の要素がまた配列だと、結局そこで参照をコピーしてしまいうまくいかなかったのでした。

function copyArray(arr){
  var newarr = [];
  for(var i = 0;i<arr.length;i++){
    newarr[i] = arr[i];
  }
  return newarr;
}
var a = [ [0,1,2] , [3,4,5] ];
alert(a[0]);
var b = copyArray(a);
alert(b[0]);

a[0][3]=3;
alert(b[0]);

まずaに多次元配列を代入し、a[0]を表示しています。[0,1,2]が表示されます。

bにさっきのcopyArrayでaをコピーしますが、ここでbはaとは別の配列です。ややこしくなってきましたが、しっかり理解しましょう。

bそのものは確かにaとは中身が同じ別の配列です。その中身は、普通の配列ではプリミティブ値でしたが、今回の多次元配列は参照でした。中身は同じですから、当然参照が指すものは同じということです。

つまりa[0]b[0]は、同じオブジェクト(今回の場合は[0,1,2])を指す参照がそれぞれ代入されています。だから、a[0]を通して操作すると、結局同じオブジェクトを操作することになってしまうというわけです。a[1]b[1]も同じです。

さて、ではどうすればいいかですが、配列の要素をひとつひとつコピーするとき、その要素が配列だったらその配列の要素をまたひとつひとつコピーする必要があります。

つまり、こうなります。

function copyArray(arr){
  var newarr = [];
  for(var i = 0;i<arr.length;i++){
    if(もしarr[i]が配列だったら){
      newarr[i] = copyArray(arr[i]);
    }else{
      newarr[i] = arr[i];
    }
  }
  return newarr;
}

copyArrayをcopyArrayの中で使うのです。一見よく分からないように思えますが、確かなのは「copyArrayは配列の要素を1つ1つコピーする関数だ」ということです。だから、配列の要素が配列だったとき、copyArrayを呼び出せばちゃんとコピーしてくれるというわけです。ちなみに、このように関数の中でさらに同じ関数を呼び出す方法を再帰といいます。

さて、if文が登場しましたが、条件が日本語で書いていますね。これでは動きません。わざわざこう書いたのは、配列かどうかの判定法はまだ解説していなかったからです。それには、Array.isArrayという関数を使います。これは引数に何かを渡すと、それが配列の場合はtrue、そうでない場合はfalseを返してくれる関数です。これを用いるとcopeArrayの完成形は次のようになります。

function copyArray(arr){
  var newarr = [];
  for(var i = 0;i<arr.length;i++){
    if(Array.isArray(arr[i])){
      newarr[i] = copyArray(arr[i]);
    }else{
      newarr[i] = arr[i];
    }
  }
  return newarr;
}

これをさっきのサンプルに組み入れると、正しく動作することがわかります。やってみましょう。