👗

この夏押さえておきたいJavaScriptの配列操作コレクション

2024/08/01に公開4

こんにちは!サイボウズ株式会社フロントエンドエンジニアの おぐえもん(@oguemon_com) です。

サイボウズの技術ブログの夏フェス・CYBOZU SUMMER BLOG FES '24が始まりました!企画の一環として、フロントエンドの記事が今日から20日連続投稿されますので、みなさんお楽しみに!

今回は、コーディングに手放せない存在の1つ「配列」をテーマに、JavaScriptの配列操作の中でも普段使いしやすいものをピックアップして、細かいテクニックや比較的新しい話を交えながらお届けします。

身近ながらも今まで見落としていたポイントを拾ってもらえたらと思います!

// 凡例
const arr = ["a", "b", "c"]
arr // console.logした出力結果 ← 出力結果に対するコメント

配列を作る

配列は、複数の値の集合をまとめて扱うことができるデータ構造です。JavaScriptにおける配列はオブジェクトの一種です。

配列を1から作成する方法はたくさんあります。

// 全て同じ中身の配列ができる
new Array("Alice", "Bob")
Array("Alice", "Bob")
Array.of("Alice", "Bob")
["Alice", "Bob"]

new Array()Array())とArray.of()の違いは、引数に単一の整数値を入れたときの挙動にあります。new Array()では単一の整数値を入れると、その長さの配列が作成されます。

new Array(3) // [空 × 3] ← 長さ3
Array.of(3)  // [3] ← 長さ1

他にも、配列を柔軟に作成できるArray.from()があります。第1引数には、文字列などの反復可能オブジェクトや、NodeListdocument.getElementsByName()などで得られるノード集合)などの配列風オブジェクトを入れることができ、こうした様々な形式の値を配列に変換してくれます。

// stringを入れると1文字ずつ分割される
Array.from('cybozu') // ["c", "y", "b", "o", "z", "u"]

// NodeListを入れると、NodeListの中にある各ノードの配列になる
// ↓はHTMLに`<input name="test">`を2つ配置したときの例
Array.from(document.getElementsByName("test")) // [input, input]

// lengthプロパティのみを持つオブジェクトを入れると、その長さの配列ができる
Array.from({ length: 3 }); // [undefined, undefined, undefined]

// 第2引数に関数を入れると、その関数を使って配列を作成できる
Array.from([1, 2, 3], (e, i) => {
  // eには第1引数から得た各要素、iには0はじまりの添字番号が入る
  return e + i; // ここの返り値が各要素の値になる
}); // [1, 3, 5]

最近だと、Array.fromAsync()というものも使えます。第1引数に、Promiseが含まれる配列や、AsyncGeneratorなどの非同期反復可能オブジェクト入れることができ、非同期処理で配列を生成します。

// 1秒後に引数の値を返す非同期関数
const wait1sec = async (num) => {
    await new Promise(resolve => setTimeout(resolve, 1000))
    return num;
}

await Array.fromAsync([
    wait1sec(1),
    wait1sec(2),
    wait1sec(3),
], (e, i) => {
    return e + i;
}); // (1秒後に)[1, 3, 5]

// 非同期ジェネレーター関数
async function* getAsyncGenerator() {
  yield await wait1sec(1);
  yield await wait1sec(2);
  yield await wait1sec(3);
}

// 非同期ジェネレーター関数が返すAsyncGeneratorから配列を作成
await Array.fromAsync(getAsyncGenerator()) // (3秒後に)[1, 2, 3]

配列を使ってループする

原始的な方法から、配列が提供するメソッドを使う方法まで様々です。

const arr = ["a", "b", "c"]

// for文の伝統的な記法を使う方法
for (let i = 0; i < arr.length; i++) {
  arr[i] // ループのたびに各要素が取れる
}

// for文のinを使う方法
for (const i in arr) {
  i // ループのたびに文字列の"0", "1", "2"が取れる
  arr[i] // ループのたびに各要素が取れる
}

// for文のofを使う方法
for (const e of arr) {
  e // ループのたびに各要素が取れる
}

