Chapter 05

配列

OJK
OJK
2022.05.08に更新

準備

勉強用フォルダの下に「05」という名前で作業フォルダを作成して、以下のファイルを作成し、ライブプレビュー画面(ブラウザー)とコンソールを準備してください。

雛形コード
index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Chapter 05: 配列</title>
</head>
<body>
  <ul id="list"></ul>
  <script src="script.js"></script>
</body>
</html>
script.js
'use strict';

console.log('Hello World!!');

配列の基本

アプリケーション開発ではデータのリストを処理することが多いのですが、これまで学んだ定数や変数は一つのデータにしかラベルを付けられません。そこで大抵のプログラミング言語にはデータのリスト(集合)を扱うためのデータ構造が用意されています。JavaScript では 配列/array と呼ばれるものです。

配列定数の宣言方法は次のとおりです。変数の場合でも同じです(const を let に変えるだけ)。

const array = [1, 2, 'dog!', 1, 2, 'cat!'];

等号の右側の内容、どこかで見たぞ…という人は記憶力がよいですね。前チャプターの for-of 文のところで出てきました。for (const data of [1, 2, 'dog!', 1, 2, 'cat!']) で使用していた [ ] の部分は配列です。

上記の例では、配列 array に 6 個の値を指定しています。配列の値のことを “要素/element” といいますが、JavaScript の場合、HTML の要素と混同しますね。文脈から判断する必要があります。

定数や変数のときは比喩として「ラベル」を使いましたが、配列の場合は「箱」のイメージのほうが分かりやすいかもしれません。普通の定数や変数を「名前付きの箱」と考えた場合、配列は「仕切りのある長い箱」です。箱は一つなので名前は一つですが、仕切りがあるので複数の値が入るイメージです。

配列

仕切りで区切られた中味(各要素)には インデックス/index という番号が振られています。配列名[index] としてアクセスすることができます。注意したいのはインデックスが 0 から始まる ことです。

インデックス

具体例をコードで見てみましょう。後ろに [index] を付けていれば、普通の定数と全く同じように使えます。

const array = [1, 2, 'dog!', 1, 2, 'cat!'];

console.log(array[0]); // → 1
const animal = array[2];
console.log(animal); // → dog!
array[3] = 'pig!';   // arrayの“要素”は定数ではないので代入可
array[4] += array[0];
console.log(array); // → [1, 2, 'dog!', 'pig!', 3, 'cat!']

array = [1, 2, 3, 'dah', '!!'];  // arrayそのものは定数なのでエラー

なお、上記の例では配列 array は定数として宣言していますが、配列の “要素” は定数ではないので代入して書き換えることができます。最終行のように、配列そのものを別の配列データで上書きしようとするとエラー(Assignment to constant variable)となります。

配列と繰り返し構文

配列は、繰り返し構文とともに使われることで威力を発揮します。インデックスが連続する数値なので、for 文を使って次のように扱うことができます。

const array = [1, 2, 3, 'dah', '!!'];

for (let i = 0; i < 5; i += 1) {
  console.log(array[i]); // → 1, 2, 3, dah, !!
  array[i] = i;  // 要素の値の書き換え
}

console.log(array); // → [0, 1, 2, 3, 4]

JavaScript の配列はオブジェクトとして実装されているので、プロパティとメソッドを持っています。length プロパティ は配列の長さを表します。

const array = [1, 2, 3, 'dah', '!!'];
console.log(array.length); // → 5

この length プロパティを使えば、for 文の繰り返し条件は次のように書けます。

const array = [1, 2, 3, 'dah', '!!'];
for (let i = 0; i < array.length; i += 1) {
  console.log(array[i]);
}

後述するように、JavaScript の配列は途中で長さを変えることができる(要素を追加したり削除したりすることができる)ので、for 文の繰り返し条件は length プロパティを使って書かないとバグになることがあります。

for-of 文

for 文による配列操作は基本知識として押さえてもらいたいのですが、配列に対する繰り返し処理では for 文ではなく for-of 文 を使うのが一般的です。length プロパティやインデックスを使わず簡潔に書けるからです。

const array = [1, 2, 3];
for (const el of array) {
  console.log(el);  // 1 → 2 → 3
}

ただし、ループ定数(サンプルコードでは el)は「定数」なので値を変更することはできません。

const array = [1, 2, 3];
for (const el of array) {
  el = 'Hello World!!';  // elは定数なのでエラー
}

