JavaScriptの配列のベターな使い方4点


先日ヘルシオホットクックを衝動買いして、あまりの便利さにメルトダウン中です。
みんなも買おうヘルシオホットクック。

以下はpacdivによる記事、Here’s how you can make better use of JavaScript arraysの日本語訳です。

Here’s how you can make better use of JavaScript arrays

この記事はさっくりと読めます。本当だよ。

この数ヶ月の間に、私がチェックしているリポジトリで4種類のプルリクエストがやってきていることに気付きました。
これらの間違いは全て自分で作っていたところだったので、この記事を書いています。
配列メソッドを正しく使うようにしましょう。

Replacing Array.indexOf with Array.includes

Array.indexOfではなくArray.includesを使う。

『Arrayから探したいものがあるならArray.indexOfを使おう』
JavaScriptを学んでいたとき、何かのコースにこのような文章が書いてありました。
このセンテンスは全くもって正しいです。

MDNのドキュメントには、最初に見つけた要素のインデックスを返すと書かれています。
従って、そのインデックスを後で使うのであればArray.indexOfが正解です。

しかし、単に配列に値が含まれているか否かだけを知りたいだけであるときはどうでしょうか。
含まれているかを知りたいだけならbool値で十分です。
この場合はbooleanを返すArray.includesを使った方がいいです。

    'use strict';

    const characters = [
      'ironman',
      'black_widow',
      'hulk',
      'captain_america',
      'hulk',
      'thor',
    ];

    console.log(characters.indexOf('hulk')); // 2
    console.log(characters.indexOf('batman')); // -1

    console.log(characters.includes('hulk')); // true
    console.log(characters.includes('batman')); // false

Using Array.find instead of Array.filter

Array.filterではなくArray.findを使う。

Array.filterは非常に便利なメソッドです。
ある配列にコールバックを渡し、それを通過した値だけを集めた新たな配列を作ります。
名前のとおり、フィルタリングを行ってより短い配列にすることができます。

しかし、コールバックが必ずひとつしか値を返さないとわかっている場合、たとえばフィルタリングのキーに一意のIDを使う、にはArray.filterは勧められません。
この場合Array.filterは長さが1の配列を返しますが、唯一の値を使用したいのですから配列になっている意味は全くありません。

パフォーマンスについても考えてみましょう。
Array.filterは一致する全ての値を返すために、配列全体を走査します。
さらにコールバックに一致する値がたくさんあったとしたら、フィルタリングされたあとの配列も非常に大きなものとなってしまいます。

そのような状況を避けるため、Array.findの使用を検討しましょう。
これはArray.filterと同じようにコールバック関数を受け取り、それを満たす最初の値を返します。
そしてその時点でArray.findはすぐに終了するため、配列の全体を走査しなくて済みます。
またArray.findを使うことで、1件だけを取り出したいという意図を明確にすることができます。

'use strict';

const characters = [
  { id: 1, name: 'ironman' },
  { id: 2, name: 'black_widow' },
  { id: 3, name: 'captain_america' },
  { id: 4, name: 'captain_america' },
];

function getCharacter(name) {
  return character => character.name === name;
}

console.log(characters.filter(getCharacter('captain_america'))); // 3と4の配列
console.log(characters.find(getCharacter('captain_america'))); // 3だけ

Replacing Array.find with Array.some

Array.findではなくArray.someを使う。

私はこのミスを何度もやってしまったことがあります。
その後友人に、MDNのドキュメントにもっといい方法があると教えてもらいました。
この段落は、Array.indexOf/Array.includesの場合とよく似ています。

前段では、Array.findにコールバックを渡し、返り値としてその要素の値を受け取っていました。
これがもし値そのものではなく、値が含まれているか否かを知りたいという場合、Array.findは最適なソリューションでしょうか。
おそらく違います。返り値はbooleanで十分なはずです。

そのような場合は、booleanを返すArray.someの使用をお勧めします。
Array.someを使うと、値そのものについては不要であるという意図が強調されます。

'use strict';

const characters = [
  { id: 1, name: 'ironman', env: 'marvel' },
  { id: 2, name: 'black_widow', env: 'marvel' },
  { id: 3, name: 'wonder_woman', env: 'dc_comics' },
];

