LoginSignup
10
5

More than 1 year has passed since last update.

javascript オブジェクトの参照とコピーについて

Last updated at Posted at 2021-08-02

挨拶

@hakeと申します。初投稿です!
オブジェクトの参照とコピーについて勉強し直したので記事にしました!
知っている方も復習する気持ちで読んでいただけると嬉しいです!

以下のコードには問題があります

Index.tsx
  const [team, setTeam] = useState({
    engineer: { reader: "kazu", member: ["eita", "yasu"] },
    designer: { reader: "satoshi", member: ["keita", "tadashi"] },
  });

  // bad
  const setEngeneerReader = () => {
    // スプレッド構文でteamをコピーする
    const newTeam = { ...team };
    // 深くネストされた値を変更する
    newTeam.engineer.reader = "koki";
    console.log(
      "prevReader",
      team.engineer.reader,
      "newReader",
      newTeam.engineer.reader
    );
    // →出力:prevReader koki newReader koki

    // この時点でコピー元であるteam.engineer.readerが変更されている
    // →意図していない挙動

    setTeam(newTeam);
  };

Reactにはstateを直接変更しては行けないというルールがあるにもかかわらず、変更してしまっています。
https://ja.reactjs.org/docs/state-and-lifecycle.html#using-state-correctly

こういったミスをしないためにも参照やコピーについて理解が必要

参照とは

他の場所にあるデータを指している情報を含む小さなオブジェクトであり、それ自身の中に(指している)データ自体を含まない。

名前の例

名前を識別子とすると、実際の「人」が値で、値に情報がたくさん詰まっている。
→(例)「のび太」という名前はのび太くん本体(値)を参照していると言える。

のび太くん本体 ← のび太
のび太くん本体 ← のびくん
のび太くん本体 ← のびちゃん
といったように複数の名前(あだ名)がのび太くん本体を参照している

参照によるメリット

複数のコードが参照によってひとつのデータを共有することができ、メモリの節約になる

コピーの種類

シャローコピー(浅いコピー)

オブジェクトの参照をコピーする
コピー元とコピー先で同じ値を参照する

ディープコピー(深いコピー)

オブジェクトの値をコピー
コピー元とコピー先で別の値を参照する

プリミティブ値のコピー

Index.tsx
  let a = "a";
  let b = "b";
  a = b;
  b = "c";
  console.log(a);
  // →出力:"b"
  console.log(b);
  // →出力:"c"

プリミティブ値の場合は値がコピーされる

オブジェクトのコピー

Index.tsx
  let obj = {p1: "dora", p2: "nobi", p3: "shizu"}
  let obj2 = obj

  obj2.p1 = "sune"

  console.log(obj.p1)
  // →出力:"sune"
  console.log(obj2.p1)
  // →出力:"sune"

オブジェクトの場合は値への参照がコピーされる
→obj2.p1を変更すると元の値が変更されてしまうので注意が必要!

とはいっても、コピー元を変更せずにコピー先を変更したいとき、ありますよね😩

コピー元を変更せずにコピー先を変更する方法

パターン1:Object.assign

Index.tsx
  let obj = {p1: "dora", p2: "nobi", p3: "shizu"}
  let obj2 = Object.assign({}, obj)

  obj2.p1 = "sune"

  console.log(obj.p1);
  // →出力:"dora"
  console.log(obj2.p1);
  // →出力:"sune"

パターン2:スプレッド演算子

Index.tsx
  let obj = {p1: "dora", p2: "nobi", p3: "shizu"}
  let obj2 = {...obj}

  obj2.p1 = "sune"

  console.log(obj.p1);
  // →出力:"dora"
  console.log(obj2.p1);
  // →出力:"sune"

問題点

①②の問題点:プロパティにオブジェクトがネストされている場合、そのオブジェクトは参照がコピーされる。

