🀄

【JavaScript】findメソッドの危険性についての考察

2020/11/30に公開約3,100字9件のコメント

はじめに

最近JavaScriptのfindメソッドを使った時に、急に怖くなったので記事にしてみたいと思いました。

findメソッドとは❓

ES6(ES2015)で追加されたメソッドでMDNでは以下のように説明されています。

提供されたテスト関数を満たす配列内の 最初の要素 の 値 を返します。

簡単に言うと、配列の中から上から順番に検索して、最初に条件に合致するものを返すということになります。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/find

MDNにはこのような例文が記載されています。

const array1 = [5, 12, 8, 130, 44];
const found = array1.find(element => element > 10);
console.log(found);
// expected output: 12

この例だとarray1を順番に検索して行って、10より大きい値があったらそれを返すので、12が出力されます。
まあこの辺はJavaScript書く方なら皆さん知っている内容かと思います。

何が怖くなったのか❓

実際に使用するときの例としては、以下のような配列からidが合致するものを検索してnameを取り出したい場合があると思います。

次のコードはid001のデータをdatasetから取得してnameを取り出しています。

const dataset = [
  { id: "001", name: "aaa" },
  { id: "002", name: "bbb" },
  { id: "003", name: "ccc" },
  //・・・
];
const id = "001"
const resFind = dataset.find((data) => data.id === id);
console.log(resFind.name);
// expected output: aaa

うん。普通にid001のデータからnameが取り出せてますね。
しかし、以下のようにdatasetに実はもう一つid=001があった場合には、

const dataset = [
  { id: "001", name: "aaa" },
  { id: "002", name: "bbb" },
  { id: "003", name: "ccc" },
  //・・・
  { id: "001", name: "zzz" }, // さらにこういうデータがあった場合
];

次のように何事もなくaaaが取得できます。

const id = "001"
const resFind = dataset.find((data) => data.id === id);
console.log(resFind.name);
// expected output: aaa

この何事もなく取得できてしまうのって怖くないですか??
私はこの部分にfindメソッドを使う怖さを感じました。

よくあるユースケースとして、データベースからユーザー情報を持ってきて、IDで名前や年齢などの情報を取得することが多いと思います。基本的にはIDは一意の値にしていると思いますが、実は一意になってなかった場合に、この辺りの情報を間違えて取得することがあるし、それに気づくことができない可能性があります。

なので、個人的にはfindは使うのが怖いので使わないようにしようかと思っています。

なのでfilter使う

じゃあどうしようかという問題が出てくるけど、filterで代用が可能です。

MDNでは以下のように説明されています。

与えられた関数によって実装されたテストに合格したすべての配列からなる新しい配列を生成します。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/filter

findメソッドと違うのは条件に合致しているものを全て取得して配列にしてくれます。
以下の例だとid001のものが二つあるので、二つを取得して配列に格納してくれます。

const dataset = [
  { id: "001", name: "aaa" },
  { id: "002", name: "bbb" },
  { id: "003", name: "ccc" },
  //・・・
  { id: "001", name: "zzz" },
];

const id = "001"
const resFilter = dataset.filter((data) => data.id === id);
console.log(resFind);
// expected output: [{id: "001", name: "aaa},{id: "001", name: "zzz}]

ここでfindメソッドとは違って条件に合致したものを全て配列に格納してくれるので、length1であれば一意のデータが取得できているということになります。
ですので、lengthが1ではなかったらログに出したり、アラート出したりすれば気づけると思います。

if (resFilter.length === 1) {
  console.log(resFilter[0].name);
}
else{
  console.log("multiple same id.");
}

こうすれば安心ですよね!!

productionモードでは無駄な処理をさせないことも重要なので、以下のようにdevelopモードだけfilterを使ったチェックを行うのが良さげです。その場合はそのままfindで取得しても問題なさそうです。

if ("development" === process.env.NODE_ENV) {
  if (1 !== dataset.filter((data) => data.id === id).length) {
    console.log("multiple same id.")
    process.exit(1)
  }
}

まとめ

今回はfindメソッド危険性についての考察と題して、実際にコード書くときに使って見たら怖いなと思ったので記事にしてみました。初学者レベルなので他にもっといいやりかたなどがあれば教えて欲しいと思ったというのも記事にした理由になります。

間違っていたり、もっとこうしたほうがいいよとかありましたら、是非コメントいただければと思います!

最後までお読みいただきありがとうございました。

GitHubで編集を提案

Discussion

一意のデータを探す場合

find は最初の一つが見つかったらきちんと処理を終了します
filter は見つかった後も無駄に処理を続けます

ユーザの CPU を無駄に使うと神奈川県警察に捕まります
……というのは冗談ですが無駄に処理を走らせていることは事実です

テスト用のコードと本番用のコードは分けた方が良いと思います

rithmetyさん、コメントありがとうございます!!
一点質問ですが、

テスト用のコードと本番用のコードは分けた方が良いと思います

の部分についてですが、コードを分けるというのはdevelopモードとproductionモードでコードを切り替えるという意味でしょうか??もしくは単体テストなどを行うときと本番でコードを分けるという意味でしょうか??

ちょっと理解できていないので、詳しく教えていただけると幸いです。

どちらかと言えば後者でしょうかね

