配列を取り出してループさせるのにfor-of やforEachってあるじゃないですか。
ループ中に配列をいじったらどうなるんだろうと思ったのでやってみました。
結論から言うと、forEachの挙動が直観に反していて面白かったです。
実験環境
- Node.js 12.18.0
- Windows 10
実験1. ループ中に配列を短くして見よう。
下記のようなtest_for,test_forof,test_foreachという関数をつくって実験します。
読めばわかりますが、各ループの中で、配列の先頭をshift()で酉だし、毎回短くしていきます。
test_for([0, 1, 2, 3, 4, 5, 6]);
test_forof([0, 1, 2, 3, 4, 5, 6]);
test_foreach([0, 1, 2, 3, 4, 5, 6]);
function test_for(a) {
let p = 0;
for (let i = 0; i < a.length; i++) {
let v = a[i];
console.log({ v });
console.log({ a });
a.shift();
p++;
}
console.log(p, "====================");
}
function test_forof(a) {
let p = 0;
for (let v of a) {
console.log({ v });
console.log({ a });
a.shift();
p++;
}
console.log(p, "====================");
}
function test_foreach(a) {
let p = 0;
a.forEach((v, i, ary) => {
console.log({ v, i, ary });
console.log({ a });
a.shift();
p++;
});
console.log(p, "====================");
}
で、どうなるかというと、こうなります。
※見やすさのためにちょっと整形してます。
PS C:\Users\etika\Desktop\atcoder> node index.js
{ v: 0 }
{ a: [ 0, 1, 2, 3, 4, 5, 6 ] }
{ v: 2 }
{ a: [ 1, 2, 3, 4, 5, 6 ] }
{ v: 4 }
{ a: [ 2, 3, 4, 5, 6 ] }
{ v: 6 }
{ a: [ 3, 4, 5, 6 ] }
4 ====================
{ v: 0 }
{ a: [ 0, 1, 2, 3, 4, 5, 6 ] }
{ v: 2 }
{ a: [ 1, 2, 3, 4, 5, 6 ] }
{ v: 4 }
{ a: [ 2, 3, 4, 5, 6 ] }
{ v: 6 }
{ a: [ 3, 4, 5, 6 ] }
4 ====================
{ v: 0, i: 0, ary: [ 0, 1, 2, 3, 4, 5, 6 ] }
{ a: [ 0, 1, 2, 3, 4, 5, 6 ] }
{ v: 2, i: 1, ary: [ 1, 2, 3, 4, 5, 6 ] }
{ a: [ 1, 2, 3, 4, 5, 6 ] }
{ v: 4, i: 2, ary: [ 2, 3, 4, 5, 6 ] }
{ a: [ 2, 3, 4, 5, 6 ] }
{ v: 6, i: 3, ary: [ 3, 4, 5, 6 ] }
{ a: [ 3, 4, 5, 6 ] }
4 ====================
はい。見事に同じ出力です。
といわけで、for-ofやforEachを使っていても、内部的には0番目、1番目…と言った感じのIndexで値を参照して取り出しているんでしょうか?
実験2. ループ中に配列を長くして見よう。
今度はループ中に配列を伸ばしてみます。
とはいえ単純に追加すると配列が無限に伸び続けてしまって終わらなくなりそうなので、ちょっと工夫を。具体的には「そのループで取り出した値が偶数だった時のみ、末尾に奇数を追加する」という方針でやってみました。
test_for2([0, 1, 2, 3]);
test_forof2([0, 1, 2, 3]);
test_foreach2([0, 1, 2, 3]);
function test_for2(a) {
let p = 0;
for (let i = 0; i < a.length; i++) {
let v = a[i];
console.log({ v });
console.log({ a });
if (v % 2 == 0) a.push(v + 9);
p++;
}
console.log(p, "====================");
}
function test_forof2(a) {
let p = 0;
for (let v of a) {
console.log({ v });
console.log({ a });
if (v % 2 == 0) a.push(v + 9);
p++;
}
console.log(p, "====================");
}
function test_foreach2(a) {
let p = 0;
a.forEach((v, i, ary) => {
console.log({ v, i, ary });
console.log({ a });
if (v % 2 == 0) a.push(v + 9);
p++;
});
console.log(p, "====================");
}
まあどうせこっちも同じようなことになるだろう……ってよく見てみたら、ちょっと違います。
{ v: 0 }
{ a: [ 0, 1, 2, 3 ] }
{ v: 1 }
{ a: [ 0, 1, 2, 3, 9 ] }
{ v: 2 }
{ a: [ 0, 1, 2, 3, 9 ] }
{ v: 3 }
{ a: [ 0, 1, 2, 3, 9, 11 ] }
{ v: 9 }
{ a: [ 0, 1, 2, 3, 9, 11 ] }
{ v: 11 }
{ a: [ 0, 1, 2, 3, 9, 11 ] }
6 ====================
{ v: 0 }
{ a: [ 0, 1, 2, 3 ] }
{ v: 1 }
{ a: [ 0, 1, 2, 3, 9 ] }
{ v: 2 }
{ a: [ 0, 1, 2, 3, 9 ] }
{ v: 3 }
{ a: [ 0, 1, 2, 3, 9, 11 ] }
{ v: 9 }
{ a: [ 0, 1, 2, 3, 9, 11 ] }
{ v: 11 }
{ a: [ 0, 1, 2, 3, 9, 11 ] }
6 ====================
{ v: 0, i: 0, ary: [ 0, 1, 2, 3 ] }
{ a: [ 0, 1, 2, 3 ] }
{ v: 1, i: 1, ary: [ 0, 1, 2, 3, 9 ] }
{ a: [ 0, 1, 2, 3, 9 ] }
{ v: 2, i: 2, ary: [ 0, 1, 2, 3, 9 ] }
{ a: [ 0, 1, 2, 3, 9 ] }
{ v: 3, i: 3, ary: [ 0, 1, 2, 3, 9, 11 ] }
{ a: [ 0, 1, 2, 3, 9, 11 ] }
4 ====================
ご覧の通り、forEachの場合だけループ数が少ないですね。
(forEachの場合に余計なindexとarrayまで表示させているのでちょっとわかりにくいですが)
普通のfor文とfor-of文の時は、追加した値についてもforループが及んでいますが、forEachの場合は、ループが最初の配列の長さである4周で終了しています。
それ以外の出力は同じ。
実験3. 配列を削りつつ伸ばしてみよう。
今度はforEachのみです。
test_foreach4([0, 1, 2]);
function test_foreach4(a) {
let p = 0;
a.forEach((v, i, ary) => {
console.log({ v, i, ary });
console.log({ a });
a.shift();
a.push(v + 9);
a.push(v + 10);
p++;
});
console.log(p, "====================");
}
{ v: 0, i: 0, ary: [ 0, 1, 2 ] }
{ a: [ 0, 1, 2 ] }
{ v: 2, i: 1, ary: [ 1, 2, 9, 10 ] }
{ a: [ 1, 2, 9, 10 ] }
{ v: 10, i: 2, ary: [ 2, 9, 10, 11, 12 ] }
{ a: [ 2, 9, 10, 11, 12 ] }
3 ====================
はい。
最初の配列の長さである3回分、コールバック関数が呼び出されました。
forEachの仕様
気になったのでforEachの仕様を確認してみると、下記の記述がありました。
forEach() によって処理される配列要素の範囲は、 callback が最初に呼び出される前に設定されます。 forEach() の呼び出しが開始された後に追加された配列要素に対しては、 callback は実行されません。既存の配列要素が変更または削除された場合、 callback に渡される値は forEach() がそれらを参照した時点での値になります。削除された配列要素を参照することはありません。既に参照された配列要素がイテレーションの間 (e.g. shift()を使用して) に削除された場合、後の要素は飛ばされます。
なるほどなるほど……と思ったのですが、よく見ると下記の記述は変です。
forEach() の呼び出しが開始された後に追加された配列要素に対しては、 callback は実行されません。
さきほど実験3で見た通り、呼び出しが開始された後に追加れた「10」という要素に対して、callback関数が実行されてしまいました。上記の仕様を素直に読むなら、callback関数は2回しか呼ばれないと思うのですが。
これは、どういうことなの……?
まあMozillaのブラウザとNode.jsとは仕様差異があるという可能性も……。
🤔🤔🤔
……ソンナコトハナカッタ。