配列を征する者はJSを制す。
JavaScriptのスマートな配列操作テクニック

211
130
586

JavaScriptでコードを記述する際、配列の各要素について処理をするケースは頻出します。開発の現場で配列操作の処理を見ていると、次のようなケースがよくあります。

  • filter()every()など配列のメソッドで処理を簡潔に書けるのに、forEach()メソッドやfor ... of文の冗長なコードを書いている
  • 書いても意味のないArray.from()を使っている

コード的には問題なく、アプリケーションは意図的に動作しているかもしれません。しかし、冗長な処理は可読性が低下し、予期せぬバグを誘発し、開発現場に混乱をもたらします。

本記事では、配列操作でよく見かける冗長な処理を、簡潔な処理で置き換える方法について解説します。いずれの処理も数年前から開発の現場で使えるので、この機会に知識をアップデートしましょう。

要素が1つでも条件に合致するかを調べる

ユーザー検索システムを作ることを考えてみましょう。サーバーからは次の配列データが返ってくるとします。

const dataList = [
  { id: 1, name: "鈴木" },
  { id: 2, name: "田中" },
  { id: 3, name: "ゴンザレス" }
];

こちらの配列データを用いて、「IDが5のデータがなければログを出力する」という処理を作るにはどうしたらよいでしょうか?

冗長な処理は次の通り。対象のIDが見つかったかどうかをfoundIDFlagにて管理し、for文でdataListの各要素を参照しています。

👎 冗長な処理

let foundIDFlag = false;

for (const data of dataList) {
  if (data.id === 5) {
    foundIDFlag = true;
    break;
  }
}

if (foundIDFlag === false) {
  console.log("IDが5のデータはありません");
}

このようなケースではsome()関数を使うとよいでしょう。

some()関数は、配列内の要素で1つでも条件に合致するものがあればtrueを返します。逆に言えば、1つも条件に合致しなければfalseが返ります。

👍 簡潔な処理

const foundIDFlag = dataList.some(data => data.id === 5);

if (foundIDFlag === false) {
  console.log("IDが5のデータはありません");
}

▼ 実行結果

似た関数として、配列内のすべての要素が条件に合致するかを調べるevery()関数というものもあります。

const noValidIDFlag = dataList.every(data => data.id !== 5);

if (noValidIDFlag === true) {
  console.log("IDが5のデータはありません");
}

■ 配列.some()

  • 意味:配列内の要素で1つでも条件に合致するものがあるかどうか?
  • 戻り値:真偽値

■ 配列.every()

  • 意味:配列内のすべての要素が、条件に合致するかどうか?
  • 戻り値:真偽値

条件に合致する要素から配列を作成する

次の配列データがあるとします。この配列から、「30歳以上の人物データの名前からなる配列」を作るにはどうしたらよいでしょうか?

const dataList = [
  { age: 40, name: "鈴木" },
  { age: 30, name: "田中" },
  { age: 21, name: "ゴンザレス" }
];

冗長な処理は次の通り。forEach文でdataListの各要素をチェックし、条件に合致するデータを配列に格納しています。

👎 冗長な処理

const over30List = [];

dataList.forEach(data => {
  if (data.age >= 30) {
    over30List.push(data.name)
  }
});

console.log(over30List);

このようなケースではfilter()関数とmap()関数を使うとよいでしょう。filter()関数は、配列の要素から条件に合致する要素からなる新しい配列を作成します。map()関数は、コールバック関数を使ってある配列を別の配列に置き換えます。

👍 簡潔な処理

const over30List = dataList
  .filter(data => data.age >= 30)
  .map(data => data.name);

console.log(over30List);

■ 配列.filter()

  • 意味:コールバック関数に合格した配列を生成する
  • 戻り値:配列

■ 配列.map()

  • 意味:コールバック関数によって、新しい配列を生成する
  • 戻り値:配列

多次元の配列を1次元にする

次のような配列データがあるとします。この配列から、hash_tagsの要素だけを集めた1次元の配列を作るにはどうしたらよいでしょうか?

const dataList = [
  {
    tweet: "今日は朝から仕事で大変だなあ",
    hash_tags: ["出勤", "早起き", "イマソラ"]
  },
  {
    tweet: "ランチで焼き肉を食べてしまった",
    hash_tags: ["ランチ", "焼き肉"]
  },
  {
    tweet: "家に帰って新作のゲームをしよう!",
    hash_tags: ["帰宅", "ゲーム", "日常"]
  }
];

