♨️

配列内のオブジェクトに変更を加えたのに反映されない

2024/07/26に公開10

記事の概要

先日「ループ処理で配列内のオブジェクトに対して変更を加える」ような処理を書いていたのですが、配列内オブジェクトに変更を加えたはずなのになぜかその変更が反映されないという問題に遭遇し、原因を突き止めるのに時間がかかったので、調べたことを忘れないようにこの記事を書きました。
参照渡しについての理解が浅かったことが原因でして恐らく基本の内容かと思われますが、同問題で困っている方にはもしかしたら参考になるかもしれないです。(配列の参照渡し周りについてざっくり書いています)

問題と解決方法の概要(サンプルコードあり)

問題コード(処理1)

今回遭遇した問題を簡単にしたもので、リザードがレベル36以上の場合リザードンに進化させる処理を書いているのですが、実際に動かしてみるとリザードのままになります。

問題コード(処理1)
const party = [
  { name: "ヒトカゲ", level: 8 },
  { name: "リザード", level: 36 },
  { name: "リザードン", level: 100 },
];

// 処理1
for (let pokémon of party) {
  if (pokémon.name === "リザード" && pokémon.level >= 36) {
    pokémon = { name: "リザードン", level: pokémon.level };
  }
}
console.log(party);
// [{ name: 'ヒトカゲ', level: 8 },{ name: 'リザード', level: 36 },{ name: 'リザードン', level: 100 }]

解決コード(処理2)

問題を解決したコードになります。
以下の書き方だと、無事リザードがリザードンに進化しました。

解決コード(処理2)
const party = [
  { name: "ヒトカゲ", level: 8 },
  { name: "リザード", level: 36 },
  { name: "リザードン", level: 100 },
];

// 処理2
for (const i in party) {
  if (party[i].name === "リザード" && party[i].level >= 36) {
    party[i] = { name: "リザードン", level: party[i].level };
  }
}
console.log(party);
// [{ name: 'ヒトカゲ', level: 8 },{ name: 'リザードン', level: 36 },{ name: 'リザードン', level: 100 }]

解決コード(処理3・処理4)