// forEachメソッドを使う方法
arr.forEach((e) => {
  e // ループのたびに各要素が取れる
})

for(;;)ではconstが使えない一方で、for(in)for(of)ではconstが使えます。

for文のcontinue文に相当することはarr.forEach()でもreturn文により可能です。一方で、for文のbreak文に相当することはarr.forEach()ではできません。

const arr = ["a", "b", "c"]

for(const e of arr) {
  if (e === "a") continue; // 次の要素の繰り返しに進む
  if (e === "b") break; // for文を抜ける
}

arr.forEach((e) => {
  if (e === "a") return; // 次の要素の繰り返しに進む
  // breakはできない!
})

配列を操作する

配列には、配列をいじるための方法がたくさん用意されています。知っていると便利なものをいくつか紹介します。

要素を1つだけ取ってくる

const arr = ["a", "b", "c", "d"]

// 添え字番号を指定する王道の方法
arr[0] // "a"
arr[-1] // undefined
arr[arr.length - 1] // "d" ← 末尾を取れる!

// atメソッドを使う方法
arr.at(0)  // "a"
arr.at(-1) // "d" ← 末尾を取れる!

// findメソッドを使う方法
arr.find(e => e === "b" || e === "c") // "b"
arr.find(e => e === "e") // undefined

// shiftメソッドを使う方法
arr.shift() // "a" ← 先頭を取れる!
arr // ["b", "c", "d"] ← 元の配列が変わる!

// popメソッドを使う方法
arr.pop() // "d" ← 末尾を取れる!
arr // ["b", "c"] ← 元の配列が変わる!

配列末尾を取る方法はarr[arr.length - 1]が定番でしたが、最近ではarr.at(-1)がシンプルです。

arr.shift()arr.pop()は、元の配列arrから先頭・末尾の要素がなくなってしまう点に注意が必要です!配列操作のメソッドには、自身の配列に変更を加えるものが少なからず存在し、このようなメソッドを破壊的メソッドと呼びます。配列操作のメソッドを使う時は、それが破壊的か否かの注意が常に必要です。

arr.find()は、複雑な条件のもとで1つの要素を探すときに便利です。複数の要素がヒットする場合も、最初に見つかった1つしか返しません。最近では、arr.findLast()というメソッドもあり、こちらは逆に最後に見つかった1つを返します。

const arr = ["a", "b", "c", "d"]
arr.findLast(e => e === "b" || e === "c") // "c"
arr.findLast(e => e === "e") // undefined

要素を複数取ってくる

const arr = ["a", "b", "c", "d"]

// sliceメソッドを使う方法
arr.slice(1, 3)   // ["b", "c"]
arr.slice(-3, -1) // ["b", "c"] ← 末尾からカウントできる!

// filterメソッドを使う方法
arr.filter(e => e === "b" || e === "c") // ["b", "c"]
arr.filter(e => e === "e") // []

arr.slice()が要素番号の範囲を明示する形で、arr.filter()が値に基づく抽出条件を設定する形で取得します。

arr.slice()は、第1引数が範囲の始点、第2引数が終点を表し、ともに0始まりの添字番号である共通点があります。しかし、得られる配列は第2引数に入れた終点の手前までのもので、終点自体は含まれません。マイナス値を入れたときの扱いはarr.at()に近く、例えば-1が末尾要素になります。

要素を探す

今までの例も「探す」の一種でしたが、これ以外にも、要素の有無を確認するメソッドや、その位置を確かめるメソッドがあります。

const arr = ["a", "b", "c", "d"]

// includesメソッドを使う方法
arr.includes("c") // true
arr.includes("e") // false

// someメソッドを使う方法
arr.some(e => e === "c") // true
arr.some(e => e === "e") // false

// everyメソッドを使う方法
arr.every(e => typeof e === "string") // true
arr.every(e => e === "c") // false

arr.includes()は引数の値と一致する要素が存在するかを確かめ、arr.some()は検索条件を細かく設定できます。arr.every()は、1つだけでなく、全ての要素が条件を満たさないとtrueを返しません。

// arr[1]とarr[2]が同じ"o"を持つ
const arr = ["f", "o", "o", "d"]

