1
1

(JavaScript) mapとforEachをちゃんと知って使い分けたい

Last updated at Posted at 2024-06-10

最近はなんとなくループ処理をするときにmap(),filter(),some()などを優先して使っている。
「モダンなJSぽいから」、「自分には見慣れているから」の理由だけでこれらのメソッドを使っているが、実際にどのような処理が行われているか何もわかっていない。

現状は下の記事を読んで、なんとなくforEachは避けるようになっていた。
コードの綺麗さで見ると間違いないが、性能についてはまだ知識がないまま。

と言うことで、まずはmapforEachについて詳しくなりたいと思った。

どんな特徴を持っている?

map()forEach()両方Arrayに関するメソッドで、ES5から使われている。

forEach()が配列の要素ごとに一回ずつ与えられた関数(コールバック)を実行する反面、

map()は配列内のすべての要素に対して各々与えられた関数を呼び出した結果を集めて、新しい配列を返す。

また、その関数は

  1. currentValue (配列要素の値)

  2. index (現在のindex)

  3. array (現在の配列)

この3つの引数を持って呼び出される。

例えば、配列の中身を10倍したものを出すには、
以下のようなコードになる。

forEach.js
// forEach()の例

const array = [1, 2, 3];
const tenTimesArray = [];

arr.forEach(num => {
  tenTimesArray.push(num * 10);
});

console.log(tenTimesArray); // [10, 20, 30]
map.js
// map()の例

const arr = [1, 2, 3];
const tenTimesArray = arr.map(num => num * 10);

console.log(tenTimesArray); // [10, 20, 30]

for文との違い

  1. コールバックを実行するが、オーバヘッドとして動作する

    • そもそもオーバヘッドとは?

      コンピュータで何らかの処理を行う際に、その処理を行うために必要となる付加的、間接的な処理や手続きのことや、そのために機器やシステムへかかる負荷、余分に費やされる処理時間などのことをオーバーヘッドということが多い。

      要するに、付加的な処理が入ること。

  2. Javascript関数がオブジェクトのgettersや、配列の要素が連続でない希少配列、通った引数が配列かなどを判断する多くの判断過程がある。それらはオーバヘッドを増やす。

このような理由でmap()forEach()の方が遅くなる。

forEachとmapの違い

それでは、map()とforEach()を比較したらどんな差があるのか?

ECMAScriptにそれぞれのロジックが詳しく書いてあった。

array.prototype.map

  1. Let O be ? ToObject(this value).
  2. Let len be ? LengthOfArrayLike(O).
  3. If IsCallable(callbackfn) is false, throw a TypeError exception.
  4. Let A be ? ArraySpeciesCreate(O, len).
  5. Let k be 0.
  6. Repeat, while k < len,
    a. Let Pk be ! ToString(𝔽(k)).
    b. Let kPresent be ? HasProperty(O, Pk).
    c. If kPresent is true, then
    i. Let kValue be ? Get(O, Pk).
    ii. Let mappedValue be ? Call(callbackfn, thisArg, « kValue, 𝔽(k), O »).
    iii. Perform ? CreateDataPropertyOrThrow(A, Pk, mappedValue).
    d. Set k to k + 1.
  7. Return A.

array.prototype.forEach

  1. Let O be ? ToObject(this value).
  2. Let len be ? LengthOfArrayLike(O).
  3. If IsCallable(callbackfn) is false, throw a TypeError exception.
  4. Let k be 0.
  5. Repeat, while k < len,
    a. Let Pk be ! ToString(𝔽(k)).
    b. Let kPresent be ? HasProperty(O, Pk).
    c. If kPresent is true, then
    i. Let kValue be ? Get(O, Pk).
    ii. Perform ? Call(callbackfn, thisArg, « kValue, 𝔽(k), O »).
    d. Set k to k + 1.
  6. Return undefined.

大きく2つの違いが存在することがわかる。

  1. mapにはAという配列があること
  2. return値の有無

mapを見てみると、Aという配列の変数は一つだけ存在し、コードではindexのみ変更されている。

ここで一般的にindexを表すiではないkを使うことに理由はあるのか?

現在の配列をオブジェクトで生成し、indexではなく、keyの値として使用するためだ。
k = keyか。

for文ではなく、whileを使って実装しているのも関係があると考えられる。
Javascriptの配列の中身が順次的に入っていないときに、配列の長さ通りに全部回ると余計な演算が入るためだ。

[undefined, undefined, undefined, undefined, undefined, 1]

上記のような場合に、配列の最後までiterationを全部回す必要がなくなるか。
map()を使うとき、上記のような配列であれば不要な処理を妨げてくれることがわかった。

ともかく、forEachとは違って、mapのコードにあるAという配列はメモリのAllocationを起こすため演算処理が必要になる。

すなわち、コードを実行するだけではなく、新しい変数に入れる作業が入っているmapの方がforEachより遅くなるしかない。

ブラウザでの検証

実際に長さ100,000,000の配列で実行した時の結果を見るため、ブラウザで検証してみた。
平均約4.7秒と8.6秒で、処理時間の差が出ている。

環境や処理によって結果は変わってくると思うが、とりあえず差は存在する。

スクリーンショット 2024-06-09 19.30.14.png

結論

forEachとmapは目的も違って、forEachでmapの役割を果たすためにはまた変数の生成が必要であることを考えると、「forEachの方が早いからmapはやめよう」にはならないと考えた。ブラウザでの検証も極端的な例でもある。

状況に合っているメソッドを使えばいいだろう。
他のメソッドとの違いもわかって使えるようになりたい。

参考

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1