uhyohyo.net

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

十一章第一回 配列

第十一章では、新しいJavaScriptについて解説します。

とはいっても、実はこの第十一章は「第一篇 古典的なJavaScript」に入っています。実は、古典的なJavaScriptの中では新しいほうであるということにとどまります(この章を書いた当時は新しかったのですが、書き進めるうちにさらに新しいものを紹介したくなったのでこうなりました)。そもそも、新しいものでも時間がたつと古くなってしまいます。この記事が最初に書かれたのは2010年ですが、「JavaScript初級者から中級者になろう」をリニューアルした2014年現在ではもはや新しくもなんでもありません。ですから、その程度のものと考えましょう。

しかしそもそも、新しいJavaScriptとは何なのでしょう。これには諸説あると思いますが、自分の中では、(これを書いた2010年時点では)「IEで動くか動かないか」が1つの基準になっていると考えていました。当時IEの最新バージョンは8でした(リニューアルした2014年時点での最新は11)。

このIE8というのが今からしたらひどいブラウザで、 最初で「IEをやめよう」と書いた通り、このサイトで解説しているDOMには、IE8では動かないものはたくさんありました。しかし、それはIEがあくまでDOMに対応していないだけで、JavaScriptそのものの文法・仕様については、特にIE非対応ということはありませんでした。

つまり、DOM部分は高度なものであっても、それは古いJavaScriptの上に乗っかったものであったのです。

この基準はなかなか大事なもので、広くみんなに見てもらいたいサイトを作る場合には、残念ながら世の中にはIEを使うようなユーザーもいるので、「新しいJavaScript」を利用するには少し抵抗がありました。そういう点により、古いJavaScriptよりは普及度合いが低いと思われます。2014年現在でも、古いIEを使う厄介なユーザーが一定数存在します。

ちなみに、IEでもバージョン11ともなると、だいぶ他のブラウザに近いクオリティになってきています(ただし前章で解説したXPathは未だに非対応です)。

それで、実は今まで解説してきたのは全てこの古いJavaScriptで(実はゲッタとセッタだけは新しいほうに入りますが)、第十一章では、この「新しいJavaScript」、つまり「IE8では動かないようなJavaScript」について解説していきます。

具体的には、実はJavaScriptにもバージョンというのがあり、最新は2.0ということになっています。ずっと昔は1.0とかの時代もありましたが、だいたい、1.6以上が新しいJavaScriptという感じだと自分は解釈しています。

バージョンというのは、新しいほど機能が追加されたり改良されたりしているということですね。

そして、今回の題名は「配列」です。配列といえば、基礎第六回で解説しましたが、基礎の基礎しか解説していませんでした。もうすこし早く解説していれば良かったのですが、ここまで伸びてしまいました。実は、古いJavaScriptでももうちょっといろいろあるので、まずはそれを解説します。

古いJavaScriptにおける配列

今までに配列について解説したのは、次のような感じです。

var array=['麻生太郎','鳩山由紀夫','菅直人'];	//←このように配列を作る

console.log(array[1]);	//←添字を用いて要素を参照できる

配列の作り方と、各要素の参照の仕方です。また、lengthというプロパティをもち、これが要素の数なのでした。

for(var i=0;i<array.length;i++){
  console.log(array[i]);
}

こうすることで、配列の全ての要素がログに表示されます。

さて、ここで、lengthをプロパティと言ったことからも分かるように、配列は実はオブジェクトの一種なのですね。これは、配列が

var array = new Array('麻生太郎','鳩山由紀夫','菅直人');

のように作ることができることからも分かります。

では、array[i]というように、[]を使って要素を参照する表現は何なのでしょう。

今まで紹介したかどうか分かりませんが、実はこれはプロパティを参照する表現なのです。

どういうことかというと、例えばarray.lengthはarray["length"]としても参照可能です。[]内が文字列であることに注意して下さい。

こうすることで、配列以外でも、プロパティ名に変数を使うことが可能です。しかし、ここで変数iは数値ですから、文字列ではありません。こういう場合単純で、文字列に変換されます。

したがって、例えばarray[0]はarray["0"]となっているということです。だからといって、数字だけの場合、array.0というようにドットを使った参照はできません。

