Arrayの隠れた便利なメソッドのススメ

JavaScriptを書いていると、何だかんだで配列のお世話になるケースが非常に多いと思います。仕様上、オブジェクト(連想配列)にはキーの順序が保証が無いので、順序を決めて複数の対象を処理するケースでは配列を使います。

この記事では、ECMAScript(JavaScriptのベース仕様)5以降に追加されたメソッドのうち、便利なのにいまいち知名度が低い、なかなか見かけないなぁというメソッドを、独断と偏見でまとめます。

Array.methodArray.prototype.methodについて
前者はいわゆるクラスメソッド、後者はいわゆるインスタンスメソッドです。クラスメソッドは各配列から呼ぶことができないので、毎回Array.methodArrayから書き始めなければなりません。インスタンスメソッドは各配列から呼ぶので、[1, 2, 3].methodと記述します。

Array.from

配列を生成する新メソッドです。Array-Likeオブジェクト(配列のように扱えるオブジェクト)を配列に変換するときに使えます。

コールバック関数を渡して、Array.prototype.mapのように各要素を好きな形に変換することもできます。個人的にはこの点が特に便利に感じるので、よく使います。

const divs = document.querySelectorAll('div')

/* Before */

// forを使って1つずつ回したり…
var arr = []
for (var i = 0; i < divs.length; i++) {
  arr.push(divs[i])
}

// Array系メソッドをcall/applyで転用したり…
Array.prototype.forEach.call(divs, function (div) {
  // ...
})

/* After */

const divsArray = Array.from(divs)
// div要素の配列
const divsTextArray = Array.from(divs, div => div.textContent)
// divのテキストだけの配列

Array.prototype.every

配列の要素を1つずつ回し、全要素が与えられた条件を満たしていたらtrue、1つでも条件を満たしていなかったらfalseを返すメソッドです。

例えば、5教科のテストの点数を配列にして、1つでも30点を下回っていたら補習決定など、配列全体の判定に利用することができます。

const A = [68, 45, 90, 82, 51]
const B = [73, 100, 87, 27, 96]

/* Before */

var isFailing = false
for (var i = 0; i < A.length; i++) {
  if (A[i] < 30) {
    isFailing = true
    break;
  }
}

/* After */

A.every(score => score >= 30)
// false
B.every(score => score >= 30)
// true

Array.prototype.some

配列の全ての要素のうち、1つでも条件を満たすものがあればtrueを、全要素が満たさないならfalseを返すメソッドです。

everyとは裏表の関係にあり、条件を裏返せばeveryと同じ処理となります。

const A = [68, 45, 90, 82, 51]
const B = [73, 100, 87, 27, 96]

A.some(score => score < 30)
// false
B.some(score => score < 30)
// true

Array.prototype.includes

対象が配列に含まれているかどうかをチェックし、true/falseで返します。

今までもArray.prototype.indexOfを使ってチェックすることができましたが、より短い記述が可能となりました。

const array = ['a', 'c', 'f', 'b', 'e', 'd']

/* Before */

array.indexOf('c') !== -1
// true
array.indexOf('g') !== -1
// false

/* After */

array.includes('c')
// true
array.includes('g')
// false

「include」という単語の意味をそのままとらえられるので、indexOfよりも「何をしているか」がわかりやすいというのも利点だと思います。

Array.prototype.findIndex

配列から条件を満たす要素を探し、見つかればそのインデックスを、見つからなければ-1を返します。

indexOfと非常に似たメソッドですが、findIndexでは条件をコールバック関数で指定するようになっているため、配列の要素がオブジェクトである場合にも使えます。

const array = [
  { a: 1 },
  { a: 2 },
  { a: 3 },
  { a: 4 },
  { a: 5 }
]

array.indexOf({ a: 4 })
// -1
array.findIndex(obj => obj.a === 4)
// 3

まったく同じプロパティを持つオブジェクトでも、別なオブジェクトであることに変わりはないので、indexOfではインデックスを得ることができません。

例えばArray-Likeオブジェクトを配列に変換して、条件を満たす要素を探すようなケースでは、findIndexや、次に紹介するfindの出番になります。

Array.prototype.find

findIndexは該当する要素のインデックスを得るメソッドでしたが、findは要素そのものが返ります。インデックスではなく要素が必要な場合はこちらを使います。

const array = [
  { a: 1 },
  { a: 2 },
  { a: 3 },
  { a: 4 },
  { a: 5 }
]

array.find(obj => obj.a === 4)
// { a: 4 }

Array.prototype.reduce

配列の要素を順に処理します。1つ前の処理の結果を、次の処理に引き継ぐことができるという点がforEachと異なります。

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

let total_forEach = 0
array.forEach(num => {
  total_forEach += num
})

const total_reduce = array.reduce((prev, current) => prev + current)

console.log(total_forEach)  // 15
console.log(total_reduce)   // 15

それから、コールバック関数でreturnされる値が次の処理に引き継がれる、という仕組みを利用して「隣同士の要素を処理する」という用途にも使うことができます。

個人的に最もお世話になったのはPromiseの直列化で、前のPromiseが終わってから次のPromiseを実行するというのをどんどん縦に繋げていく場合、reduceを使うと短く書くことができます。

const urls = [
  'url1',
  'url2',
  'url3',
  'url4',
  'url5'
]

urls.map(url => () => {
  return new Promise(resolve => fetch(url).then(resolve))
}).reduce((prev, current) => prev.then(current), Promise.resolve())

Promise.then().then().then()...と数珠つなぎにするところを、対象がいくつであっても同じ書き方で処理できるので、重宝しています。

まとめ

いかがでしょうか。ここで紹介したメソッドは、「できないことができるようになる」というよりも、

  • より短く書ける
  • 一時変数を使わずに書ける
  • よりわかりやすく書ける
  • より浅いネスト(入れ子)で書ける

というもので、もしかするとあまり魅力的に感じなかったかもしれません。

しかし、より大きなプロジェクトでは、こうした小さな違いが積み重なって大きな差になってきます。日頃からこうした意識を持って書くことはとても大事だと思います。

もしこれらのメソッドの中に、知らなかったあるいは使ったことが無いものがあれば、少しずつ取り入れてみてください。

番外編:Array.prototype.flatten(策定中)

策定中のメソッドflattenという便利そうなメソッドがありました。lodashには既にありますが、2次元や3次元などの配列を「ならす」ためのメソッドです。

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

array.flatten()
// [1, 2, 3, 4, 5, 6]

いずれこのように感じで使えるようになるのではないかと思います。