JavaScript | ES6+ 文法:スプレッド構文 – オブジェクトコピー

JavaScript
スポンサーリンク

オブジェクトコピーとスプレッド構文の関係

オブジェクトのスプレッド構文は、{ ...obj } のように書いて「オブジェクトの中身(プロパティ)を展開して、新しいオブジェクトを作る」書き方です。ここが重要です:const copy = original; は “同じオブジェクトを指す別名” を作るだけですが、const copy = { ...original }; は “中身が同じ別オブジェクト” を作ります。元のオブジェクトを壊さずに扱いたいなら、スプレッド構文でコピーを作るのが基本になります。

const user = { id: 1, name: "Alice" };

const alias = user;        // 同じオブジェクトへの別名
const copy  = { ...user }; // 中身をコピーした別オブジェクト

alias.name = "Bob";

console.log(user);  // { id: 1, name: "Bob" }
console.log(copy);  // { id: 1, name: "Alice" } ← 影響を受けていない
JavaScript

代入とスプレッドコピーの違いをしっかり理解する

代入(=)は「同じ箱に別ラベルを貼る」イメージです。どちらから触っても同じ中身が変わります。

const config = { theme: "light" };
const same = config;  // 参照コピー

same.theme = "dark";

console.log(config.theme); // "dark"
console.log(same.theme);   // "dark"
console.log(config === same); // true(完全に同じもの)
JavaScript

スプレッドコピーは「中身を複製して別の箱を作る」イメージです。ここが重要です:{ ...obj } で作った方は「別物」なので、一方を変更しても、もう一方には影響しません。

const config = { theme: "light" };
const copy = { ...config }; // 浅いコピー

copy.theme = "dark";

console.log(config.theme); // "light"(元はそのまま)
console.log(copy.theme);   // "dark"
console.log(config === copy); // false(別オブジェクト)
JavaScript

「元を壊さずに一部だけ変えたい」という場面では、必ず { ...obj } でコピーを作ってから変更する、という習慣がとても重要です。

浅いコピーという性質(ネストした中身の扱い)

スプレッド構文によるコピーは「浅いコピー(shallow copy)」です。これは「一段目のプロパティだけ新しく作り、プロパティの中に入っているオブジェクトや配列は“参照を共有する”」という意味です。

const state = {
  id: 1,
  profile: { name: "Alice", age: 20 }
};

const copy = { ...state };

copy.profile.name = "Bob";

console.log(state.profile.name); // "Bob"
console.log(copy.profile.name);  // "Bob"
console.log(state === copy);         // false(外側は別)
console.log(state.profile === copy.profile); // true(内側は同じ)
JavaScript

ここが重要です:

  • 数値や文字列だけのシンプルな設定オブジェクトなら、だいたい気にしなくてよい
  • 中にオブジェクトや配列が入っているときは、「そこは共有される」ことを頭に入れておく

「ネストしたオブジェクトまで完全に別物にしたい」場合は、ディープコピー用の処理(再帰的にコピーする、専用ライブラリを使うなど)が必要になります。まずは「スプレッドは浅いコピー」だけしっかり押さえておけば十分です。

「元を変えずに一部だけ変える」定番パターン

設定や状態を扱うとき、「元のオブジェクトをそのまま残しつつ、一部だけ変えた新バージョンを作る」ということがよくあります。ここが重要です:{ ...old, 変えたいプロパティ: 新しい値 } が定番パターンです。右側に書いたものほど優先される(上書きされる)ことも覚えておきましょう。

const config = {
  theme: "light",
  lang: "ja",
  debug: false
};

// debug だけ true にした新設定
const newConfig = {
  ...config,
  debug: true
};

console.log(config);
// { theme: "light", lang: "ja", debug: false }

console.log(newConfig);
// { theme: "light", lang: "ja", debug: true }
JavaScript

順番を逆にすると結果が変わるのも確認しておきましょう。

const a = { x: 1, y: 2 };
const b = { y: 99, z: 3 };

const ab = { ...a, ...b }; // { x: 1, y: 99, z: 3 }
const ba = { ...b, ...a }; // { y: 2, z: 3, x: 1 }
JavaScript

「デフォルト → 上書きする実際の値」の順に並べる、というルールにしておくと迷いません。

分割代入の rest と組み合わせて「除外コピー」

スプレッドは「展開(コピー・マージ・上書き)」の機能で、プロパティを“削除”することはできません。特定のプロパティを除いたコピーを作りたいときは、オブジェクトの分割代入の ...rest と組み合わせます。

const user = {
  id: 1,
  name: "Alice",
  password: "secret",
  token: "abc123"
};