さて、そこで、実は配列を操作するために便利なメソッドがあります。これらのメソッドは、Array.prototypeにあることが予想できますね。

さて、まず紹介するのがpushとpopです。pushは、配列の最後に要素を追加するものです。追加する要素は引数で指定します。逆に、popは最後の要素を取り除きます。取り除いた要素は戻り値となります。

var array=['麻生太郎','鳩山由紀夫'];

array.push("菅直人");
console.log(array);	//麻生太郎,鳩山由紀夫,菅直人 と表示される

var a = array.pop();	//配列は 麻生太郎,鳩山由紀夫 になる
console.log(a);	//戻り値は菅直人
        

逆に、最初に要素を追加するのもあります。それがunshiftです。逆に、最初の要素を取り出すのがshiftです。

var array=['鳩山由紀夫','菅直人'];

array.unshift("麻生太郎");
console.log(array);	//麻生太郎,鳩山由紀夫,菅直人

var a = array.shift();	//配列は 鳩山由紀夫,菅直人 になる
console.log(a);	//麻生太郎
        

もう1つ便利なのが、spliceメソッドです。これは、途中の要素を抜き出すメソッドです。

引数は最低2つで、1つ目は抜き出し開始位置(添字と同じで最初が0なので注意)、2つ目は抜き出す数です。

例えば、さっきの var array=['麻生太郎','鳩山由紀夫','菅直人'];

という配列の場合、真ん中の鳩山由紀夫だけを抜き出したい場合、鳩山由紀夫は1番目の要素で、抜き出したい数は1つなので、

array.splice(1,1);

とします。例えば、「菅直人」も一緒に抜き出したい場合、抜き出す数は2つになるので

array.splice(1,2);

とします。

そして、「引数は最低2つ」といったのは、3つ以上でもいいということです。3つめ以降の引数があると、抜き出した部分に代わりにそれを要素として挿入します。例えば、

var array=['麻生太郎','鳩山由紀夫','菅直人'];
array.splice(1,1,"小沢一郎");

とした場合、splice後の配列は

['麻生太郎','小沢一郎','菅直人']

となるわけです。引数を4つとか、挿入すべき要素を2つ以上渡すと、それが全部挿入されます。

また、第二引数(抜き出す数)を0にすることで、抜き出さずに挿入だけするということも可能です。

そしてもう1つ、reverseを紹介します。これは引数がなく、配列を逆にします。つまり、

var array=['麻生太郎','鳩山由紀夫','菅直人'];
array.reverse();

console.log(array);	// 菅直人,鳩山由紀夫,麻生太郎
          

そして次にsortです。これは、配列のソートです。つまり、並び替えるということです。やってみましょう。

var array=['麻生太郎','鳩山由紀夫','菅直人'];
array.sort();

console.log(array);	// 菅直人,鳩山由紀夫,麻生太郎
          

なんと偶然reverseと同じ結果になりましたが、reverseとは違います。文字列の場合、その文字のコード(コードポイント値)を利用して、小さい順に並び替えています。

分かりにくいので、数値でやってみましょう。

var array=[5,8,0,2,7,1,4,3,9,6];
array.sort();

console.log(array);	// 0,1,2,3,4,5,6,7,8,9
        

これは分かりやすいですね。小さい順に並んでいます。

文字列でも、日本語だと分かりにくいですが、アルファベットなら分かりやすいかもしれません。

var array=["dog","cat","rabbit","elephant","dolphin"];
array.sort();

console.log(array);	// cat,dog,dolphin,elephant,rabbit"
        

これはいわゆる「辞書順」というやつですね。さっき文字のコードポイントで判定されていると言いましたが、英数字の場合単純で、コードポイント値も辞書順に並んでいるのです。だからこのような結果になります。

ところが、また数値に戻って、2桁の数が混ざってくると様子が違ってきます。

var array=[5,2,10,365,8];
array.sort();

console.log(array);	// 10,2,365,5,8
        

なんと、順番がでたらめです。これは何かと思いますが、実は、数値も文字列に変換されているのです。つまり、[5,2,10,365,8]を並び替えるときには、["5","2","10","365","8"]という配列を並び替えるのと同様に扱われます(実際に配列の値が文字列に変わったりはしませんが)。ここで、数字も文字の一種ですから、コードポイント値というものがあり、数字が小さいほどコードポイント値も小さくなります。

