1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JavaScriptにおけるオブジェクトコピーの落とし穴

Posted at

1. テーマの概要と重要性

JavaScriptでオブジェクトを扱う際、「参照」という概念は非常に重要です。オブジェクトは単純に代入しただけでは独立したコピーが作られず、同じメモリ上の場所を指し示すため、一方を変更すると他方も変更されるという現象が起こります。これはプリミティブ型(数値や文字列)の挙動とは大きく異なります。

この「参照」の仕組みを理解し、「シャローコピー(浅いコピー)」と「ディープコピー(深いコピー)」の違いを把握することは、予期せぬバグを防ぎ、データの整合性を維持するために必須の知識です。特に状態管理が重要なWebアプリケーション開発では、この概念の理解が開発者としての成長に大きく影響します。

2. 前提知識の整理

JavaScriptのオブジェクトの参照と深いコピーを理解するために必要な基礎知識:

  • プリミティブ型とオブジェクト型の違い:数値、文字列、真偽値などのプリミティブ型は値としてコピーされるが、オブジェクト、配列、関数などのオブジェクト型は参照としてコピーされる
  • 変数とメモリの関係:変数はメモリ上の値や参照を格納する「箱」
  • オブジェクトリテラル{}を使ったオブジェクトの作成と、プロパティへのアクセス
  • 参照の概念:データそのものではなく、データが格納されているメモリ上の場所を指すポインタ
  • 変更可能性(ミュータビリティ):JavaScriptのオブジェクトは作成後でもプロパティの追加・変更・削除が可能

3. あきらパパと太郎の対話によるポイント整理

ポイント1:オブジェクトは参照として扱われる

あきらパパ:「太郎くん、JavaScriptのオブジェクトについて話そうか。まず簡単な例を見てみよう」

let user1 = { name: "太郎" };
let user2 = user1;

user2.name = "次郎";
console.log(user1.name); // "次郎"と表示される

あきらパパ:「何が起きたと思う?user2を変更したのに、user1まで変わっちゃったね」

太郎:「えっ!なんでですか?user1とuser2は別々の変数なのに...」

あきらパパ:「そこがポイントなんだ。JavaScriptではね、オブジェクトを変数に入れると、その変数には『オブジェクトそのもの』じゃなくて『オブジェクトがどこにあるかを示す住所』みたいなものが入るんだよ。これを『参照』って言うんだ」

太郎:「住所...つまり両方の変数が同じオブジェクトを指してるから、どっちから変更しても同じものが変わるんですね!」

あきらパパ:「そうそう!そのイメージで正解。メモリ上の同じ場所を指しているから、どちらから変更しても同じオブジェクトが変更されるんだ」

ポイント2:シャローコピーとディープコピーの違い

あきらパパ:「じゃあ、独立したコピーを作るにはどうすればいいと思う?」

太郎:「えっと...新しいオブジェクトを作ればいいんですか?」

あきらパパ:「そうだね!でも、そこにも落とし穴があるんだ。次の例を見てみよう」

let user1 = { 
  name: "太郎", 
  profile: { age: 16 } 
};

// Object.assignを使ったコピー
let user2 = Object.assign({}, user1);

user2.name = "次郎";
console.log(user1.name); // "太郎" - 変わっていない

user2.profile.age = 17;
console.log(user1.profile.age); // 17 - 変わってしまった!

太郎:「あれ?nameは独立してるのに、profile.ageは変わっちゃいましたね...」

あきらパパ:「その通り!これが『シャローコピー(浅いコピー)』と呼ばれるものだよ。一番上の階層のプロパティだけが新しくコピーされて、ネストした(入れ子になった)オブジェクトは参照がコピーされるだけなんだ」

太郎:「なるほど!じゃあ完全に独立させるには?」

あきらパパ:「それが『ディープコピー(深いコピー)』で、すべての階層で新しいオブジェクトを作ることだね」

ポイント3:シャローコピーの方法

太郎:「シャローコピーって、どんな方法があるんですか?」

あきらパパ:「よく使われる方法をいくつか紹介するね」

// 方法1: Object.assign()
let copy1 = Object.assign({}, original);

// 方法2: スプレッド演算子
let copy2 = {...original};