実は ループ定数は “変数” として宣言することもできます。for-of 変数は代入によって値を変えることができます。しかし、元の配列の値は変化しません。

const array = [1, 2, 3];
for (let el of array) {
  el = 'Hello World!!';  // for-of変数を書き換えても…
}
console.log(array); // 元の配列には変化なし → [1, 2, 3]

このように for-of 変数を書き換えることに意味はないので、const で宣言して for-of 定数としておくことをお勧めします。

繰り返し処理の中で配列の要素を書き換えたい場合は、前述の例のように for 文を使うことになります。しかし、配列の要素の書き換えについては、後述する map メソッドを使うほうが効率的です。

配列要素の追加と undefined

配列の要素を追加する一つの方法は、配列の末尾要素の次のインデックスを指定して値を代入することです。

const array = [0, 1, 2, 3];
array[4] = 'end';
console.log(array); // → [0, 1, 2, 3, 'end']

「配列の末尾要素の次のインデックス」は「配列の長さ」なので、length プロパティを使えば数え間違いがありません。

const array = [0, 1, 2, 3];
array[array.length] = 'end';
console.log(array); // → [0, 1, 2, 3, 'end']

実は、末尾要素の次に限らず、どこにでも値は代入できます。

const array = [0, 1, 2, 3];
array[5] = 'end';
console.log(array); // → [0, 1, 2, 3, empty, 'end']

その場合、抜かされてしまった要素には “undefined” という値が入ります(未定義という意味です)。Chrome のコンソールではなぜか “empty” と表現されますが、JavaScript のデータ型には empty はありません[1]。「undefined」が正確な表記です。

要素単体で console.log してみると undefined と表示されます。

console.log(array[4]); // → undefined

この undefined という値は配列に限らず、未定義の変数に与えられる値です。以下のコードで確認してください。

let x;  // 初期値なしで変数宣言
console.log(x);  // → undefined

if (x == undefined) {
  console.log('xが未定義だよ');
}

undefined は JavaScript のデータ型の一つなので、上記のように条件式にも書くことができます。文字列(String 型)ではないので 'undefined' のように引用符で囲わないことに注意してください。

アプリケーション開発では undefined (や null)がよく混入してエラーやバグの元になりますので、このように if 文で適宜対処する必要があります。本書ではこれ以上取り上げませんが、undefined という値があることは覚えておいてください。

配列の基本メソッド

冒頭でも触れたように、配列にはプロパティとメソッドが用意されています。プロパティは length だけなのですが、メソッドは大量にあります。ここでは代表的なものを紹介します。

先頭/末尾への追加と削除

まず、配列の先頭や末尾に要素を追加/削除するメソッドです。先頭/末尾 × 追加/削除で 4 個のメソッドがあります。

const array = [1, 2, 3];

array.push(4);     // 末尾に追加 [1, 2, 3, 4]
array.unshift(0);  // 先頭に追加 [0, 1, 2, 3, 4]
array.pop();       // 末尾の要素を削除 [0, 1, 2, 3]
array.shift();     // 先頭の要素を削除 [1, 2, 3]

// console.logを使って確認してみてください

このように、末尾要素の追加(push)と削除(pop)、先頭要素の追加(unshift)と削除(shift)が用意されています。要素を追加する push と unshift は、追加したい要素を引数で渡します。

push メソッドがあれば、array[array.length] = 値; という書き方はもう不要ですね。ただし、知識としては理解しておいてください。

要素を削除する pop と shift は、削除した要素を定数や変数で受け取ることができます。そういう意味では、「要素の削除」ではなく「要素の取り出し」といったほうが正確です。

const array = [1, 2, 3];

const tail = array.pop();
console.log(tail);  // → 3

let head = array.shift();
console.log(head);  // → 1

これら四つのメソッドは、配列を スタック/stackキュー/queue という 2 種類のデータ構造に見立てたときの操作に当たります。
スタックとキューは図を見るとわかりやすいでしょう。

スタックとキュー

スタックは積まれた本によく例えられます。LIFO(Last-In First-Out)といわれ、最後に入れたものが最初に取り出されるデータ構造です。配列の操作でいうと、末尾の要素の出し入れに当たります。
上が空いた筒に要素を出し入れする感じで、要素を上から押し込んだり(push)、要素が上からポンッと飛び出したり(pop)するイメージです。

一方、キューは順番待ちの行列です。順番が来たら先頭の人が行列を離れます。つまり、キューの自然な流れは shift(先頭の要素の取り出し)です。座って並んでいるときに行列が一つ進むと「席を一つずつシフトして…」とかいいますよね。スタックの LIFO に対して、キューは FIFO(First-In First-Out)といわれます。