ですから、数として大きいかどうかは関係なく、文字列を辞書順で並び替えたときにどうなるかということになっているわけです。

しかし、数値順に並び替えたいということもあるかもしれません。そこで、sortを用いる場合に、並び替えの方法を指定してやることができます。ここで使うのが、コールバック関数です。これはつまり、sortの引数関数を渡そうということです。

そうすると、その関数がソート中に呼ばれます。この関数は何をするかというと、2つの値を比べることです。sort関数は状況に応じて2つの値を比べようとします。そのときの動作を関数の形で指定してやることで、結果的にソート結果をいじることができるというわけです。

その関数には、次のような形になります。

function compare(a, b){
}

2つの引数が渡されていますね。引数の名前はなんでもいいのですが、一般的にはaとbが使われるようです。

比較結果を通知するのには戻り値を使います。0未満ならaのほうがbより小さい(bより前)、0より大きいならaのほうがbより大きい(bより後)、0なら同じということになります。

つまり、数値比較用の関数を作るとなると、次のようにするとよいでしょう。

function compare(a, b){
  if(a<b){
    //aのほうが小さい(aのほうが前)
    return -1;
  }else if(a>b){
    //aのほうが大きい(aのほうが後)
    return 1;
  }else{
    //同じ
    return 0;
  }
}

これを用いてやってみましょう。

var array=[5,2,10,365,8];
array.sort(compare);

console.log(array);	// 2,5,8,10,365

function compare(a, b){
  if(a<b){
    //aのほうが小さい(aのほうが前)
    return -1;
  }else if(a>b){
    //aのほうが大きい(aのほうが後)
    return 1;
  }else{
    //同じ
    return 0;
  }
}

正しい結果になりました。

ただ、実はこんなに難しくする必要はありません。次のような形で十分です。

function compare(a, b){
  return a-b;
}

これは、aがbより小さければ負、大きければ正の値、そして同じならば0を返します。

また、これならもう無名関数にしてしまってsortの引数に直接書いてもよいでしょう。

array.sort(function(a,b){return a-b});

他に、もっと複雑な並び替えをしたいときなどにsortは役立ちます。

さて、いままで紹介してきたメソッドはいずれも破壊的なメソッドでした。破壊的とは、配列のメソッドを呼び出せば、その配列そのものが変化するというものです。しかし、ここから先はそうではありません。

そんな中、次に紹介するのはconcatです。これは、配列に他の配列や値をつなげるというものです。さっきも言ったとおりconcatは破壊的でない(非破壊的といいます)ので、concatを呼び出しても配列そのものは変化せず、戻り値として新しい配列が返されます。


var array1 = ['小泉純一郎','安倍晋三','福田康夫','麻生太郎'];	//配列1
var array2 = ['鳩山由紀夫','菅直人'];	//配列2

var newarray = array1.concat(array2);
console.log(newarray);	//小泉純一郎,安倍晋三,福田康夫,麻生太郎,鳩山由紀夫,菅直人
          

2つの配列をつなぎあわせて、新しい配列をつくっています。ここで、

console.log(array1);

として配列1を表示してみても、変わっていないことが分かります。

また、配列以外を直接追加させることもできます。

var array1 = ['小泉純一郎','安倍晋三','福田康夫','麻生太郎'];	//配列1

var newarray = array1.concat('鳩山由紀夫','菅直人');
console.log(newarray);	//小泉純一郎,安倍晋三,福田康夫,麻生太郎,鳩山由紀夫,菅直人
          

引数に配列以外を渡してやると、それがそのまま追加されます。

また、引数に配列を複数渡すとそれを全部つなぎあわせてくれるなど、なかなか応用がききます。

次に紹介するのはjoinです。これは、配列を全部つなぎあわせて文字列にして返します。引数で、要素の間にはいる文字(区切り文字)を指定できます。具体的には、次のような形です。

var array=['麻生太郎','鳩山由紀夫','菅直人'];
console.log(array.join("→"));	// "麻生太郎→鳩山由紀夫→菅直人"
          

これも使いどころがあると思います。