// 方法3: 配列の場合はslice()
let arrCopy = originalArray.slice();
// または
let arrCopy2 = [...originalArray];

あきらパパ:「これらはどれも似たようなことをしてて、オブジェクトの一番上の階層だけを新しいオブジェクトにコピーするんだ」

太郎:「へぇ〜、選択肢がいくつもあるんですね。でも結局どれも『浅い』コピーなんですね」

あきらパパ:「そうそう、どれも同じ制限があるんだよ。ネストしたオブジェクトは参照コピーになっちゃう」

ポイント4:ディープコピーの方法

太郎:「じゃあディープコピーはどうやるんですか?」

あきらパパ:「一番簡単な方法はこれかな」

// JSON変換を使ったディープコピー
let deepCopy = JSON.parse(JSON.stringify(original));

あきらパパ:「これは一度オブジェクトをJSON文字列に変換して、それを再度パースすることで完全に新しいオブジェクトを作る方法だよ」

太郎:「おお!それならすべての階層が独立するんですね!」

あきらパパ:「その通り。でもこの方法にも制限があるんだ。関数やDateオブジェクト、正規表現、循環参照などは正しくコピーできない。もっと完全なディープコピーには専用のライブラリを使ったり、『構造化クローンアルゴリズム』を使ったりする方法もあるよ」

ポイント5:イミュータブルな操作の重要性

あきらパパ:「最後に大事なポイントとして、『イミュータブル(不変)』な操作について話そう」

太郎:「イミュータブル?それは何ですか?」

あきらパパ:「元のデータを変更せず、常に新しいコピーを作って操作することだよ。特にReactのような現代的なフレームワークでは重要な概念なんだ」

// ミュータブル(変更可能)な操作 - 避けるべき
function addTask(tasks, newTask) {
  tasks.push(newTask);  // 元の配列を変更
  return tasks;
}

// イミュータブル(不変)な操作 - 推奨
function addTask(tasks, newTask) {
  return [...tasks, newTask];  // 新しい配列を返す
}

太郎:「なるほど!元のデータを変更せず、新しいデータを作るんですね。これなら副作用が少なくなりそう!」

あきらパパ:「その通り!予測可能性が高まって、バグも減らせるんだ。特に状態管理が重要なアプリではね」

4. 段階的な学習ロードマップ

初級

  • プリミティブ型とオブジェクト型の違いを理解する
  • 参照の基本概念を学ぶ
  • 単純なオブジェクトのコピー方法を知る
  • Object.assignとスプレッド演算子によるシャローコピーを習得する

中級

  • ネストしたオブジェクトでの参照問題を理解する
  • JSON.stringifyとJSON.parseによるディープコピー方法を学ぶ
  • ディープコピーの制限を知る
  • イミュータブルな操作パターンを理解する

応用

  • 複雑なオブジェクト構造でのコピー戦略を立てる
  • 循環参照などの特殊ケースを処理できる
  • 構造化クローンやライブラリによる高度なコピー方法を使いこなす
  • パフォーマンスを考慮したコピー方法を選択できる

5. 主要概念の詳細説明

参照とは

太郎:「あきらパパ、参照って具体的にどういうものなんですか?」

あきらパパ:「いい質問だね。プログラミングにおける『参照』とは、データそのものじゃなくて、データがメモリのどこに保存されているかという『住所』のようなものなんだ」

太郎:「うーん、まだイメージがつかめないです...」

あきらパパ:「じゃあ、身近な例で考えてみよう。例えば図書館のことを考えてみて。本が実際のデータとすると、その本の『請求番号』が参照に当たるんだ。君が友達に『面白い本があるよ、請求番号はJ918.68だよ』と教えたとすると、その友達は同じ本を手に取ることができる。これが参照の仕組みなんだ」

太郎:「なるほど!変数にはオブジェクトそのものではなく、『どこにあるか』という情報が入ってるんですね!」

あきらパパ:「そうそう!だから変数をコピーすると、保存場所の情報だけがコピーされて、指し示す先は同じオブジェクトのままなんだよ」

// メモリのイメージ図
// 変数: [参照アドレス] → メモリ上のオブジェクト: {プロパティ: 値}