// indexOfメソッドを使う方法
arr.indexOf("o") // 1 ← 最初に見つかったもの
arr.indexOf("e") // -1

// lastIndexOfメソッドを使う方法
arr.lastIndexOf("o") // 2 ← 最後に見つかったもの
arr.lastIndexOf("e") // -1

arr.indexOfは、引数の値と一致する最初の要素の添字番号を返します。見つからないときは-1を返します(falseではありません!)。arr.lastIndexOfは逆に最後に見つかった要素の添字番号を返します。

要素の値を埋め尽くす

const arr = [1, null, 3]
arr.fill(0) // [0, 0, 0]
Array(5).fill(0) // [0, 0, 0, 0, 0] ← 初期化が楽々

arr.fill()を使うと、配列を第1引数に入れた値で埋め尽くします。arr自身に変更を加え、変更された後の配列を返します。

arr.fill()には第2引数と第3引数があり、値を埋める範囲を指定できます。入れる数値に基づく範囲の決まり方は、arr.slice()と同じ感じで、例えば終点の要素番号は含まれません。

const arr = [1, null, 3, 4, 5]
arr2.fill(0, 1, 3) // [1, 0, 0, 4, 5]

配列をくっつける

const arr1 = ["a", "b"];
const arr2 = ["c", "d"];

// スプレッド構文を使う方法
[...arr1, ...arr2] // ["a", "b", "c", "d"]
[...arr1, "e"]     // ["a", "b", "e"]
["e", ...arr1]     // ["e", "a", "b"]

// concatメソッドを使う方法
arr1.concat(arr2)  // ["a", "b", "c", "d"]
arr1.concat("e") // ["a", "b", "e"]
arr1.concat(arr2, "e") // ["a", "b", "c", "d", "e"] ← 無数に連結できる!

// shiftメソッドを使う方法
arr1.unshift("e", "f") // 4 ← 新たな長さが返る!
arr1 // ["e", "f", "a", "b"] ← 先頭に追加される!

// pushメソッドを使う方法
arr1.push("g", "h") // 6 ← 新たな長さが返る!
arr1 // ["e", "f", "a", "b", "g", "h"] ← 末尾に追加される!

arr.concat()も有用ですが、非配列の後ろに配列を連結させることもできるスプレッド構文が便利です。

arr.shift()arr.push()は、arr自身に要素を追加するためのメソッドである点に注意が必要です。前述のarr.shift()arr.pop()と性質が似ていますが、返り値は変更後の配列でなく、変更後の配列の長さです。

また、ここで出たメソッドはどれも引数の個数を好きなだけ設定できるので、限界こそあれど実用上は無数に連結できます。

配列を並び替える

昇順(小さい順)への並び替え

const arr1 = [3, 2, 4, 1];
const arr2 = [
  { name: "Alice",   salary: 200_000 }, // 単位:ドル
  { name: "Bob",     salary: 180_000 }, // 単位:ドル
  { name: "Charlie", salary: 250_000 }, // 単位:ドル
]

// sortメソッドを使う方法
arr1.sort() // [1, 2, 3, 4] ← 昇順に並ぶ!
arr1 // [1, 2, 3, 4] ← 元の配列も変わる!
arr2.sort((a, b) => a.salary - b.salary)
// [
//   { name: "Bob",     salary: 180_000 },
//   { name: "Alice",   salary: 200_000 },
//   { name: "Charlie", salary: 250_000 },
// ] ← 指定した条件による並び替えもできる!

const arr3 = [3, 2, 4, 1];

// toSorted()を使う方法
arr3.toSorted() // [1, 2, 3, 4]
arr3 // [3, 2, 4, 1] ← 元の配列は変わらない!

arr.sort()が定番ですが、arr自身の要素を並び替える点(破壊的メソッドである点)に注意が必要です。返ってくる配列は、arrと同じものです。

元の配列を変えたくないニーズに応える形で、最近はarr.toSorted()が使えるようになりました!これは、arr.sort()と同じ使い方ですが、元の配列を変えず、並び替え後の新しい配列を返します。このように、元の配列に変更を加えないメソッドを、破壊的メソッドに対して非破壊的メソッドと呼びます。近年、配列操作の非破壊的メソッドが複数追加されており、後に出てくるarr.to***()系のメソッドもその一部です。