function hasCharacterFrom(env) {
  return character => character.env === env;
}

console.log(characters.find(hasCharacterFrom('marvel'))); // { id: 1, name: 'ironman', env: 'marvel' }
console.log(characters.some(hasCharacterFrom('marvel'))); // true

Using Array.reduce instead of chaining Array.filter and Array.map

Array.filter + Array.mapではなくArray.reduceを使う。

たしかにArray.reduceはわかりにくいです。
しかし、Array.filterしてArray.mapするのは何かこう違う感じがありませんか?

ここでは配列を2回読み込んでいます。
最初の配列をフィルタリングして短い配列を作成し、それを使ってさらにもうひとつの配列を作成しています。
ひとつの配列を得るためだけに、ふたつのメソッドを使ってしまいました。
この無駄によるパフォーマンス低下を避けるため、かわりにArray.reduceの使用をお勧めします。
結果が同じなら、より良いコードを選びましょう。
Array.reduceを使うと条件を満たす要素をフィルタリングして第二引数accumulatorに積むことができるようになります。
accumulatorとしてはインクリメント、オブジェクト、文字列、配列などが使えます。

今回の例ではArray.mapを使っていたので、accumulatorには配列を使うことにしましょう。
次の例ではenvの値に応じて値をaccumulatorに積むか、何もしないかを決めています。

'use strict';

const characters = [
  { name: 'ironman', env: 'marvel' },
  { name: 'black_widow', env: 'marvel' },
  { name: 'wonder_woman', env: 'dc_comics' },
];

console.log(
  characters
    .filter(character => character.env === 'marvel')
    .map(character => Object.assign({}, character, { alsoSeenIn: ['Avengers'] }))
);
// [
//   { name: 'ironman', env: 'marvel', alsoSeenIn: ['Avengers'] },
//   { name: 'black_widow', env: 'marvel', alsoSeenIn: ['Avengers'] }
// ]

console.log(
  characters
    .reduce((acc, character) => {
      return character.env === 'marvel'
        ? acc.concat(Object.assign({}, character, { alsoSeenIn: ['Avengers'] }))
        : acc;
    }, [])
) // filter+mapと全く同じになる

That’s it!

この記事は役に立ちましたか?
意見や他のユースケースがあるなら是非コメントを残してください。
役に立ったようであれば、拍手して、この記事をシェアしてください。
読んでくれてありがとう。

注意:IEではArray.findArray.includesがサポートされてないから、使うときにはサポートしてるバージョンに気をつけよう。

コメント欄

「よい記事。さっそく幾つかを試してみよう。」
「コード分割と再利用の観点から、場合によってはあえてfilter+mapを使う方が優れているだろう。」
Array.prototype.includesはES7の機能で、サポートされてないブラウザがあるから注意が必要。」
「パフォーマンスについての主張はおそらく間違っています。確認しましたか? そこ以外は全てグッド。」
「LovelyなIEのためにforでループさせられることを強いられているんだ」
「最後の例はよいとは思えない。かわりにtransducerを使うといいんじゃないか。」
「基本的にはいい記事だけど、みんながコメントしてるようにツッコミどころがいくつもある。次の更新を楽しみにしてるよ。」
「concatしてるせいで毎回新しいオブジェクトを生成してる。そうではなくこうするべき。」

characters .reduce((acc, character) => {
  if (character.env === marvel) {
    acc.push(Object.assign({}, character, { alsoSeenIn: [Avengers] }));
  }
  return acc;
}, [])

感想

『○○のかわりに××が使えるよ』の後に必ず『ただし△△なら』が入ってるので、そのまま機械的に差し替えられるようなものではありません。
役に立つかどうかといえば役立ちますが、7千いいねも得るほどかなあ?という印象。

そもそも配列操作なんて、よっぽど長い配列でもない限りパフォーマンスなんて大して変わらないのだから、他の部分に注力した方がいいんじゃないか。
という長文のマジレスが返ってきていました。

このエントリは参考にとどめて、読みやすさに重きを置いたコーディングをしたほうがいいでしょう。