let obj1 = { value: 10 };
// obj1: [0x123] → メモリ0x123: {value: 10}

let obj2 = obj1;
// obj2: [0x123] → メモリ0x123: {value: 10}
// obj1とobj2は同じメモリアドレスを参照している

obj2.value = 20;
// メモリ0x123: {value: 20}
// obj1とobj2は同じオブジェクトを参照しているので両方の値が変わる

シャローコピーの詳細

太郎:「シャローコピーについてもう少し詳しく教えてください」

あきらパパ:「シャローコピーは、オブジェクトの『一番上の階層』だけを新しく作るコピー方法だよ。図で表すとこんな感じかな」

元のオブジェクト:
person1 = {
  name: "太郎",
  age: 16,
  scores: {
    math: 90,
    english: 85
  }
}

シャローコピー後:
person2 = {
  name: "太郎",  // 新しいメモリに保存された独立した値
  age: 16,       // 新しいメモリに保存された独立した値
  scores: {      // 元のオブジェクトと同じ参照
    math: 90,
    english: 85
  }
}

あきらパパ:「nameとageは新しくコピーされるけど、scoresオブジェクトは元のオブジェクトと同じものを参照しているんだ」

太郎:「だから、person2.scores.mathを変更すると、person1.scores.mathも変わってしまうんですね!」

あきらパパ:「その通り!シャローコピーの制限はそこなんだよね。実際のコードで見てみよう」

const person1 = {
  name: "太郎",
  age: 16,
  scores: {
    math: 90,
    english: 85
  }
};

// シャローコピー
const person2 = Object.assign({}, person1);
// または const person2 = {...person1};

person2.name = "次郎";         // 問題なし - トップレベルのプロパティ
console.log(person1.name);     // "太郎" のまま

person2.scores.math = 95;      // ネストしたオブジェクトを変更
console.log(person1.scores.math); // 95 - person1の値も変わってしまう!

ディープコピーの詳細

太郎:「JSON.parse(JSON.stringify())でディープコピーができるというのは興味深いです。どうしてそれで深いコピーができるんですか?」

あきらパパ:「いい質問だね!この方法は『シリアライズ』と『デシリアライズ』を使ったトリックなんだ。まずJavaScriptのオブジェクトをJSON文字列に変換して、それを再度JavaScriptのオブジェクトに戻す。その過程で、すべての値が新しく作られるから、完全に独立したコピーができるんだよ」

const original = {
  name: "太郎",
  age: 16,
  scores: {
    math: 90,
    english: 85
  }
};

// ステップ1: JSONに変換(シリアライズ)
const jsonString = JSON.stringify(original);
console.log(jsonString);
// 結果: {"name":"太郎","age":16,"scores":{"math":90,"english":85}}

// ステップ2: JSONからオブジェクトに変換(デシリアライズ)
const deepCopy = JSON.parse(jsonString);

あきらパパ:「これで元のオブジェクトとは完全に独立したコピーができるんだ」

太郎:「おお、なるほど!でも制限があるって言ってましたよね?」

あきらパパ:「鋭いね!この方法では次のようなデータはうまくコピーできないんだ:

  1. 関数:JSONには関数の概念がないので、関数はundefinedになる
  2. undefined値:JSONでサポートされていないので消えてしまう
  3. Date、RegExp、Map、Set:普通のオブジェクトや文字列に変換される
  4. 循環参照:自分自身を参照するオブジェクトはエラーになる

実際に見てみよう」

const originalWithFunctions = {
  name: "太郎",
  greet: function() { console.log("こんにちは"); },
  birthday: new Date(2007, 0, 1),
  regex: /test/,
  undefinedValue: undefined
};

// ディープコピーを試みる
const deepCopyAttempt = JSON.parse(JSON.stringify(originalWithFunctions));

console.log(deepCopyAttempt);
// 結果: {
//   "name": "太郎",
//   "birthday": "2007-01-01T00:00:00.000Z", // 文字列に変換された
//   "regex": {}                            // 空オブジェクトに
//   // greetとundefinedValueは消えてしまった
// }

太郎:「確かに!いくつかのプロパティが消えたり変わったりしてますね」

