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, and Object.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' than Array.forEach.

和訳

しかし、配列の繰り返しにおける副作用はバッドプラクティスであり、イテレータに対し副作用(例: appendChild, Array.push, などなど)を及ぼす繰り返しをするなら明示的であるべきであるというのが私の意見です。このケースでは、for ofArray.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...ofairbnbプリセットで許可されることはありません。

この論拠は単体ではあまり私には響きませんでした。 for...ofのpollyfillは実際に動き、パフォーマンスより可読性を重視したいので大した問題ではないと考えます。 しかし、前述の通り可読性の点でもforEachが優れているという考え方もでき、そうなるとこの論拠も含めてfor...ofを使う理由がないと言えると思います。

さらに可読性の点については「ループ内で副作用を起こす稀なケースよりもpollyfillの問題が重要だ」と同コメントに書かれています。

とはいえfor..ofを使いたいケースもあるのでは?

つかれたので暇な時に別記事にまとめます。 むしろループの書き換えの方が需要がありそうですね。

まとめ

  • 関数型が好きならやむをえず副作用を起こす際はforEachで不吉な匂いをかもそう
  • for...ofのpollyfillは重い