次に紹介するのはsliceです。これはspliceと同様配列の一部を抜き出しますが、非破壊的なのでもとの配列を変更せず、その部分が新しい配列となって返ってきます。

ただ、引数は2つですが、spliceと少し違います。1つ目の引数は抜き出し始めの添字であり同じですが、第二引数では抜き出し終了位置を指定します。第二引数が省略された場合は最後までとなっています。

この抜き出し終了位置も添字ですが、その要素の1つ前までを抜き出します。たとえば、

array.slice(1,4)

だったとしたら、1番目から3番目まで(0から数えることに注意)を抜き出します。

新しいJavaScript

以上で、古いJavaScriptにおける配列処理用のメソッドの紹介は終了です。

ここからが本題です。新しいJavaScriptでは、配列処理用の新しいメソッドが追加され、結構便利になりました。

配列の各要素に対する処理

今あなたは、配列をもとにしてul要素・li要素によるリストを作成したいとします。例えば、

['麻生太郎','鳩山由紀夫','菅直人']

という配列があったならば、これをもとにして

<ul>
  <li>麻生太郎</li>
  <li>鳩山由紀夫</li>
  <li>菅直人</li>
</ul>

を作りたいということです。配列というのもリストのようなものですから、このような機会はなかなか多いと思います。

普通にやってみると、次のようになります。見ないで自分で作れるといいですね。

var array=['麻生太郎','鳩山由紀夫','菅直人'];	//配列名はarrayとする
var ul=document.createElement("ul");	//まずul要素を作る

//配列の要素数だけ繰り返す
for(var i=0;i<array.length;i++){
  var li=document.createElement("li");	//li要素を作る
  li.textContent=array[i];	//li要素の中身を array[i]のテキストにする

  ul.appendChild(li);	//li要素をul要素に追加する
}

document.body.appendChild(ul);	//一応ul要素をbody要素の最後に追加しておこう
        

さて、今回の場合、配列の要素1つずつに対して「li要素をつくってul要素に追加する」という処理をしています。これは今までの場合、このようにfor文でループを回すのが定石でしたね。しかし、これをもっと楽にすることができます。

そこで登場するのが、forEachというメソッドです。これはコールバック関数を引数に取り、配列の各要素に対して順番にその関数を実行するというものです。

そして、今配列のどの要素を処理しているかが分かるように、そのコールバック関数の引数として現在の要素が渡されます。これを踏まえてコードを書くと、このようになります。

var array=['麻生太郎','鳩山由紀夫','菅直人'];	//配列名はarrayとする
var ul=document.createElement("ul");	//まずul要素を作る

//配列の要素数だけ繰り返す
array.forEach(function(x){
  var li=document.createElement("li");
  li.textContent=x;
  ul.appendChild(li);
});

document.body.appendChild(ul);	//一応ul要素をbody要素の最後に追加しておこう
        

forEachの引数として、次のような無名関数が渡されています。


function(x){
  var li=document.createElement("li");
  li.textContent=x;
  ul.appendChild(li);
}
        

引数xをとっていることが分かります。これに現在の要素が入っていることになりますから、forEachの効果により、この関数が引数にそれぞれ"麻生太郎"、"鳩山由紀夫"、"菅直人"をとって3回呼ばれるということになります。

この方法の利点は、カウンタ変数(for文を使ったさっきのサンプルの場合、変数i)がいらないという点などです。配列の各要素に対する処理を簡単に書けるためとても便利です。

また、実はコールバック関数に渡される引数は、現在の要素以外にもあります。第二引数は現在の添字、第三引数は現在処理している配列そのものです。

第二引数の現在処理している添字というのは、この配列の場合だと、「麻生太郎」のときは0、「鳩山由紀夫」のときは1、「菅直人」のときは2ということです。

第三引数引数ですが、上のサンプルの場合だと、現在処理している配列は変数arrayなので、いちいち第三引数を使わなくても変数arrayをそのまま使えばいいと思うかもしれません。

しかし、コールバック関数を複数の配列で使い回す場合などに必要になることもあります。

ちなみに、配列の要素1つずつに対して毎回関数呼び出しが発生するので、for文を使った方法に比べて処理速度が遅いと言われています。そのため、処理速度が求められる場面ではforEachが避けられることがあります。

配列をもとにして新しい配列を作る