あきらパパ:「そうなんだ。だからこの方法を使うときは、オブジェクトの中身が何かをよく理解しておく必要があるね」

構造化クローンとライブラリによるディープコピー

太郎:「JSON.parse(JSON.stringify())の制限を回避する方法はあるんですか?」

あきらパパ:「あるよ。一つは『構造化クローンアルゴリズム』を使う方法だね。これはブラウザが提供するAPIで、より多くの型をサポートしているんだ」

// 構造化クローンアルゴリズムを使ったディープコピー
function structuredClone(obj) {
  return new window.MessageChannel().port1.postMessage(obj);
}

// または最新のブラウザでは直接使える
const deepCopy = structuredClone(original);

あきらパパ:「もう一つの方法は、専用のライブラリを使う方法だね。例えば『lodash』というライブラリにはcloneDeepという関数があって、これを使えばほとんどのケースで問題なくディープコピーができるよ」

// lodashを使用した例(事前にライブラリの読み込みが必要)
const _ = require('lodash');
const deepCopy = _.cloneDeep(original);

太郎:「へぇ〜、専用のライブラリまであるんですね!」

あきらパパ:「そうだね。実務ではこういったライブラリを使うことも多いよ。特に複雑なオブジェクト構造を扱う場合はね」

6. 実践的な例題と詳しい解説

例題1:シンプルなオブジェクトの参照問題

あきらパパ:「では、簡単な例題から始めよう。ユーザー情報を管理するプログラムで、参照がどう問題になるか見てみよう」

// ユーザー情報を管理するプログラム
function updateUser(user, newName) {
  // ユーザー情報を更新
  user.name = newName;
  return user;
}

const originalUser = { id: 1, name: "太郎" };
console.log("元のユーザー:", originalUser);

// バックアップを取っておく(つもり)
const backupUser = originalUser;

// ユーザー情報を更新
const updatedUser = updateUser(originalUser, "次郎");
console.log("更新後のユーザー:", updatedUser);

// バックアップを確認
console.log("バックアップユーザー:", backupUser);  // { id: 1, name: "次郎" } になってしまう!

あきらパパ:「このコードの問題点、分かるかな?」

太郎:「あ!backupUserも変わっちゃってますね。本当のバックアップになってない!」

あきらパパ:「その通り!これを修正するにはどうすればいいかな?」

太郎:「えっと...Object.assignとか使えばいいんですよね?」

あきらパパ:「正解!シャローコピーで十分だね。修正してみよう」

// ユーザー情報を管理するプログラム(改良版)
function updateUser(user, newName) {
  // 入力オブジェクトを変更せず、新しいオブジェクトを返す
  const updatedUser = Object.assign({}, user);
  updatedUser.name = newName;
  return updatedUser;
}

const originalUser = { id: 1, name: "太郎" };
console.log("元のユーザー:", originalUser);

// 正しいバックアップ方法
const backupUser = Object.assign({}, originalUser);

// ユーザー情報を更新
const updatedUser = updateUser(originalUser, "次郎");
console.log("更新後のユーザー:", updatedUser);

// バックアップを確認
console.log("バックアップユーザー:", backupUser);  // { id: 1, name: "太郎" } で保持されている!
console.log("元のユーザー:", originalUser);      // { id: 1, name: "太郎" } で保持されている!

例題2:ネストしたオブジェクトとシャローコピー

あきらパパ:「次は、ネストしたオブジェクトがある場合の例を見てみよう」

// 学生の成績管理プログラム
const student = {
  id: 1,
  name: "太郎",
  grades: {
    math: 85,
    science: 90,
    english: 78
  }
};

// 成績のバックアップを取る(つもり)
const backupStudent = Object.assign({}, student);

// 数学の成績を修正
student.grades.math = 95;

console.log("修正後の数学の成績:", student.grades.math);        // 95
console.log("バックアップの数学の成績:", backupStudent.grades.math);  // 95 - バックアップまで変わってしまった!

太郎:「あ!今度はObject.assignを使ったのに、バックアップが変わってますね」

あきらパパ:「そうなんだ。これがシャローコピーの限界だね。ネストしたgradesオブジェクトは参照がコピーされただけだからね。これを修正するには?」

