【JavaScript】オブジェクトを複製する!スプレッド構文とObject.assignの使い分け

目次

概要

JavaScriptのオブジェクトは「値渡し」で扱われるため、単に const b = a; と代入しただけではコピーにならず、b を変更すると a も変わってしまいます。

別の独立したデータとして扱いたい場合は、明示的に「複製(クローン)」を作成する必要があります。現代のJavaScriptでは、スプレッド構文 {...obj} を使うのが主流です。

今回は、RPGのキャラクター作成をテーマに、テンプレート(ひな形)から新しいキャラクター設定を複製・カスタマイズする処理を実装します。

仕様(構文)

オブジェクトのコピー方法

手法構文特徴
スプレッド構文const copy = { ...original };推奨。ES2018で登場。記述が簡潔で読みやすい。
Object.assignconst copy = Object.assign({}, original);従来の方法。複数のオブジェクトをマージする場合にも使われる。
  • 注意点(重要): どちらも シャローコピー(浅いコピー) です。
    • オブジェクトの直下のプロパティは複製されますが、ネスト(入れ子)されている配列やオブジェクトは「参照」のまま共有されます。

基本の使い方

1. スプレッド構文 {...}

オブジェクトを展開して、新しい波括弧 {} の中に入れます。

const baseStats = { hp: 100, mp: 50 };

// 複製と同時にプロパティの上書き・追加も可能
const heroStats = { ...baseStats, mp: 200, attack: 15 };

console.log(heroStats);
// { hp: 100, mp: 200, attack: 15 }

2. Object.assign()

空のオブジェクト {} に対して、元のオブジェクトの中身をマージします。

const baseStats = { hp: 100, mp: 50 };

const enemyStats = Object.assign({}, baseStats);

コード全文(HTML / JavaScript)

ゲームのキャラクターメイク画面です。

「基本の戦士セット(テンプレート)」を複製し、新しいキャラクター「勇者」と「魔法剣士」を作成します。また、シャローコピーの注意点(装備品の配列操作)についても確認します。

HTML

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Character Maker</title>
    <style>
        .maker-panel {
            font-family: 'Courier New', monospace;
            max-width: 600px;
            background-color: #2c3e50;
            color: #ecf0f1;
            padding: 20px;
            border-radius: 8px;
        }
        h3 { margin-top: 0; color: #f1c40f; border-bottom: 2px solid #7f8c8d; padding-bottom: 10px; }
        
        .char-box {
            background-color: #34495e;
            margin-bottom: 15px;
            padding: 15px;
            border-radius: 4px;
            border-left: 5px solid #bdc3c7;
        }
        .char-title {
            font-weight: bold;
            color: #e67e22;
            margin-bottom: 5px;
            display: block;
        }
        .stats {
            font-size: 0.9rem;
            line-height: 1.6;
        }
        .equipment {
            color: #1abc9c;
        }
        
        button {
            background-color: #e74c3c;
            color: white;
            border: none;
            padding: 10px 20px;
            border-radius: 4px;
            cursor: pointer;
            font-size: 1rem;
            margin-top: 10px;
        }
        button:hover { background-color: #c0392b; }

        .note {
            font-size: 0.8rem;
            color: #95a5a6;
            margin-top: 10px;
        }
    </style>
</head>
<body>

<div class="maker-panel">
    <h3>Character Setting Clone</h3>
    
    <div id="char-list">
        </div>

    <button id="btn-clone">キャラクターを複製して作成</button>
    <div class="note">※コンソールでシャローコピーの挙動も確認してください</div>
</div>

<script src="char_clone.js"></script>
</body>
</html>

JavaScript

/**
 * キャラクター作成スクリプト
 * オブジェクトの複製とシャローコピーの検証
 */

// 1. テンプレートとなるオブジェクト(コピー元)
const templateChar = {
    job: 'Warrior',
    level: 1,
    stats: {
        str: 10,
        int: 5
    },
    // 配列(参照型)を含む
    equipment: ['Copper Sword', 'Leather Armor']
};

const listArea = document.getElementById('char-list');
const btnClone = document.getElementById('btn-clone');

/**
 * キャラ情報をHTMLとして表示する関数
 */
const renderChar = (name, charObj) => {
    const div = document.createElement('div');
    div.className = 'char-box';
    div.innerHTML = `
        <span class="char-title">[${name}]</span>
        <div class="stats">
            Job: ${charObj.job} / Lv: ${charObj.level}<br>
            Stats: STR=${charObj.stats.str}, INT=${charObj.stats.int}<br>
            Equip: <span class="equipment">${charObj.equipment.join(', ')}</span>
        </div>
    `;
    listArea.appendChild(div);
};

/**
 * 複製処理の実行
 */
const createCharacters = () => {
    listArea.innerHTML = ''; // クリア

    // 1. スプレッド構文 {...} で複製(推奨)
    // 勇者を作成: Jobを書き換え、Lvを変更
    const hero = {
        ...templateChar,
        job: 'Hero',
        level: 50
    };

    // 2. Object.assign({}, ...) で複製
    // 魔法剣士を作成
    const magicKnight = Object.assign({}, templateChar);
    magicKnight.job = 'Magic Knight';
    magicKnight.stats = { str: 15, int: 15 }; // statsオブジェクトごと上書き(安全)

    // ★ シャローコピーの注意点の実証
    // heroの装備を変更してみる
    // pushなどの破壊的メソッドを使うと、元のtemplateCharの配列も変わってしまう(参照が同じだから)
    hero.equipment.push('Hero Shield'); 

    // 表示
    renderChar('Template (Original)', templateChar);
    renderChar('Hero (Spread Clone)', hero);
    renderChar('Magic Knight (Assign Clone)', magicKnight);
    
    console.log('--- Shallow Copy Check ---');
    console.log('Template Equip:', templateChar.equipment); // 盾が増えてしまっている!
    console.log('Hero Equip:', hero.equipment);
};

// イベントリスナー
btnClone.addEventListener('click', createCharacters);

カスタムポイント

  • ディープコピー(深いコピー):ネストされた配列やオブジェクトも含めて完全に独立したコピーを作りたい場合は、structuredClone() を使用します。JavaScriptconst deepCopy = structuredClone(originalObject); これを使えば、equipment 配列を変更してもコピー元には影響しません。

注意点

  1. 構文の違い:
    • 配列のコピー: [...array]
    • オブジェクトのコピー: {...object}括弧の種類を間違えないようにしましょう。
  2. シャローコピーのリスク:上記のコード例にある通り、コピーしたオブジェクト内の「配列」や「子オブジェクト」を変更(push 等)すると、コピー元まで書き換わってしまいます。これを防ぐには、ネストされた部分も個別にコピーするか、structuredClone を使う必要があります。

まとめ

  • 基本: { ...obj } でコピーを作成し、元のデータを保持したまま新しいデータを作る。
  • 注意: ネストされたデータは「参照」がコピーされるだけ(シャローコピー)なので、変更時の副作用に注意する。
  • 解決策: 完全に切り離したい場合は structuredClone(obj) を使う。
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

私が勉強したこと、実践したこと、してることを書いているブログです。
主に資産運用について書いていたのですが、
最近はプログラミングに興味があるので、今はそればっかりです。

目次