Index.tsx
  let obj = { p1: "nobi", p2: "shizu", robot: { r1: "doraemon", r2: "dorami" } };
  let obj2 = { ...obj };

  obj2.robot.r1 = "roomba";

  console.log(obj.robot.r1);
  // →出力:"roomba"
  // 同じ値を参照しているので元の値も変更されてしまう
  console.log(obj2.robot.r1);
  // →出力:"roomba"

つまり、上記のパターンはシャローコピー(浅いコピー)!!!
→深いネストのオブジェクトをコピーした場合はコビー元も変更されてしまう!!

##ディープコピーしたい場合

パターン1:一度json文字列に変換する

Index.tsx
  let obj = {p1: "nobita", p2: "shizu", robot: {r1: "doraemon", r2: "dorami"}}
  let obj2 = Object.assign({}, JSON.parse(JSON.stringify(obj)))

  obj2.robot.r1 = "roomba"

  console.log(obj.robot.r1);
  // →出力:"doraemon"
  console.log(obj2.robot.r1);
  // →出力:"roomba"

以下のオブジェクトはdeepCopyできないのであまり推奨されていない
・Dateオブジェクト
・function
・undefined

パターン2:lodashのcloneDeep

Index.tsx
  let obj = { p1: "nobi", p2: "shizu", robot: { r1: "doraemon", r2: "dorami" } };
  let obj2 = _.cloneDeep(obj);

  obj2.robot.r1 = "roomba";

  console.log(obj.robot.r1);
  // →出力:"doraemon"
  console.log(obj2.robot.r1);
  // →出力:"roomba"

問題のコードをもう一度見てみよう!

Index.tsx
  const [team, setTeam] = useState({
    engineer: { reader: "kazu", member: ["eita", "yasu"] },
    designer: { reader: "satoshi", member: ["keita", "tadashi"] },
  });

  // bad
  const setEngeneerReader = () => {
    // teamをディープコピーする
    const newTeam = _.cloneDeep(team);
    // 深くネストされた値を変更する
    newTeam.engineer.reader = "koki";
    console.log(
      "prevReader",
      team.engineer.reader,
      "newReader",
      newTeam.engineer.reader
    );
    // →出力:prevReader koki newReader koki

    // この時点でコピー元であるteam.engineer.readerが変更されている
    // →意図していない挙動

    setTeam(newTeam);
  };

とは言っても、なるべくネストしないように定義するべきですね!!!

おまけ

分割代入

Index.tsx
  const obj = {
    p: "dora",
  };

  let { p } = obj;
  p = "nobi";

  console.log(obj.p, p);
  // →出力:"dora" "nobi"

  const obj2 = {
    p1: {
      p2: "dora",
    },
  };

  let { p1 } = obj2;

  p1.p2 = "nobi";

  console.log(obj2.p1.p2, p1.p2);
  // →出力:"nobi" "nobi"

分割代入の場合オブジェクトがネストされている場合、参照がコピーされるので注意!!

新しくインスタンス化

Index.tsx
  class Team {
    public id: number = 0;
    public engineer: { reader: string; front: string[]; back: string[] } = {
      reader: "",
      front: [],
      back: [],
    };
    public designer: { reader: string; member: string[] } = {
      reader: "",
      member: [],
    };

    constructor(init?: Partial<Team>) {
      // assign
      Object.assign(this, init);
    }
  }

  const team1 = new Team({
    id: 1,
    engineer: {
      reader: "dora",
      front: ["atom", "haro"],
      back: ["r2", "c3"],
    },
    designer: {
      reader: "roomba",
      member: ["braaba", "eufy"],
    },
  });

  const team2 = new Team(team1);
  team2.engineer.reader = "ashimo";

  console.log(team1.engineer.reader, team2.engineer.reader);
  // →出力:"ashimo" "ashimo"

newでのインスタンス化はシャローコピーになる

▼複業でスキルを活かしてみませんか?複業クラウドの登録はこちら!
https://talent.aw-anotherworks.com/?login_type=none

10
5
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
5