少し調べたところ、処理2の書き方はあまりよくないらしく、もう少し良さそうな解決コードを2つ書いてみました。(参考:JavaScript の for ループ / for in ループ/ for of ループ ってなにが違うの?

処理3は新しいオブジェクトを詰めなおしたい場合に使えそうかなと思います。
処理4は既存のオブジェクトの一部の値を変更したい場合に使えそうかなと思います。

解決コード(処理3)
const party = [
  { name: "ヒトカゲ", level: 8 },
  { name: "リザード", level: 36 },
  { name: "リザードン", level: 100 },
];

// 処理3
for (let i = 0; i < party.length; i++) {
  if (party[i].name === "リザード" && party[i].level >= 36) {
    party[i] = { name: "リザードン", level: party[i].level };
  }
}
console.log(party);
// [{ name: 'ヒトカゲ', level: 8 },{ name: 'リザードン', level: 36 },{ name: 'リザードン', level: 100 }]
解決コード(処理4)
const party = [
  { name: "ヒトカゲ", level: 8 },
  { name: "リザード", level: 36 },
  { name: "リザードン", level: 100 },
];

// 処理4
for (const pokémon of party) {
  if (pokémon.name === "リザード" && pokémon.level >= 36) {
    pokémon.name = "リザードン";
  }
}
console.log(party);

 

問題と解決方法の詳細(図解あり)

問題コード(処理1)

処理1はpokémonという変数に、元々の配列内に保存されている元々のオブジェクトへの参照を渡して処理を実行する動きになっているため、for文内の2回目のループ時の動きとしてはpokémon新しいオブジェクトへの参照を渡しているだけなので、元々の配列には何の変化も起きないという結果になったようです。

解決コード(処理2・処理3)

処理2と処理3は元々の配列内に保存されている元々のオブジェクトへの参照を新しいオブジェクトへの参照に変更する処理になっているので、元々の配列に変化が起きるという結果になったようです。

解決コード(処理4)

処理4はpokémonという変数に、元々の配列内に保存されている元々のオブジェクトへの参照を渡して処理を実行する動きになっていますが、for文内の2回目のループ時の動きとしては参照先の元々のオブジェクトのプロパティを変更する動きになっているので、元々の配列に変化が起きるという結果になったようです。

まとめ

上記の内容を書く際に参考にさせていただいた記事が以下になります。
何かおかしな点に気づいた方や、「もっといい書き方あるよ!」という方はコメント等いただけると嬉しいです。
ここまでお読みいただきありがとうございました。

Discussion

nap5nap5

何かおかしな点に気づいた方や、「もっといい書き方あるよ!」という方はコメント等いただけると嬉しいです。

こういったのはデータ構造にIDを持たせて、mapなどで処理するのがいいと思いました

発展させて、signiadiomaを組み合わせて、Reactでデモしてみました

リザードンからリザードへ退化
リザードからリザードンへ進化

の2つをレベル36をエッジとして、チャレンジしてみました

そうえもんそうえもん

コメントありがとうございます!
発展形のデモもつけていただきありがとうございます!(せっかくデモしていただいたところ申し訳ないのですが、理解しきれなかったので勉強させてもらいます…!)

おっしゃるとおりmapを使う方が良いですね!
書いていただいたコードから読み取れず大変申し訳ないのですが、データ構造にIDを持たせるのは同名かつ同レベルのポケモンを識別できるようにするためでしょうか?

// map版
const evolvedParty = party.map((pokémon) => {
  if (pokémon.name === "リザード" && pokémon.level >= 36) {
    return { name: "リザードン", level: pokémon.level };
  }
  return pokémon;
});

console.log(party);
// [{ name: 'ヒトカゲ', level: 8 },{ name: 'リザード', level: 36 },{ name: 'リザードン', level: 100 }]

console.log(evolvedParty);
// [{ name: 'ヒトカゲ', level: 8 },{ name: 'リザードン', level: 36 },{ name: 'リザードン', level: 100 }]

nap5nap5

データ構造にIDを持たせるのは同名かつ同レベルのポケモンを識別できるように

進化させる条件predicateとは別の観点ではあるのですが、大体合っていそうです

Web化ないしアプリ化するにあたり、UIを用意するとは思うのですが、ポケモンはレベルアップすると進化するので、どのポケモンをレベルアップさせるかを特定するためにも、IDは必要です

このIDを持ってクリックイベントでレベルアップ、レベルダウンさせています

また、退化は進化との対称性の観点から、実際にあるかは別として組み込んだだけとなります

情報設計の観点からもIDで体系付けて管理するというのもあります

たとえば、グルーピング処理等で、同名同レベルのカウント数を集約するといったことがある際はIDで体系付けてあることで、IDの数を持ってポケモンの集約情報の取得を達成できます

そうえもんそうえもん

進化させる条件predicateとは別の観点ではあるのですが、大体合っていそうです
Web化ないしアプリ化するにあたり、UIを用意するとは思うのですが、ポケモンはレベルアップすると進化するので、どのポケモンをレベルアップさせるかを特定するためにも、IDは必要です

理解できました、ありがとうございます!
同名・同レベルのポケモンがパーティーに2体いた場合、任意で選んだ片方のポケモンのみ進化させるケースなども想定するとIDは必要ですね!

情報設計の観点からもIDで体系付けて管理するというのもあります
たとえば、グルーピング処理等で、同名同レベルのカウント数を集約するといったことがある際はIDで体系付けてあることで、IDの数を持ってポケモンの集約情報の取得を達成できます

グルーピングの際にIDレベルでフィルタリングするという意味だと読み取ったのですが、その方法が名前レベルでフィルタリングする方法よりも優れているのはどういった点になりますでしょうか?(フィルタリングの速度が速くなるなどでしょうか?)

nap5nap5

グルーピングの際にIDとレベルでフィルタリングするという意味だと読み取った

フィルタリングの文脈は登場させているつもりはないです

名前、レベル単位でグルーピングする場合は、IDで体系づけていることで、その件数でもって、確かめることができますよねといったぐらいの程度のものです。

import { describe, test, expect } from "vitest";
import { tidy, groupBy, summarize, count } from "@tidyjs/tidy";

interface Pokemon {
  id: string;
  name: string;
  level: number;
}

describe("TidyJS Group and Count", () => {
  test("Count Pokemon by name and level", () => {
    const inputData: Pokemon[] = [
      { id: "1", name: "ヒトカゲ", level: 28 },
      { id: "2", name: "ヒトカゲ", level: 28 },
      { id: "3", name: "ヒトカゲ", level: 28 },
      { id: "4", name: "リザード", level: 35 },
      { id: "5", name: "リザード", level: 35 },
      { id: "6", name: "リザードン", level: 38 },
      { id: "7", name: "リザードン", level: 38 },
      { id: "8", name: "リザードン", level: 38 },
      { id: "9", name: "リザードン", level: 38 },
    ];

    const outputData = tidy(
      inputData,
      groupBy(
        ["name", "level"],
        [
          summarize({
            count: (d) => d.length,
          }),
        ]
      )
    );

    const expectedOutput = [
      {
        name: "ヒトカゲ",
        level: 28,
        count: 3,
      },
      {
        name: "リザード",
        level: 35,
        count: 2,
      },
      {
        name: "リザードン",
        level: 38,
        count: 4,
      },
    ];

    expect(outputData).toEqual(expectedOutput);
  });
});
そうえもんそうえもん

すいません勘違いしてました。
サンプルコードも勉強になりました!色々と教えていただきありがとうございました!

junerjuner

問題コードの

pokémon = { name: "リザードン", level: pokémon.level };

Object.assign(pokémon, { name: "リザードン", level: pokémon.level });

にすればよかったのでは……?みたいなところあります。(※ただし、この場合はインスタンス(参照値)が変更されない)

pokémon が 参照変数でなかったのが原因ですね。(※ javascript に参照変数は無い)

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/assign

参考まで 参照変数のある C# のコードも載せておきます

(string name, int level)[] party = [
  ( name: "ヒトカゲ", level: 8),
  ( name: "リザード", level: 36 ),
  ( name: "リザードン", level: 100 ),
];

// 処理1
foreach (ref var pokémon in party.AsSpan()) {
  if (pokémon.name == "リザード" && pokémon.level >= 36) {
    pokémon = (name: "リザードン", level: pokémon.level );
  }
}
Console.WriteLine($"{string.Join(", ", party.Select(v => $"{{name:{v.name} level:{v.level}}}"))}");
// {name:ヒトカゲ level:8}, {name:リザードン level:36}, {name:リザードン level:100}

https://sharplab.io/#v2:C4LgTgrgdgPgAgJgIwFgBQcAMACOSB0AMgJZQCOA3OtWgBR45QCGAtgKYA02pw2ANmwBubPgEoA2gF1sAByZhgAT2wBebOPTZstbM3YhsAIkBLDIAmGQNUMgJoZDXAcL4GAHKI6btu1mwOHAVwyA2hkAfhkBJhht+IREDAGYANmwXNx09LyN/YMBnhlC7COwkTBx4tEkqNHQAelLsQDPFQDAXQER/9AAzAHswNiYAYwALbVaG7EF5WSaAawBLliaobim5BUV8AEEAZwBlOShaUVFsAG83Yj7aGRHxyfwk1TVfQJDsADI7obGJqHwsvmwAPjUY7b20LRaY7PSaqbRJbypIIZWzhBxPU6vd5xYpaAC+6AxGCQAE5aAASQw7Bj4ABSTVItFCoVmSnwKxEbHawFoglUn2whJ2OwhO0E508aLC9hAfLecLRksMWzR0ooQA===

junerjuner

参照変数が無いなら作ればいいというのだとこういうアプローチでもよいですね。

const party = [
  { name: 'ヒトカゲ', level: 8 },
  { name: 'リザード', level: 36 },
  { name: 'リザードン', level: 100 },
];

// 処理1
for (let pokémon of ref(party)) {
  const p = pokémon.value;
  if (p.name === 'リザード' && p.level >= 36) {
    pokémon.value = { name: 'リザードン', level: p.level };
  }
}
console.log(party);

function* ref(array) {
  for (let index of array.keys())
    yield {
      get value() {
        return array[index];
      },
      set value(value) {
        array[index] = value;
      },
    };
}

React 系だとこうですかね

const party = [
  { name: 'ヒトカゲ', level: 8 },
  { name: 'リザード', level: 36 },
  { name: 'リザードン', level: 100 },
];

// 処理1
for (let [pokémon, setPokémon] of state(party)) {
  if (pokémon.name === 'リザード' && pokémon.level >= 36) {
    setPokémon({ name: 'リザードン', level: pokémon.level });
  }
}
console.log(party);

function* state(array) {
  for (let index of array.keys())
    yield [
      array[index],
      (value) => array[index] = value,
    ];
}
そうえもんそうえもん

junerさんコメントありがとうございます!
ジェネレータ関数ですね!このような書き方ができるんですね、、、(Reaceの例もありがとうございます)
ジェネレータ関数もですが、「参照」「参照渡し」周りの理解が浅いことに気づけたのでどこかで勉強してみようと思いました。
良い機会をいただきありがとうございました!

そうえもんそうえもん

junerさんコメントありがとうございます!
ジェネレータ関数ですね!このような書き方ができるんですね、、、(Reaceの例もありがとうございます)
ジェネレータ関数もですが、「参照」「参照渡し」周りの理解が浅いことに気づけたのでどこかで勉強してみようと思いました。
良い機会をいただきありがとうございました!