l08084のブログ

プログラミングについて書きます

forEachループの途中で要素を削除した場合のメモ

はじめに

forEachでループを回している配列の要素をループの途中で削除した時に、(あくまで自分からみるとの話です)よくわからない挙動をしたので備忘録目的でまとめました。

developer.mozilla.org

ループの途中で要素を削除した場合についての、👆MDNのサイトで記載されている部分の引用です。

forEach によって処理される配列要素の範囲は、callback が最初に呼び出される前に設定されます。forEach の呼び出しが開始された後に追加された配列要素に対しては、callback は実行されません。既存の配列要素が変更または削除された場合、callback に渡される値は forEach がそれらを参照した時点での値になります。削除された配列要素を参照することはありません。

👆上記引用文ですが、渡された時点の配列要素の範囲でループを回すので、ループの途中で配列を追加したり、削除してもループ回数とループで渡す要素には影響ないよ、という意味だと自分は解釈しています。

検証プログラム

let eventA = {
    title: '遠征',
    start: '4/10'
}
let eventB = {
    title: '練習試合',
    start: '4/10'
}
let eventC = {
    title: '留学',
    start: '4/11'
}
let eventD = {
    title: '旅行',
    start: '4/10'
}
let eventList = [eventA, eventB, eventC, eventD];
let convertedList = [];

eventList.forEach((event, index, array) => {
    // startが同じeventの配列を作成する
    let filteredArray = eventList.filter((element) => {
        return (element.start === event.start);
    });

    // startが同じeventの件数をtitleに代入する
    event.title = filteredArray.length;

    // 件数をカウントしたeventをeventListから削除する
    eventList = eventList.filter((element) => {
        return (event.start !== element.start);
    });
    convertedList.push(event);
});

console.log(convertedList);
/*  実行結果
    Array(4) [Object, Object, Object, Object]
    length:4
    __proto__:Array(0) [, …]
    0:Object {title: 3, start: "4/10"}
    1:Object {title: 0, start: "4/10"}
    2:Object {title: 1, start: "4/11"}
    3:Object {title: 0, start: "4/10"}
*/

上記のソースコードですが、開始(start)が同じ日付のeventオブジェクトが複数件あった場合、一つにまとめて、まとめたeventオブジェクトの件名(title)に開始が同じ日付のeventの件数を設定するということをしています。 上記プログラムを動かす前は、開始(start)が4/10のイベントが3件、開始(start)が4/11のイベントが1件あるので、下記の通り表示される認識でしたが、

    Array(2) [Object, Object]
    length:4
    __proto__:Array(0) [, …]
    0:Object {title: 3, start: "4/10"}
    2:Object {title: 1, start: "4/11"}

実際には、次の通り表示されました。

    Array(4) [Object, Object, Object, Object]
    length:4
    __proto__:Array(0) [, …]
    0:Object {title: 3, start: "4/10"}
    1:Object {title: 0, start: "4/10"}
    2:Object {title: 1, start: "4/11"}
    3:Object {title: 0, start: "4/10"}

一度、開始(start)が同じだと判断してまとめたeventについては、次の部分で削除しているので、

// 件数をカウントしたeventをeventListから削除する
eventList = eventList.filter((element) => {
    return (event.start !== element.start);
});

titleが0のeventは出力されない認識でしたが、下記2つの要因が原因で上記の通り出力されたようです。

  • MDNのサイトに記載されている通り、ループの途中で要素を削除しても、ループの範囲とループで渡される要素には、影響がない

  • forEachで回しているeventList配列と、内部から参照しているeventList配列が別々の参照になっているため、forEachで回しているeventList配列では、要素が削除されていないが、内部から参照しているeventList配列では、要素が削除されいる扱いになっている(4回目のループでは、forEachで回しているeventList配列の要素数は4件だが、内部から参照しているeventList配列の要素数は2件になっていると思われる)

スコープについての理解が曖昧なせいか(?)原理がよくわからない…そもそもループの途中で要素を削除するようなプログラムを書くこと自体が間違っているのか?それともforEach以外を使うべきだったのか…という感じです。