ある配列のデータを利用して、新しい配列を作りたい場合があるとおもいます。

例えば、さっきの

['麻生太郎','鳩山由紀夫','菅直人']

という配列の各要素に「氏」をつけて、

['麻生太郎氏','鳩山由紀夫氏','菅直人氏']

という配列を新しく作りたいとしましょう。普通に考えるとこうなりますね。

var array=['麻生太郎','鳩山由紀夫','菅直人'];	//配列名はarrayとする

var newarray=[];	//結果の配列を作っておく

//配列の要素数だけ繰り返す
for(var i=0;i<array.length;i++){
  newarray.push(array[i]+"氏");
}

console.log(newarray);	//麻生太郎氏,鳩山由紀夫氏,菅直人氏
        

もとの配列の要素1個につき、新しい配列に要素1個を追加するわけです。

これを楽にしてくれるのが、mapメソッドです。これも同じくコールバック関数を引数に渡します。これはforEachと同じですね。

forEachと同様に、各要素ごとにコールバック関数が呼ばれます。その際の引数も同じです。

forEachと違う点は、そのコールバック関数が、その要素に対する処理結果を返し、mapメソッドそのものも、最終的に処理結果からなる新しい配列を返すという点です。

要するに、こういうことです。

var array=['麻生太郎','鳩山由紀夫','菅直人'];	//配列名はarrayとする

//配列の要素数だけ繰り返す
var newarray = array.map(function(x){return x+"氏";});

console.log(newarray);	//麻生太郎氏,鳩山由紀夫氏,菅直人氏
        

これは何が起こっているかというと、mapの引数に渡された関数の動きを見てみましょう。

配列の各要素ごとに関数が呼ばれますから、まず"麻生太郎"を引数にとって関数が呼ばれます。するとこの関数は"麻生太郎氏"を返しますね。次は同様に"鳩山由紀夫氏"、"菅直人氏"と続きます。そして、これらを集めた新しい配列がmapの返り値となっているのです。

さらにこれは、一回使うだけならいちいち変数に代入する必要もありませんね。

var array=['麻生太郎','鳩山由紀夫','菅直人'];	//配列名はarrayとする

console.log(array.map(function(x){return x+"氏";}));	//麻生太郎氏,鳩山由紀夫氏,菅直人氏
        

さて、これに、さっき紹介したjoinを組み合わせて、各要素に「氏」を付けた上で「→」で結びたい場合はどうしましょう。一度変数にいれて、次のようにするのが考えられます。

var array=['麻生太郎','鳩山由紀夫','菅直人'];	//配列名はarrayとする

//配列の要素数だけ繰り返す
var newarray = array.map(function(x){return x+"氏";});

console.log(newarray.join("→"));	//麻生太郎氏→鳩山由紀夫氏→菅直人氏
        

しかしやはりこれも、いちいち変数に代入するのは面倒なので、こうできます。

var array=['麻生太郎','鳩山由紀夫','菅直人'];	//配列名はarrayとする

console.log(array.map(function(x){return x+"氏";}).join("→"));	//麻生太郎氏→鳩山由紀夫氏→菅直人氏
        

まずmapメソッドを呼んで、その返り値が配列なのでそのまま.(ドット)を付けてjoinメソッドを呼び出しています。

条件を満たす要素のみを集める

ここから先は、条件を満たすかどうかという話になってきます。

配列の中で、特定の条件を満たすもののみを集めたいという場合があると思います。例えば、数値の配列があって、5以下のもののみを抜き出したいとします。例によって普通にfor文でやると、こうです。

var array=[3,8,2,10,5,9,8];	//配列名はarrayとする

var newarray=[];	//結果の配列を作っておく

//配列の要素数だけ繰り返す
for(var i=0;i<array.length;i++){
  if(array[i]<=5){	//条件を満たすもののみ
    newarray.push(array[i]);
  }
}

console.log(newarray);	//3,2,5
        

これを楽に行ってくれるのが、filterメソッドです。これも、条件を判定するコールバック関数を渡す必要があり、コールバック関数の仕様はforEachやmapと同じです。

条件は「満たす」か「満たさない」ですから、コールバック関数は真偽値、すなわちtrueかfalseを返します。とはいっても、それ以外の値でも真偽値に変換されます。