// password と token を取り除いた“公開用”オブジェクト
const { password, token, ...publicUser } = user;

console.log(publicUser);
// { id: 1, name: "Alice" }
JavaScript

ここが重要です:

  • 分割代入の ...publicUser(左側)は「残りをまとめて受け取る」
  • スプレッドの { ...publicUser }(右側)は「その中身を展開して新しいオブジェクトを作る」

「機密情報を除いたオブジェクトを別で扱う」ような場面で非常に役立ちます。

関数の中での「安全なオブジェクト操作」

外から渡されたオブジェクトを、関数の中で直接書き換えると、呼び出し側が予想していない動きになりがちです。ここが重要です:関数内では「まずスプレッドでコピーを作ってから変更し、新しいオブジェクトを返す」習慣をつけると、安全でバグの少ないコードになります。

function enableDebug(config) {
  // 引数の config は触らず、新しいオブジェクトを返す
  return {
    ...config,
    debug: true
  };
}

const base = { theme: "light", debug: false };
const updated = enableDebug(base);

console.log(base);    // { theme: "light", debug: false }
console.log(updated); // { theme: "light", debug: true }
JavaScript

設定マージもよくあるパターンです。

const defaultOptions = {
  method: "GET",
  timeout: 3000
};

function request(url, options = {}) {
  const opts = {
    ...defaultOptions, // デフォルト
    ...options         // 呼び出し側の上書き
  };
  console.log(url, opts);
}

request("/api/users", { timeout: 5000 });
// timeout だけ 5000 に変わる
JavaScript

よくある落とし穴と注意点

一つ目は「浅いコピーであること」を忘れて、ネストした中身まで別になっていると勘違いすることです。オブジェクトや配列の中身を変えると元にも影響するので、「変えていいかどうか」を意識してから変更してください。

const state = {
  user: { name: "Alice" }
};

const copy = { ...state };
copy.user.name = "Bob";

console.log(state.user.name); // "Bob"(共有)
JavaScript

二つ目は、「順番で上書きの優先度が変わる」ことを軽く考えてしまうことです。どちらを優先したいかを常に意識し、「デフォルト → 上書き」と並べるのを癖にしましょう。

三つ目は、「削除したいのにスプレッドだけでなんとかしようとしてしまう」ことです。削除したいプロパティがあるなら、分割代入で取り出して捨て、残りを ...rest でまとめてから使う、という二段構えが必要です。

例題で理解を固める

// 1) ユーザー情報をコピーして表示用フラグを追加
const user = { id: 1, name: "Alice" };
const viewUser = { ...user, isOnline: true };

console.log(user);     // { id: 1, name: "Alice" }
console.log(viewUser); // { id: 1, name: "Alice", isOnline: true }

// 2) 設定の一部上書き(lang だけ変更)
const baseCfg = { theme: "dark", lang: "ja", debug: false };
const newCfg  = { ...baseCfg, lang: "en" };

console.log(newCfg);
// { theme: "dark", lang: "en", debug: false }

// 3) 機密情報を除外したレスポンス作成
const fullUser = { id: 1, name: "Alice", password: "secret", token: "xxx" };
const { password, token, ...safeUser } = fullUser;

console.log(safeUser);
// { id: 1, name: "Alice" }

// 4) ネストを含む状態を更新(外側だけ新しくする)
const state = {
  count: 0,
  meta: { updatedBy: "system" }
};

// count だけ増やした新しい state
const next = { ...state, count: state.count + 1 };

console.log(state); // { count: 0, meta: { updatedBy: "system" } }
console.log(next);  // { count: 1, meta: { updatedBy: "system" } }

// 5) デフォルトオプションに部分的な設定をマージ
const defaults = { method: "GET", timeout: 3000, cache: "no-cache" };
function makeOptions(userOptions = {}) {
  return { ...defaults, ...userOptions };
}

console.log(makeOptions({ timeout: 1000 }));
// { method: "GET", timeout: 1000, cache: "no-cache" }
JavaScript

まとめ

オブジェクトコピーにおけるスプレッド構文の核心は、「{ ...obj } で元と同じ中身を持つ“別オブジェクト”を作り、元を壊さずに安全に扱う」ことです。代入は同じものへの別名、スプレッドは浅いコピー。ネストした中身は共有される点に注意しつつ、{ ...old, changed: value } で一部だけ変えた新バージョンを作る、分割代入の ...rest と組み合わせて「除外コピー」を作る、関数内ではコピーしてから変更して返す、というパターンを身につけると、初心者でもバグの少ない ES6+ のオブジェクト操作が書けるようになります。

タイトルとURLをコピーしました