太郎:「ディープコピーが必要ですね!JSON.stringify使えばいいんですよね?」

あきらパパ:「その通り!修正してみよう」

// 学生の成績管理プログラム(改良版)
const student = {
  id: 1,
  name: "太郎",
  grades: {
    math: 85,
    science: 90,
    english: 78
  }
};

// ディープコピーでバックアップを取る
const backupStudent = JSON.parse(JSON.stringify(student));

// 数学の成績を修正
student.grades.math = 95;

console.log("修正後の数学の成績:", student.grades.math);        // 95
console.log("バックアップの数学の成績:", backupStudent.grades.math);  // 85 - バックアップはそのまま!

例題3:実際のアプリケーションでのディープコピーの応用

あきらパパ:「最後に、実際のアプリケーションでどう使うかの例を見てみよう。簡単なTodoリストアプリを考えてみるね」

// Todoリストの状態管理
const appState = {
  todos: [
    { id: 1, text: "牛乳を買う", completed: false },
    { id: 2, text: "宿題をする", completed: true }
  ],
  filter: "all",
  user: {
    name: "太郎",
    preferences: {
      theme: "light",
      fontSize: "medium"
    }
  }
};

// ❌ 悪い例: 状態を直接変更
function toggleTodoBad(state, todoId) {
  // 該当するTodoを見つける
  const todo = state.todos.find(todo => todo.id === todoId);
  // 直接変更(ミュータブル)
  if (todo) {
    todo.completed = !todo.completed;
  }
  return state;  // 同じオブジェクトを返す
}

// ✅ 良い例: イミュータブルな状態更新
function toggleTodoGood(state, todoId) {
  // 新しい状態を作成(ディープコピー)
  const newState = JSON.parse(JSON.stringify(state));
  // 新しい状態内のTodoを見つけて変更
  const todo = newState.todos.find(todo => todo.id === todoId);
  if (todo) {
    todo.completed = !todo.completed;
  }
  return newState;  // 新しいオブジェクトを返す
}

// 実行してみる
console.log("元の状態:", appState);

// 悪い方法で更新
const badResult = toggleTodoBad(appState, 1);
console.log("悪い方法の結果:", badResult);
console.log("元の状態(変わってしまった):", appState);  // 元の状態も変更されている

// 元に戻す
appState.todos[0].completed = false;

// 良い方法で更新
const goodResult = toggleTodoGood(appState, 1);
console.log("良い方法の結果:", goodResult);
console.log("元の状態(保持されている):", appState);  // 元の状態は変更されていない

太郎:「おお!アプリの状態管理で使う例ですね。確かにこういうところで役立ちそう!」

あきらパパ:「そうだね。特にReactのようなフレームワークではこのイミュータブルな状態更新がとても重要なんだよ。状態が予測可能になって、バグも少なくなるからね」

7. ハンズオン問題とステップバイステップの解説

ハンズオン問題1:オブジェクトの配列を安全にコピーする

あきらパパ:「ではハンズオン問題に挑戦してみよう。次のようなユーザーの配列があるとき、すべてのユーザーの年齢を1つ増やしたいけど、元の配列は変更したくない。どうする?」

const users = [
  { id: 1, name: "太郎", age: 16, scores: { math: 85, english: 90 } },
  { id: 2, name: "花子", age: 15, scores: { math: 95, english: 85 } },
  { id: 3, name: "次郎", age: 17, scores: { math: 75, english: 80 } }
];

太郎:「うーん、配列のコピーを作って、その中の各ユーザーもディープコピーする必要がありそうですね...」

あきらパパ:「その通り!では一緒に解いていこう」

ステップバイステップの解説

ステップ1: 配列全体をディープコピーする

// JSON.stringifyとJSON.parseを使ったディープコピー
const usersCopy = JSON.parse(JSON.stringify(users));

ステップ2: 各ユーザーの年齢を増やす

usersCopy.forEach(user => {
  user.age += 1;
});

ステップ3: 結果を確認する

console.log("元の配列:", users);
console.log("変更した配列:", usersCopy);

// 元の配列が変更されていないことを確認
console.log("最初のユーザーの年齢:", users[0].age);  // 16のまま
console.log("コピーの最初のユーザーの年齢:", usersCopy[0].age);  // 17に増えている