冗長な処理は次のとおりです。dataListの配列をループさせた後、さらに各要素にある配列hash_tagsについてループしています。

👎 冗長な処理

const tagList = [];
dataList.forEach(data => {
  data.hash_tags.forEach(tag => {
    tagList.push(tag);
  });
});

console.log(tagList);

配列の多階層を解除し、別の配列に置き換えたいので、flatMap()メソッドを使うとよいでしょう。flatMap()メソッドは、map()メソッドの後に「flat()メソッド」を実行します。flat()メソッドとは、多次元の配列を1次元にする(フラット化する)ためのメソッドです。

👍 簡潔な処理

const tagList = dataList.flatMap(data => data.hash_tags);

console.log(tagList);

▼ 実行結果

■ 配列.flat()

  • 意味:配列をフラット化する。引数でフラット化する配列の深さを指定でき、初期値は1
  • 戻り値:配列

■ 配列.flatMap()

  • 意味:map()の後にflat()を実行する。flat()の深さは1
  • 戻り値:配列

Object.entries()のコールバック関数の引数

Object.entries(対象オブジェクト)を実行すると、[[キー1, バリュー1], [キー2, バリュー2],...]という配列が返ります。

次のオブジェクトを例に考えてみましょう。

const myObject = {
  age: 18,
  name: "鈴木",
  address: "福岡"
};

オブジェクトに対してObject.entries(対象オブジェクト)を実行し、戻り値の配列をforEach()で処理をする場合、次のようなコードが考えられます。forEachのコールバック関数の引数entryに注目してください。

👎 冗長な処理

Object.entries(myObject)
  .forEach(entry => {
    console.log(`キー: ${entry[0]}`);
    console.log(`バリュー: ${entry[1]}`);
  });

entry[0]entry[1]というアクセスの仕方は冗長です。どちらがキーで、どちらがバリューなのかが瞬時に判断できません。「配列の分割代入」を使ってコードを簡潔にしましょう。次のA, Bが同じ意味であることに注目します。

// A
const entry = [キー, 値];
// B
const [key, value] = [キー, 値];

前述の各値について処理を、配列の分割代入を用いて書き換えると次のようになります。entry[0]entry[1]でアクセスするよりも、簡潔で直感的な処理となりました。

👍 簡潔な処理

Object.entries(myObject)
  .forEach(([key, value]) => {
    console.log(`キー: ${key}`);
    console.log(`バリュー: ${value}`);
  });

▼ 実行結果

document.querySelectorAll()Array.from()で不用意に囲むべからず

document.querySelectorAll()は引数のセレクターに合致した要素を返すメソッドです。戻り値の型はNodeListです。

返された各要素に対してforEach()で処理をしたい場合、Array.from()で囲むコードをなぜかよく見かけます。

👎 冗長な処理

Array.from(document.querySelectorAll(".foo"))
  .forEach((element) => {
    element.addEventListener("click", () => {
      console.log("クリックされました");
    });
});

document.querySelectorAll()の戻り値のNodeListにはforEach()メソッドがあるので、わざわざArray.from()を使う必要はありません。次の処理で充分です。

👍 簡潔な処理

document.querySelectorAll(".foo")
  .forEach((element) => {
    element.addEventListener("click", () => {
      console.log("クリックされました");
    });
});

Array.from()を使うのは、every()filter()などの配列用のメソッドを使いたいケースです。

次の関数は、チェックボックス用input要素がすべてチェックされているかを調べる関数です。配列用のevery()を使いたいので、Array.from()を用いています。

const checkedAll = () =>
  Array.from(document.querySelectorAll(`input[type="checkbox"]`)).every(
    (element) => element.checked
  );

Array.from()の代わりにスプレッド構文を用いることもできます。

const checkedAll = () =>
  [...document.querySelectorAll(`input[type="checkbox"]`)].every(
    (element) => element.checked
  );

ちなみに、Array.from(document.querySelectorAll(セレクター)).forEach(処理)Array.from()は、IE11のためだと思っている方もいるようですが、IE11ではArray.from()が使えません。document.querySelectorAll()の各要素に対してforEach()を使いたければ、ポリフィルを使うのが手軽でしょう。

配列回りの処理をスッキリさせて簡潔なコードを書こう

配列操作はフロントエンド開発では頻出します。簡潔でバグが発生しづらい処理にすることは、アプリケーション品質の向上、チーム全体の開発効率の向上に繋がります。

JavaScriptには次々と新しい処理が生まれているので、「今自分が書いているコードが最適なものなのか?」を常に自問自答しながら、知識をアップデートしましょう。