「一意のデータが取得できるようになっているかどうかは単体テストで確認して
本番用のコードでは無駄な処理はしないようにした方が良い」という意味です

ただし develop では assert が走るようになっているようなコードでも良いと思います

if ("development" === process.env.NODE_ENV) {
  if (1 !== dataset.filter((data) => data.id === id).length) {
    console.log("multiple same id.")
    process.exit(1)
  }
}
return dataset.find((data) => data.id === id)

なるほど!
developモードだけチェックするのが良さそうですね!!
本番用では無駄な処理を書かないほうがいいですもんね。

コメントいただきありがとうございました!!

findはそういう機能なので、ユニークなデータかどうかが不明だからfindを使わない、findに危険性があるというのは無茶な主張かと思いました。そんな理由でfindが危険なのだったら、indexOfでもなんでも先頭からサーチしてみつける処理はみな危険ということになります。
データが生成されるときに、ユニークであるべきなのに、idが重複しているものがくるとしたら、データ生成タイミングではじきましょう。
あと、連想配列という単語の使い方はあってないと思います。

standard softwareさん、コメントありがとうございます!!

findに危険性があるというのは無茶な主張かと思いました。

これについては無茶な主張であるのは重々承知しています。その上で皆さんどうしてるのかなと聞いてみたかったという側面もあり、記事にしてみました。

データが生成されるときに、ユニークであるべきなのに、idが重複しているものがくるとしたら、データ生成タイミングではじきましょう。

データ生成タイミングではじくべきというのも重々承知しているのですが、APIから取得したデータなどでも信用していいのかという疑問がありました。

あと、連想配列という単語の使い方はあってないと思います。

連想配列(オブジェクト)の配列ということでよろしいでしょうか?
「連想配列=オブジェクト」であってますよね?

連想配列なのですが、記事中の連想配列の使い方が普通の配列のことを連想配列と書いているので、ちょっと変かと思いました。

また、よくひっかかるネタなのですが、JSでは連想配列=オブジェクトとみなす記事が多く書かれていますが、そうするとけっこうやばいバグにあたるんです。連想配列はMapというものがあるので、そっちがいいです。なので、「連想配列=オブジェクト」と書いている記事は信じちゃだめって感じです。

APIから取得したデータなどでも信用していいのかという疑問がありました。

これはわかります。
これは本当に怪しい、と思うときは、受信した直後にわざと全件重複チェックをしてしまうのです。ここでfilterを使っても当然OKで、受信直後のデータに問題があれば「throw new Error」とかして、プログラムをそれ以上動作させないようにします。
ユニーク値を送ってこないのはAPIが悪い、という風に問題の切り分けが素早く行えます。

取得したデータがユニークな値を持つデータなのか、ユニークな値を持たないデータなのかが、わからないままプログラムを動かして、あとあと、更新ボタンを押したら落ちる、とかじゃなくて、受信したデータはあるキーに対してユニークですよ。というのを保証しておけば、プログラム全体でfind使っても安心してつかえるようになります。

で、API側の実装で絶対にユニーク値は送らないですよ、ということが固定してきたら、そのエラー吐くユニークチェックコードを消しておけばパフォーマンスも問題にならないです。

そこまでやるのはめんどくさいと思うかもしれませんが、受信データが特定キーでユニークかどうかわからないままプログラミングするほうが、あとでかなりめんどうなことになりますし、特定キーでユニークかどうか安心してプログラム組めれば、findを使うのは当たり前になります。filterで毎回lengthが1件かどうかチェックするほうが大変というか冗長かと思います。

なるほど、勉強になりました!
「連想配列=オブジェクト」ではないんですね・・・まんまと騙されました。
普通に以下のような連想配列以下のような配列に変更させていただきます。

他の方にもコメントをいただきましたが、filterを使ってチェック(developモードだけ)して、実際の処理はfindで行うのが良さそうですね。

standard softwareさん、ありがとうございました!!

以前のコメントにいいねもらった機会に改めて見直しましたが、私の言葉足らずで、日本語書くよりコード書いた方がわかりよいと思いましたので追記します。

const getData = id => {
  if ("development" === process.env.NODE_ENV) {
    const len = dataset.filter(d => d.id === id).length;
    if (len === 0) {
      throw new Error('dataset no id');
    } else if (len !== 1) {
      throw new Error('dataset multiple same id.');
    }
  }

  return dataset.find(d => d.id === id);
}

こんな風にしておくと、データに問題があるときは例外がでてプログラムが終了するという「事前条件」になります。「契約プログラミング」とか「防御的プログラミング」という手法になります。

console.log process.exit でもいいのですがエラーを投げるようにしておいても、知っている人にはわかりやすいです。

このプログラムのこの関数(getData)では、IDに不一致なものはないし、IDは重複してない、という「事前条件」が書いてあると、developmentモードではない通常の動作としては、getDataが、戻り値として undefined は返さないんだな、ということが明確になり、プログラミングが楽になります。

細かい所を書いてしまいましたが、一見、こういう細かいどうでもいいと思われることが、実際にはプログラマのスキルを向上させるだろうなと思いましたので、くどくど書いてしまいました。

あと、連想配列についても記事書いてました。

オブジェクトを連想配列として使うと痛い目にあいますよ
https://zenn.dev/standard_soft/articles/7458d1f49fd2ef

ログインするとコメントできます