arr.sort()arr.toSorted()のどちらも、引数には並び替え条件を示す関数を設定できます。これを使えばオブジェクトの並び替えもできて便利です。具体例は上にある通りですが、次のような関数にする必要があります。

  1. 比較対象の2つを引数abを持つ
  2. 次の数値を返す
    • aの方を大きなものと扱うときはプラスの値
    • aの方を小さなものと扱うときはマイナスの値
    • 両者を同じ大きさとして扱うならば0

逆順への並び替え

const arr1 = [3, 2, 4, 1];

// reverseメソッドを使う方法
arr1.reverse() // [1, 4, 2, 3] ← 逆順に並ぶ!
arr1 // [1, 4, 2, 3] ← 元の配列が変わる!

const arr2 = [3, 2, 4, 1];

// toReversedメソッドを使う方法
arr2.toReversed() // [1, 4, 2, 3] ← 逆順に並ぶ!
arr2 // [3, 2, 4, 1] ← 元の配列は変わらない!

reverse()が定番ですが、こちらもsort()と同様、arr自身の要素を並び替える点に注意が必要で、返ってくる配列は、arrと同じものです。

元の配列を変えたくないニーズに応える形で、最近はarr.toReversed()が使えるようになり、こちらもarr.toSorted()と同じく元の配列を変えず、並び替え後の新しい配列を返します。

ちなみに、sort()toSorted())とreverse()toReversed())を組み合わせると、降順に並び替えることができます。

const arr = [3, 2, 4, 1];
arr.toSorted().toReversed() // [4, 3, 2, 1]

// 数値の配列ならこれでもできる
arr.toSorted((a, b) => b - a) // [4, 3, 2, 1]

柔軟に加工する

const arr1 = [3, 2, 4, 1];

// mapメソッドを使う方法
arr1.map(e => e * 2) // [6, 4, 8, 2]

// spliceメソッドを使う方法
arr1.splice(1, 2, "a", "b") // [2, 4] ← 除去された要素の配列
arr1 // [3, "a", "b", 1] ← 元の配列が変わる!

const arr2 = [3, 2, 4, 1];

// toSplicedメソッドを使う方法
arr2.toSpliced(1, 2, "a", "b") //  [3, "a", "b", 1] ← 加工後の配列
arr2 // [3, 2, 4, 1] ← 元の配列は変わらない!

arr.map()は、配列の各要素を指定した関数の返り値に変更した新たな配列を返します。全ての要素を個々の要素の事情に応じて好きな値に変更できるので、ものすごく柔軟です。

arr.splice()は、要素の除去、差し替えができます。第1引数で指定した要素番号から、第2引数で指定した個数の要素を除去して、そこに第3引数以降の値を新しく挿入します。第1引数にはマイナスの数も使え、例えば-1ならば末尾要素を指します。返り値は、除去された要素の配列で、arr自身が変更される点に注意が必要です。

最近使えるようになったarr.toSpliced()では、加工後の配列が返り、arr自身を変更しません。

入れ子を平たくならす

const arr1 = [
  [10, 11, 12],
  [20, 21],
  [30],
];
const arr2 = [
  [[10, 11], 12], // さらに深い入れ子
  [20, 21],
  [30],
];

// flatメソッドを使う方法
arr1.flat()  // [10, 11, 12, 20, 21, 30]
arr2.flat()  // [[10, 11], 12, 20, 21, 30] ← ならしに限界がある
arr2.flat(2) // [10, 11, 12, 20, 21, 30] ← 引数を入れると限界を越えられる

const arr3 = [1, 2, 3];

// flatMapメソッドを使う方法
arr3.flatMap(e => e > 1 ? [e, 0] : 9) // [9, 2, 0, 3, 0]

// flatMapはこれと変わらない
arr3.map(e => e > 1 ? [e, 0] : 9).flat() // [9, 2, 0, 3, 0]

