2022年7月10日日曜日

JavaScriptで配列をループ中に要素を追加したらどうなるのっと

突然ですが、以下のJavaScriptコードを実行したら何が出力されるでしょうか。

const values = [1, 2, 3];
for (const value of values) {
  console.log(value);
}

はい、正解です。1, 2, 3の3つが出力されます。

では次のコードは?

const values = [1, 2, 3];
for (const value of values) {
  console.log(value);

  // 最初のループで配列に要素を追加
  if (value === 1) {
    values.push(4);
  }
}

というわけで、本日はECMAScriptの規格の話です。

要するに何が言いたいのか

つまり「for of構文のループ内で配列に要素を追加した場合、追加した要素の分までループされるのか?」ということです。

上記のコードの可能性としてはおそらく

  • 1, 2, 3が出力される
  • 1, 2, 3, 4が出力される

くらいしかないわけで、普通に考えれば後者だろうと予想されるのですが、果たして本当でしょうか。

つまり、forの終わりは最初にforに入った時点で決まるのか、ループごとに毎回評価されるのか。そしてそれは規格で定義されているのかということです。

前者(forに入った時点で終わりも決まる)のメリットとしては、例えばループ回数が事前に決まることでJITコンパイラーによるループアンローリングなどの最適化の余地がある等です。

処理系の挙動

手元のChromeとFirefoxで試してみたところ、やはりというべきかどちらも後者の挙動、つまり1, 2, 3, 4が出力されました。

つまりループごとにforの終わりが毎回評価されているということ。

で、問題はこれがECMAScriptの規格で定義されているのか、定義されておらず各処理系が勝手に自分たちの都合のいい実装にしている(そして挙動がChromeとFirefoxでたまたま同じだった)のか。

仕様はどうなってる?

ECMA-262を眺めるのはだるいので、まずはMDNを覗いてみます。

この辺を見る限りでは、少なくともMDNには明記はされていないっぽいです。

仕様を見てみた

そもそもの話、これは突き詰めると配列のイテレーターの挙動をECMAScriptがどのように規定しているかという話になってくるわけです。

というわけで重い腰を上げてECMA-262のArray Iterator Objectsを見てみました。ここにイテレーターの詳細な挙動が規定されています。

23.1.5.1 CreateArrayIterator ( array, kind )を流し読みすると、len(配列の長さ)は各ループ内で毎回取得していることがわかります(1-b-i,ii)。

つまり、ループの途中で配列の長さが変わったら次回のループで新しい長さに合わせて終了条件が変更されます。そしてこれは先のChrome/Firefoxでの実験結果と合致します。

そしてECMAScriptは後方互換性を重視しているので、おそらく今後この仕様が変更されることはないんじゃないかなと思います(希望的観測)。

まとめ

for ofループの途中で反復対象の配列の要素数が変更された場合でも、ループの終了条件は動的に変わります。これは直感的にも納得いく挙動で、ECMA-262でも定義されています(処理系依存ではありません)。

結論としては特に目新しいことはないですが、技術ブログらしくあまり行わないような操作の仕様を確認してみました。

0 件のコメント:

コメントを投稿