JavaScriptの for...of vs forEach
目次
TL;DR
forEach
は案外悪くないです。for...of
派からforEach
派になりました。
ちなみにfor...of
を書き換える方法についてはこの記事では記述していません。
導入
eslint-config-airbnbをそのまま使っているとfor...of文に対しno-restricted-syntaxのエラーが出ます。
Airbnbのスタイルガイドには次のように書かれています。
11.1 Don’t use iterators. Prefer JavaScript’s higher-order functions instead of loops like for-in or for-of. eslint: no-iterator no-restricted-syntax Why? This enforces our immutable rule. Dealing with pure functions that return values is easier to reason about than side effects. Use
map()
/every()
/filter()
/find()
/findIndex()
/reduce()
/some()
/ ... to iterate over arrays, andObject.keys()
/Object.values()
/Object.entries()
to produce arrays so you can iterate over objects.
和訳
イテレータを使わない。for-inやfor-ofのようなループよりもJavaScriptの高階関数を優先する。eslint: no-iterator no-restricted-syntax 何故か?これは不変ルールを遵守する。値を返す純粋関数を扱う方が副作用より推論しやすい。 配列に対して繰り返すには
map()
/every()
/filter()
/find()
/findIndex()
/reduce()
/some()
/ ... を使い、オブジェクトに対して繰り返すにはObject.keys()
/Object.values()
/Object.entries()
を使う。
たしかにその通りです。私も関数型のプログラミングを好みますし、可能ならmap()
/ every()
/ filter()
/ find()
/ findIndex()
/ reduce()
/ some()
/ ...を使います。
しかし、パフォーマンスなどの理由で副作用を起こしたいこともあります。副作用を起こすのであれば、関数型寄りの記述である高階関数ではなく手続き型のfor..of文で書いた方が自然なのではないか?と思えてしまいます。
気になって検索してみたところ、for...ofを禁止する理由について尋ねるairbnb/javascriptのIssueにたどり着きます。150以上のコメントのつく議論を読んでいて、やむを得ない場合forEach
による副作用もmake senseだと思え、興味深い議論だと思ったのでまとめました。
副作用を起こす場合for...ofよりforEachを使う論拠
主な論拠は下記の2つです。
map
,every
,filter
,... を使わずforEach
を使っていることで副作用の不吉な臭いを明示できるfor...of
に対するpollyfilが要求するregenerator-runtimeは重すぎる
forEachにより不吉な臭いを明示できる
この論拠はこのコメントに書かれています。
that's exactly the point. side effecty iterations SHOULD be a code smell.
和訳
まさにそれがキモです。副作用のある繰り返しは不吉な臭いがするべきです。
これはこのコメントへの返信です。
However, it is my opinion that side effects in array iteration is a bad practice and you should be explicit when you are looping over some iterator to produce side effects (e.g.
appendChild
,Array.push
, whatever). In this case,for of
has much less of a 'code smell' thanArray.forEach
.
和訳
しかし、配列の繰り返しにおける副作用はバッドプラクティスであり、イテレータに対し副作用(例:
appendChild
,Array.push
, などなど)を及ぼす繰り返しをするなら明示的であるべきであるというのが私の意見です。このケースでは、for of
はArray.forEach
よりも「不吉な臭い」がとても少ないです。
このやりとりは私の疑問をまさに捉えており、forEach
を使うべき理由として目から鱗でした。
forEach
は副作用を起こす無名関数を受け取る高階関数なため、関数型のパラダイムではない中途半端で受け入れがたい存在として私はずっと避けてきました。
私は副作用を起こすなら手続き型のfor...of文を使うのが綺麗だとまさに信じていました。
そして、なんとこれはforEach
を使うべき理由と両立するというのです。
副作用を及ぼすような関数型ではない書き方をするのであれば、忌々しいforEach
を使うことでコードに「ここには副作用があり好ましくない」という不吉な臭いを残すべきという考え方です。
for...of
に対するpollyfilが要求するregenerator-runtimeは重すぎる
こちらの論拠も同じくこのコメントで言及されています。
for..of
will not be allowed by the airbnb preset because it requires Symbols to exist, and Symbols can not truly be polyfilled - and regenerator-runtime is too heavyweight.
和訳
for...of
はSymbolが存在することが必要でそのSymbolは完全にはpolyfillできず、そしてregenerator-runtimeが重すぎるためfor...of
はairbnbプリセットで許可されることはありません。
この論拠は単体ではあまり私には響きませんでした。
for...of
のpollyfillは実際に動き、パフォーマンスより可読性を重視したいので大した問題ではないと考えます。
しかし、前述の通り可読性の点でもforEach
が優れているという考え方もでき、そうなるとこの論拠も含めてfor...of
を使う理由がないと言えると思います。
さらに可読性の点については「ループ内で副作用を起こす稀なケースよりもpollyfillの問題が重要だ」と同コメントに書かれています。
とはいえfor..ofを使いたいケースもあるのでは?
つかれたので暇な時に別記事にまとめます。 むしろループの書き換えの方が需要がありそうですね。
まとめ
- 関数型が好きならやむをえず副作用を起こす際は
forEach
で不吉な匂いをかもそう for...of
のpollyfillは重い