arr.flat()は、入れ子になった配列を平たくならします。引数に入れた数字の深さだけならします。引数を入れないと1階層分しか平たくしないので、入れ子が2以上の深さになっていたら、2階層目以降は入れ子として残ります。

arr.flatMap()は、arr.map().flat()と本質的に同じで、一旦配列をarr.map()してから、その結果.flat()します。

もしarrに入れ子がないことが分かっているならば、条件に応じて[]を返す関数をflatMap()に設定することで、事実上のarr.filter()を機能させるワザがあります。この方法は、TypeScriptでarr.filter()による型の絞り込みができなかった時代に有用でした。

const arr = [1, null, 3]
arr.flatMap(e => {
    if (e === null) return [] // 除去したい要素には空配列を返す
    return e * 2
}) // [2, 6] ← フィルターと要素加工を同時にできる

配列を1つの値にまとめる

const arr1 = [1, 2, 3, 4]
const arr2 = ["a", null, "c", "d"]

// reduceメソッドを使う方法
arr1.reduce((prev, cur) => prev + cur, 0) // 10
arr2.reduce((prev, cur) => prev + "-" + cur) // "a-null-c-d"

// joinメソッドを使う方法
arr2.join("-") // "a--c-d"

arr.reduce()は、先頭2要素を第1引数の関数に放りこんで、関数から得られた値と3つ目の要素を再び同じ関数に放り込んで…ってのを全要素に対して行うことで、配列から1つの値を導出します。上にあるarr1の例は全要素の総和を求めています。第2引数に初期値を設定することができて、そのときは、はじめに初期値と先頭要素が関数に放り込まれます。

arr.join()は、arr.reduce()の特化型のような関数で、配列を文字列として連結することに特化しています。引数には区切り文字を指定できます。また、arr.join()にはnullundefinedの要素を空文字("")として扱うなどの特徴があります。

ちなみに、arr.reduce()の他にもarr.reduceRight()というのもあり、これはarr.reduce()の関数放り込みを逆順(後ろの要素からの順)にしたものです。

const arr = ["a", "b", "c"]
arr.reduceRight((prev, cur) => prev + "-" + cur) // "c-b-a"

重複を排除する

const arr = ["a", "i", "o", "i"]
Array.from(new Set(arr)) // ["a", "i", "o"]

配列には重複値を排除するメソッドが存在しませんが、Setオブジェクトを利用することで重複の排除ができます。

Setオブジェクトは、一意の値を格納するオブジェクトで、重複値を許容しません。配列値をもとにSetを作成すると、その過程で重複値が排除されます。そして、作成したSetに基づいて再び配列を生成すると、重複のない配列が出来上がります。

文字列や数値などのプリミティブ型の配列では重複が排除されますが、オブジェクト型の配列では内部データの性質上、必ずしも中身の値の重複が排除されない点に注意が必要です。

const arr = [
  { price: 100 },
  { price: 200 },
  { price: 100 },
]
Array.from(new Set(arr))
// [
//   { price: 100 },
//   { price: 200 },
//   { price: 100 },
// ] ← 中身の値の重複は排除されない!

おわり

この夏押さえておきたいJavaScriptの配列操作コレクションでした。

配列はプログラミングに欠かせない主役級アイテムの1つ。だからこそ、鮮やかに使いこなせるとたくさんのシーンで重宝するはずです。

今回ご紹介した方法を自分の実装スタイルに取り込んでみて、シンプルできれい見えするコードを狙ってみましょう!

GitHubで編集を提案
サイボウズ フロントエンド

Discussion

nokogirinokogiri

要素を探すに find があってもいいかもですね!

おぐえもんおぐえもん

「要素を1つだけ取ってくる」のところに置いてたんですが、メソッド名の意味を考えたら確かにかなり「要素を探す」寄りですね笑

MelodyclueMelodyclue

null、undefinedを削除する.filter(Boolean)も!

おぐえもんおぐえもん

このような感じでFalsyな値を全部省けんですね。知らなかったです!!

const arr = ['hoge', '', null, undefined];
console.log(arr.filter(Boolean)); // ['hoge']

情報提供ありがとうございます!