あきらパパ:「完全なコードはこうなるね」

const users = [
  { id: 1, name: "太郎", age: 16, scores: { math: 85, english: 90 } },
  { id: 2, name: "花子", age: 15, scores: { math: 95, english: 85 } },
  { id: 3, name: "次郎", age: 17, scores: { math: 75, english: 80 } }
];

// ディープコピーを作成
const usersCopy = JSON.parse(JSON.stringify(users));

// 年齢を増やす
usersCopy.forEach(user => {
  user.age += 1;
});

// 検証
console.log("元の配列の最初のユーザーの年齢:", users[0].age);  // 16
console.log("コピーの最初のユーザーの年齢:", usersCopy[0].age);  // 17

// scoresも独立していることを確認
usersCopy[0].scores.math = 100;
console.log("元の配列の最初のユーザーの数学の点:", users[0].scores.math);  // 85のまま
console.log("コピーの最初のユーザーの数学の点:", usersCopy[0].scores.math);  // 100に変更された

太郎:「なるほど!配列の中のオブジェクトも全部独立してますね!」

ハンズオン問題2:循環参照を含むオブジェクトを安全にコピーする

あきらパパ:「次に、ちょっと難しい問題。循環参照(自分自身を参照する)オブジェクトをコピーする方法を考えてみよう」

// 循環参照を持つオブジェクト
const circular = {
  name: "循環オブジェクト",
  value: 42,
  self: null  // 後で自分自身を参照させる
};

// 自分自身への参照を作成
circular.self = circular;

// これはエラーになる
try {
  const copy = JSON.parse(JSON.stringify(circular));
  console.log("コピー成功:", copy);
} catch (error) {
  console.error("コピー失敗:", error.message);
}

太郎:「あ、エラーになりましたね...」

あきらパパ:「そうなんだ。JSON.stringifyは循環参照を処理できないんだよ。じゃあどうすればいいかな?」

ステップバイステップの解説

ステップ1: 循環参照を検出・置換するカスタム関数を作成する

function decycle(obj, cache = new WeakMap()) {
  // 既に処理したオブジェクトはキャッシュから返す
  if (obj === null || typeof obj !== 'object') return obj;
  if (cache.has(obj)) return { $ref: 'cyclic' };  // 循環参照をマーク
  
  // 新しいオブジェクトをキャッシュに保存
  const copy = Array.isArray(obj) ? [] : {};
  cache.set(obj, copy);
  
  // 再帰的にプロパティをコピー
  Object.keys(obj).forEach(key => {
    copy[key] = decycle(obj[key], cache);
  });
  
  return copy;
}

ステップ2: 再循環する関数を作成する(オプション)

function recycle(obj, cache = new WeakMap()) {
  // 循環参照を復元する処理(実装省略)
  // 実際のアプリケーションによって異なる
}

ステップ3: 循環参照を扱えるディープコピー関数を作成

function safeDeepCopy(obj) {
  return decycle(obj);
}

ステップ4: 適用して結果を確認

const safeCopy = safeDeepCopy(circular);
console.log(safeCopy);  // 循環参照がマークされたオブジェクトが出力される

あきらパパ:「かなり難しい例だけど、実際のところ、循環参照が必要なケースは限られているんだ。でも、こういう問題もあるということを覚えておくといいよ」

太郎:「難しいですね...でもWeakMapを使って既に処理したオブジェクトを記録するというアイデアはなるほどって感じです!」

あきらパパ:「そうだね。実務ではこういう複雑なケースは、専用のライブラリに任せることが多いよ。例えば『lodash』のcloneDeepは循環参照も適切に処理してくれるんだ」

8. よくつまずくポイントとその克服法

つまずきポイント1:参照の概念を理解できない

あきらパパ:「参照の概念は最初は分かりにくいよね」

太郎:「はい、特に他の言語を知らないと混乱しやすいです」

あきらパパ:「参照を理解するためのコツを3つ紹介するね:

  1. 視覚的なメンタルモデルを作る
    変数は『箱』、プリミティブ値は『箱の中の値』、オブジェクトは『別の場所にあるもの』、オブジェクト型の変数は『その場所への地図』と考える

  2. 実験する
    単純なコードを書いて、様々な操作が結果にどう影響するか試してみる

  3. デバッグツールを使う
    ブラウザの開発者ツールでオブジェクトを展開して詳細を見る習慣をつける