途中の要素の追加と削除

もう一つ押さえておきたい基本メソッドが、配列の途中に要素を挿入・削除する splice メソッド です。使い方がやや複雑なので、ひとまず要素を 1 個だけ挿入・削除する記法だけ頭に入れておきましょう。

配列.splice(インデックス, 0, 挿入する要素) // 挿入
配列.splice(インデックス, 1) // 削除(抽出)

第 2 引数が 0 だと挿入、1 だと削除(抽出)です。
第 1 引数に挿入/削除する場所をインデックスで指定します。

const array = [1, 2, 3];

array.splice(1, 0, 'A');  // インデックス 1 に'A'を挿入 [1, 'A', 2, 3]
array.splice(2, 1);       // インデックス 2 を削除 [1, 'A', 3]

この splice メソッドと indexOf メソッド を組み合わせることで、配列の特定の要素を削除することができます。

indexOf メソッドは、引数で指定した値を配列が持っているか照合し、持っていた場合はそのインデックスを戻り値として返します。一致するものがなかった場合は -1 を返します。

const インデックス = 配列.indexOf(照合する値)

こうして得られたインデックスを splice メソッドの第 1 引数として渡せば、特定の値を削除することができます。

const array = ['a', 'b', 'c'];

const index = array.indexOf('b'); // 戻り値:1
if (index != -1) {
  array.splice(index, 1);  // → ['a', 'c']
}

なお、indexOf メソッドに指定した値が配列に存在しなかったときは戻り値が -1 となるので、splice メソッドを if 文で囲んでおく必要があります[2]

ラムダ式メソッド

配列のメソッドには、ラムダ式/Lambda expression といわれる形式のものがあります。ここでは配列のラムダ式タイプのメソッドから代表的なものを二つ紹介します。
メソッドの引数の中にコードを記述するので慣れるまで難しく感じるかと思いますが、慣れると for 文や for-of 文を使うより論理的に記述できます。

まずは、元の配列の各要素を加工して新しい配列を作りだす map メソッド です。

const 新しい配列 = 元の配列.map((要素) => 加工処理);

具体例を見てみましょう。
配列の各要素の後ろに「号」という文字を付けています。

const array = [1, 2, 3];

const newArray = array.map((el) => el + '号');
console.log(newArray);  // → [1号, 2号, 3号]

// 同じ処理をfor-of文で描くと…
const newArray2 = [];
for (const el of array) {
  newArray2.push(el + '号');
}

for-of 文と比べると、新しい配列をあらかじめ用意する必要がなく、push メソッドも必要ありません。変数/定数 el の意味はどちらも同じで、配列の各要素を順番に受け取っています(map メソッドのほうの el は変数になります)。

もう一つ、元の配列から条件を満たした要素だけを抽出して新しい配列を作り出す filter メソッド を紹介します。

const 新しい配列 = 元の配列.filter((要素) => 抽出条件);

具体例を見てみましょう。
1 ~ 5 の値を持った配列から 3 より大きい値を抽出し、新しい配列として生成します。

const array = [1, 2, 3, 4, 5];

const newArray = array.filter((el) => el > 3);
console.log(newArray);  // → [4, 5]

// 同じ処理をfor-of文で描くと…
const newArray2 = [];
for (const el of array) {
  if (el > 3) {
    newArray2.push(el);
  }
}

if 文を使わず、条件式を書くだけで要素を取捨選択しています。for-of 文の 6 行が 1 行で書けてしまうのは簡潔ですね。

ラムダ式メソッドの便利なところは、メソッドを . でつなげることで処理を連続して適用できる点です。

例えば、上に紹介した二つの例をつなげてみます。
filter メソッドを適用して [4, 5] にしてから map メソッドを適用して「号」を付けています。

const array = [1, 2, 3, 4, 5];

const newArray = array.filter((el) => el > 3).map((el) => el + '号');
console.log(newArray);  // → [4号, 5号]

余計にややこしい…という声も聞こえてきそうですが、慣れてくると「この部分はフィルタリング(要素の抽出)」で「この部分はマッピング(要素の加工)」というように処理がはっきりと区別できるのでバグが入りにくくなります。