そうすると、filterメソッドは、true(真)が返された要素のみを集めて新たな配列として返します。すなわち、こうします。

var array=[3,8,2,10,5,9,8];	//配列名はarrayとする

var newarray= array.filter(function(x){return x<=5;});

console.log(newarray);	//3,2,5
          

かなりすっきりしましたね。もちろん、この場合はいちいち変数にいれないでも可能です。

配列から条件を満たす要素を探す

配列があって、「その中のどれか1つでも条件を満たせばOK」とか「全て条件を満たせばOK」という状況があります。それをするメソッドが、some(どれか)・every(全部)です。

コールバック関数はfilterと同じく「条件」ですから、filterと全く同様のものでOKです。

someは、要素全部について調べて、コールバック関数がtrue(真)を返すものが1つでもあればtrueを返し、1つもなければfalseを返します。everyは、全部trueだったならtrueを返し、1つでもfalseがあればfalseを返します。

例えば、5以下の数値が1つでもあるかどうか調べたい場合、

var array=[3,8,2,10,5,9,8];	//配列名はarrayとする

console.log(array.some(function(x){return x<=5;}));	//true
          

とします。これをeveryにすると、6以上のものもあるのでfalseとなるわけです。

ここまで紹介してきたメソッドは、JavaScript1.6と呼ばれる世代で追加されたメソッドです。

配列の各値に関数を適用して1つの値を作る

分かりにくい見出しですが、最後に紹介するのはreduceです。これはJavaScript1.8で追加されたものです。

例えば、「配列の各値を全部足したい」という場合はどうしますか。次のようにすることでしょう。

var array=[3,8,2,10,5,9,8];	//配列名はarrayとする

var sum=0;	//合計

//配列の要素数だけ繰り返す
for(var i=0;i<array.length;i++){
  sum+=array[i];
}

console.log(sum);	//45
          

しかし、変数sumを使わないでもいい方法があるのです。それがreduceです。さっきの例をreduceで書き直すと、次のようになります。

var array=[3,8,2,10,5,9,8];	//配列名はarrayとする

var sum=array.reduce(function(x,y){
  return x+y;
});

console.log(sum);	//45
          

いったい何が起こったかですが、まずreduceに渡されたコールバック関数に注目しましょう。最終的にやりたいことは「配列の値を全部足す」ですが、これを見ると「2つの値を足す」ということをしています。

実は、reduceの動作は、まず0番目と1番目について計算を行い、その計算結果と2番目を計算、 さらにその計算結果と3番目…というように順番に進んでいきます。

そして最後まで進んだときの結果がreduceの戻り値となるのです。

今回の場合、まず最初は0番目と1番目だから、コールバック関数に、xは3,yは8が代入されて呼び出されます。したがって返り値は11です。

すると、次の呼び出しでは、xには結果の11がyには2番目の要素である2が代入され、戻り値は13です。

以下 x=13,y=10 → x=23,y=5 → x=28,y=9 → x=37,y=8 → 結果は45となり、45が返されます。

理解するのは大変だと思いますが、使いどころを見つければ便利です。

ちなみに、右から(後ろから)順番にこれを行う、reduceRightもあります。

また、reduceにはコールバック関数以外にも引数を渡すことができます。それは「デフォルト値」です。

デフォルト値がある場合、普通まず最初は「0番目と1番目」なのが、「デフォルト値と0番目」、「0番目と1番目」、・・・というように進んでいきます。

例えば、実はreduceは、要素が1つしかない配列に適用した場合は「0番目と1番目」の計算すらできませんが、その場合は0番目のをそのまま返すことになっています。

ところが、要素が0個の場合はどうしようもなくてエラーを発生させます。これではまずいことがありますね。

そこでこのデフォルト値を指定してやると、前述のような動作に加えて「配列の要素が1つも無い場合はデフォルト値をそのまま返す」という動作をしてくれます。

例えば上の例だと、要素がひとつもない配列の場合だと0が返されるのが妥当でしょうから、デフォルト値に0を指定して次のようになるでしょう。

var array=[3,8,2,10,5,9,8];	//配列名はarrayとする

var sum=array.reduce(function(x,y){
  return x+y;
},0);

console.log(sum);	//45