太郎:「なるほど!特に視覚的なイメージは分かりやすいです!」

つまずきポイント2:ディープコピーのパフォーマンス問題

あきらパパ:「ディープコピーは便利だけど、大きなオブジェクトでは性能問題が発生することがあるんだ」

太郎:「ディープコピーは遅いんですか?」

あきらパパ:「そうなんだ。すべてのプロパティをコピーする必要があるから、オブジェクトが大きいと処理時間がかかるし、メモリも多く使うんだよ。克服するための方法を紹介するね:

  1. 必要な部分だけコピーする
    アプリケーションの設計によっては、完全なディープコピーではなく、変更が必要な部分だけをコピーする『部分的なディープコピー』で十分な場合もある

  2. イミュータブルデータ構造を使う
    Immutable.jsのようなライブラリを使うと、効率的なイミュータブルなデータ構造を実現できる

  3. メモ化を活用する
    繰り返し同じオブジェクトをコピーする場合は、結果をキャッシュする

太郎:「へぇ〜、効率化の方法もいろいろあるんですね!」

つまずきポイント3:循環参照によるエラー

あきらパパ:「さっきのハンズオン問題でも見たけど、循環参照があるとJSON.stringify/parseでは対応できないんだよね」

太郎:「循環参照って、実際にはよく起こる問題なんですか?」

あきらパパ:「意図的に作らなければそう頻繁には起こらないけど、DOMオブジェクトや複雑なデータ構造では発生することがあるよ。対処法を紹介するね:

  1. 専用ライブラリを使う
    lodashのcloneDeepのように循環参照を処理できるライブラリを使う

  2. 構造化クローンAPIを使う
    structuredClone()は循環参照も処理できる

  3. カスタム関数を実装する
    さっき見たように、WeakMapを使って既に処理したオブジェクトを追跡する関数を実装する

太郎:「なるほど!特にライブラリを使う方法は簡単そうですね」

9. 発展学習のための追加リソース紹介

あきらパパ:「ここまでの内容をさらに深めるためのリソースをいくつか紹介するね」

  1. MDN Web Docs - オブジェクトのコピー
    https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/assign

  2. JavaScriptの参照とディープコピーについての詳細ガイド
    https://www.digitalocean.com/community/tutorials/js-deep-cloning-javascript-objects

  3. 構造化クローンアルゴリズム - Web API
    https://developer.mozilla.org/ja/docs/Web/API/structuredClone

  4. Immutable.js - 効率的なイミュータブルデータ構造のライブラリ
    https://immutable-js.com/

  5. Lodashライブラリ - cloneDeep関数
    https://lodash.com/docs/4.17.15#cloneDeep

太郎:「わあ、ありがとうございます!特にMDNとImmutable.jsは気になります」

あきらパパ:「いいね!MDNは特に公式ドキュメントだから信頼できるし、Immutable.jsは大規模アプリケーションでよく使われているよ」

10. 理解度チェッククイズ

あきらパパ:「最後に、理解度をチェックするクイズを5問出すね。自分の理解をテストしてみて!」

クイズ1:参照の基本

問題:次のコードを実行した後、console.log(a.value)は何を出力するでしょうか?

let a = { value: 10 };
let b = a;
b.value = 20;

太郎:「えっと...bを変更しているので、aも変わって20ですね!」

あきらパパ:「正解!aとbは同じオブジェクトを参照しているから、どちらを変更しても両方に影響するんだ」

クイズ2:プリミティブ型vs参照型

問題:次のコードを実行した後、console.log(x, y)は何を出力するでしょうか?

let x = 10;
let y = x;
y = 20;

太郎:「これはxは10のまま、yだけ20になります!プリミティブ型は値がコピーされるからですね!」

あきらパパ:「素晴らしい!プリミティブ型と参照型の違いをしっかり理解しているね」

クイズ3:シャローコピー

問題:次のコードを実行した後の出力は何でしょうか?