今回紹介した二つのメソッドと同様に、新しい配列を生成するラムダ式メソッドに reduce メソッドがあります。また、新しい配列を生成するのではなく、配列の要素が特定の条件を満たすか否かを調べる some メソッドevery メソッドfind メソッドがあります。これらのメソッドはすべて、. でつなげることができます。

ちなみに、ラムダ式メソッドの本当のメリットは、繰り返し処理を for ブロックに入れずに “式” として書けることです。本格的なウェブアプリケーション開発でよく使われる JSX をはじめ、いくつかの場面でこの特徴が必須となるため、初心者のときから慣れておきましょう。同様に、条件分岐(if 文)を “式” で書ける三項演算子 条件式 ? A : B も JSX では必須です。

HTML への出力

十分に盛りだくさんのチャプターでしたが、最後に HTML へ出力して終わります。

前チャプターのときと同じように、index.html に記述された ul 要素に JavaScript で生成した li 要素を追加してみましょう。

index.html
<ul id="list"></ul>

配列データを使ったリスト表示

以下のようなリストを作りたいとします。

例題1

記述すべきコードをコメントで書いておいたので、先を読み進めずに、まず自分で挑戦してみてもよいと思います。

JavaScript
'use strict';

const etoList = ['ネズミ', 'ウシ', 'トラ', 'ウサギ'];

// ul要素を呼び出す(定数ul)

for (/* ... */) {
  // liエレメントを生成(定数li)
  // liエレメントの内容を配列arrayの各要素に
  // liエレメントをulエレメントの子要素にする
}

まず、ul 要素を JavaScript 側に呼び出します。id 属性が付いているので、処理の軽い getElementById メソッドを使います。

const ul = document.getElementById('list');

次に繰り返し処理を書いていきます。前チャプターでも触れましたが、for 文/for-of 文の中でしか生成した li エレメントは使用しないので、繰り返しブロックの中で定数 li を宣言します。

配列が一つなので for-of 文を使うと簡潔に記述できます。

for-of文で書いた場合
for (const eto of etoList) {
  const li = document.createElement('li');
  li.textContent = eto;  // etoに配列要素が入っている
  ul.appendChild(li);
}

今回は違いますが、配列を同時に扱う場合などには普通の for 文が活躍します。

for文で書いた場合
for (let i = 0; i < etoList.length; i += 1) {
  const li = document.createElement('li');
  li.textContent = etoList[i];  // 配列要素をインデックスで指定する
  ul.appendChild(li);
}

配列データのフィルタリングと加工

もう一つやってみましょう。さきほどと同じ HTML ファイルを使います。
配列 numList の要素から偶数だけを選び出し、その値の前に「No.」を付けてから、li 要素としてブラウザーに出力します。

例題2

const numList = [1, 2, 3, 4, 5, 6, 7, 8];

配列以外は 1 問目の手順とほぼ同じなので、書き換えていくとよいでしょう。
偶数の抽出と「No.」の付加は for 文でも for-of 文でも filter + map でも書けます。

まずは for-of 文で書いてみます。
偶数は「2 で割った余りが 0 となる数」ですので、num % 2 == 0 という条件式となります。

for-of文で書いた場合
for (const num of numList) {
  if (num % 2 == 0) {
    const li = document.createElement('li');
    li.textContent = 'No.' + num;
    ul.appendChild(li);
  }
}

では、これをラムダ式タイプのメソッドを使ってみましょう。
偶数を filter メソッドで抜き出すには次のように書きます。変数 el は配列 array の要素を表しますので、それを使ってフィルタリングの条件式を書きます。

filterメソッド
numList.filter((num) => num % 2 == 0)

次に、map メソッドを使って「No.」という文字を付けます。filter と map をドットで繋げると、map メソッドの変数 even にはフィルタリングされたあとの要素(偶数)のみが渡されます。

filterメソッド
numList.filter((num) => num % 2 == 0).map((even) => 'No.' + even)

その結果を新しい配列 evenList に代入して、あとは for-of 文でいつもどおりの作業です。

filterとmapで書いた場合
const numList = [1, 2, 3, 4, 5, 6, 7, 8];

const evenList = numList.filter((num) => num % 2 == 0).map((even) => 'No.' + even);

for (const num of evenList) {
  const li = document.createElement('li');
  li.textContent = num;
  ul.appendChild(li);
}
脚注
  1. Windows 版 Google Chrome のバージョン 88.0.4324.182 での表現です。 ↩︎

  2. JavaScript では、配列のインデックス -1 は末尾要素を示すので、array.splice(-1, 1) は末尾要素を削除します。 ↩︎