const original = { a: 1, b: { c: 2 } };
const copy = Object.assign({}, original);
copy.a = 3;
copy.b.c = 4;
console.log(original.a, original.b.c);

太郎:「hmm...originalのaは変わらないので1、でもb.cは参照なので4になります!」

あきらパパ:「正解!シャローコピーの特性をよく理解しているね」

クイズ4:ディープコピー

問題:JSON.parse(JSON.stringify())を使ったディープコピーで次のオブジェクトをコピーした場合、何が失われるでしょうか?

const obj = {
  name: "太郎",
  greet: function() { return "こんにちは"; },
  date: new Date(),
  regex: /test/,
  undef: undefined
};

太郎:「えっと...関数のgreet、undefinedのundef、そしてdateは文字列になって、regexはほとんど空のオブジェクトになるはずです!」

あきらパパ:「完璧!JSON.stringify/parseの制限をしっかり理解しているね」

クイズ5:応用問題

問題:次のコードでは、なぜimmutableAddUserの方が推奨されるのでしょうか?

// ユーザー配列
const users = [
  { id: 1, name: "太郎" },
  { id: 2, name: "花子" }
];

// 方法1
function mutableAddUser(users, name) {
  users.push({ id: users.length + 1, name });
  return users;
}

// 方法2
function immutableAddUser(users, name) {
  return [...users, { id: users.length + 1, name }];
}

太郎:「immutableAddUserは元の配列を変更せず、新しい配列を返すので、元のデータが保持されます。これによって予測可能性が高まり、バグも減らせるんですよね!」

あきらパパ:「素晴らしい!イミュータブルな操作の重要性をしっかり理解しているね。特にReactのようなフレームワークでは、状態を直接変更せず新しいオブジェクトを返す方法が重要なんだ」

太郎:「ありがとうございます!参照とコピーの違い、シャローコピーとディープコピーの違い、そしてイミュータブルな操作の重要性がよく分かりました!これで安全にオブジェクトを操作できそうです」

あきらパパ:「そうだね。この知識はJavaScriptの開発において本当に重要だから、今日学んだことをぜひ実際のコードで試してみてね!何か質問があればいつでも聞いてね」

まとめ

あきらパパ:「じゃあ、今日学んだことをまとめておこうか。

JavaScriptでオブジェクトを扱う際の『参照』と『コピー』について学んだね。重要なポイントをおさらいすると:

  1. 参照の仕組み:JavaScriptのオブジェクトは代入すると『値』ではなく『参照』がコピーされる。つまり、同じメモリ上のオブジェクトを指すようになるため、一方を変更するともう一方も変わってしまう。

  2. シャローコピーとディープコピー

    • シャローコピー(浅いコピー)は、一番上の階層だけが新しくなり、ネストしたオブジェクトは依然として同じ参照を持つ
    • ディープコピー(深いコピー)は、すべての階層で完全に独立したコピーを作る
  3. シャローコピーの方法

    • Object.assign({}, obj)
    • スプレッド演算子 {...obj}
    • 配列なら array.slice()[...array]
  4. ディープコピーの方法

    • JSON.parse(JSON.stringify(obj)) - 簡単だが関数やDateなどをうまく扱えない制限がある
    • ライブラリ(lodashのcloneDeepなど)- より多くの型に対応
    • structuredClone() - モダンなブラウザで使用可能
  5. イミュータブルな操作の重要性

    • 元のデータを変更せず、新しいコピーを返す方法
    • 予測可能性が高まり、バグも減らせる
    • 特にReactなどのモダンなフレームワークでは重要な概念

この知識を活かして、安全で予測可能なコードを書いていこう!特にオブジェクトを変更する前は『これは参照?それともコピー?』と自問する習慣を身につけるといいよ。

最初はちょっと分かりにくい概念かもしれないけど、実際にコードを書いて試してみることで徐々に理解が深まるはずだよ。ぜひ実践して、参照とコピーをマスターしてくれ!」

太郎:「ありがとうございます!参照とコピーの違いがよく分かりました。特にシャローコピーとディープコピーの違いは重要ですね。これからコードを書くときは、意図せず元のデータを変更してしまわないよう、適切なコピー方法を選ぶようにします!」

1
1
0

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
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?