JavaScript Object.assign()の実践的な使い方|実務で必須なオブジェクト操作パターン

JavaScript

JavaScript Object.assign()の実践的な使い方|実務で必須なオブジェクト操作パターン

1. Object.assign()の基本理解

JavaScriptで開発していると、複数のオブジェクトを組み合わせたり、デフォルト値を適用したり、既存のオブジェクトに新しいプロパティを追加したりする場面は頻繁に出てきます。こうした場面で活躍するのがObject.assign()メソッドです。

Object.assign()は、1つ以上のソースオブジェクトから、ターゲットオブジェクトへすべてのプロパティをコピーします。元々のオブジェクトを変更しないオブジェクト操作を実現するために、実務では必須のメソッドになっています。

基本的な構文は以下の通りです。

Object.assign(targetObject, sourceObject1, sourceObject2, ...);

ターゲットオブジェクトが返されます。複数のソースオブジェクトを指定した場合、左から右へ順番にマージされます。

2. 実務で使うユースケース

Object.assign()が活躍するのは、以下のようなシーンです。

  • APIレスポンスにデフォルト値を適用する
  • フォーム入力値と既存データをマージする
  • 設定オブジェクトをユーザー設定で上書きする
  • コンポーネントの状態を更新する際の不変性を保つ
  • 複数のAPIレスポンスデータを統合する

これらはすべて実際のプロジェクトで毎日のように出会う場面です。適切に使うことで、コードの可読性と保守性が大きく向上します。

3. 実装コード例①:API レスポンスへのデフォルト値適用

最も実務的な例から始めましょう。バックエンドのAPIから返されるユーザー情報に、フロントエンド側で定義したデフォルト値を適用する場面です。

// デフォルトのユーザー設定
const defaultUserConfig = {
  theme: 'light',
  notifications: true,
  language: 'ja',
  pageSize: 20,
  autoSave: true
};

// APIから返されたユーザー設定(一部のみ)
const apiResponse = {
  theme: 'dark',
  language: 'en'
};

// Object.assign()でマージ:デフォルト値とAPIレスポンスを統合
const userConfig = Object.assign({}, defaultUserConfig, apiResponse);

console.log(userConfig);
// 出力:
// {
//   theme: 'dark',          // APIで上書きされた
//   notifications: true,    // デフォルト値が使われた
//   language: 'en',         // APIで上書きされた
//   pageSize: 20,           // デフォルト値が使われた
//   autoSave: true          // デフォルト値が使われた
// }

ここで重要なポイントは、第1引数に空のオブジェクト{}を渡している点です。こうすることで元のデフォルト値を変更せず、新しいオブジェクトが返されます。

4. 実装コード例②:フォーム更新時の状態管理

Reactなどのフレームワークを使う場合、ユーザーがフォームの一部を変更した時に、既存の状態と新しい値をマージする必要があります。

// 現在のフォーム状態
const currentFormState = {
  firstName: '田中',
  lastName: '太郎',
  email: 'tanaka@example.com',
  phone: '090-1234-5678',
  zipCode: '100-0001',
  address: '東京都千代田区'
};

// ユーザーが名前を変更した(他はそのまま)
const userChanges = {
  firstName: '山田',
  lastName: '花子'
};

// 更新後の状態
const updatedFormState = Object.assign({}, currentFormState, userChanges);

console.log(updatedFormState);
// 出力:
// {
//   firstName: '山田',
//   lastName: '花子',
//   email: 'tanaka@example.com',
//   phone: '090-1234-5678',
//   zipCode: '100-0001',
//   address: '東京都千代田区'
// }

フロントエンドの状態管理では、オブジェクトを直接変更しないイミュータブルなアプローチが重要です。Object.assign()を使うことで、古い状態を保ちながら新しい状態を作成できます。

5. 実装コード例③:複数データソースの統合

複数のAPIから取得したデータを1つのオブジェクトに統合する場面も実務では多くあります。

// ユーザーマスタAPI
const userMaster = {
  id: 101,
  name: '佐藤次郎',
  email: 'sato@example.com',
  department: '営業部'
};

// ユーザー権限API
const userPermissions = {
  canEdit: true,
  canDelete: false,
  canApprove: true,
  accessLevel: 5
};

// ユーザー購買履歴API
const userPurchaseHistory = {
  totalPurchases: 15,
  totalAmount: 250000,
  lastPurchaseDate: '2024-01-15',
  loyaltyPoints: 2500
};

// すべてのデータを統合
const completeUserData = Object.assign(
  {},
  userMaster,
  userPermissions,
  userPurchaseHistory
);

console.log(completeUserData);
// 出力:
// {
//   id: 101,
//   name: '佐藤次郎',
//   email: 'sato@example.com',
//   department: '営業部',
//   canEdit: true,
//   canDelete: false,
//   canApprove: true,
//   accessLevel: 5,
//   totalPurchases: 15,
//   totalAmount: 250000,
//   lastPurchaseDate: '2024-01-15',
//   loyaltyPoints: 2500
// }

複数のAPIから返されたデータを統合する際、Object.assign()は非常に便利です。3つ以上のソースオブジェクトを同時に処理できる点も実務的です。

6. TypeScriptでの実装パターン

TypeScriptを使う場合、型安全性を保ちながらObject.assign()を使うことが大切です。

interface UserConfig {
  theme: 'light' | 'dark';
  notifications: boolean;
  language: string;
  pageSize: number;
  autoSave: boolean;
}

interface APIUserConfig extends Partial<UserConfig> {
  // APIは一部のプロパティのみ返す可能性がある
}

const defaultUserConfig: UserConfig = {
  theme: 'light',
  notifications: true,
  language: 'ja',
  pageSize: 20,
  autoSave: true
};

const apiResponse: APIUserConfig = {
  theme: 'dark',
  language: 'en'
};

// 型安全なマージ
const userConfig: UserConfig = Object.assign(
  {},
  defaultUserConfig,
  apiResponse
);

// このコードは型チェックを通る
const theme: 'light' | 'dark' = userConfig.theme; // OK

// 以下はコンパイルエラー
// const invalidValue: number = userConfig.theme;

TypeScriptを使うことで、Object.assign()の処理結果が意図した型になっているかコンパイル時に検証できます。特にAPIレスポンスを扱う際は重要です。

7. よくある応用パターン①:条件付きマージ

実務では、特定の条件下でのみプロパティをマージしたい場合があります。

// ユーザーの管理者フラグを確認して、管理者権限をマージするかどうかを決定
const baseUserData = {
  id: 101,
  name: '田中太郎',
  email: 'tanaka@example.com'
};

const adminPermissions = {
  canManageUsers: true,
  canDeleteContent: true,
  canViewAnalytics: true,
  canModifySettings: true
};

const isAdmin = true; // バックエンドから取得した値

// 条件付きでマージ
const userData = Object.assign(
  {},
  baseUserData,
  isAdmin ? adminPermissions : {}
);

console.log(userData);
// 出力(isAdminがtrueの場合):
// {
//   id: 101,
//   name: '田中太郎',
//   email: 'tanaka@example.com',
//   canManageUsers: true,
//   canDeleteContent: true,
//   canViewAnalytics: true,
//   canModifySettings: true
// }

三項演算子を使って、マージするオブジェクトを動的に決定できます。

8. よくある応用パターン②:レイヤード設定(優先度付き)

グローバル設定、ユーザー設定、セッション設定など、複数のレイヤーの設定が存在する場合があります。

// グローバルデフォルト設定
const globalDefaults = {
  apiTimeout: 30000,
  retryAttempts: 3,
  cacheDuration: 3600,
  logLevel: 'info',
  debugMode: false
};

// ユーザーのカスタム設定(ローカルストレージから取得)
const userCustomSettings = {
  apiTimeout: 15000,
  logLevel: 'debug'
};

// 現在のセッション設定(ユーザーが今実行中に変更した値)
const sessionOverrides = {
  debugMode: true
};

// 優先度順にマージ:グローバル < ユーザー < セッション
const finalConfig = Object.assign(
  {},
  globalDefaults,
  userCustomSettings,
  sessionOverrides
);

console.log(finalConfig);
// 出力:
// {
//   apiTimeout: 15000,      // ユーザー設定で上書き
//   retryAttempts: 3,       // グローバルデフォルト
//   cacheDuration: 3600,    // グローバルデフォルト
//   logLevel: 'debug',      // ユーザー設定で上書き
//   debugMode: true         // セッション設定で上書き
// }

複数の設定レイヤーがある場合、Object.assign()の呼び出し順序が優先度を決めます。右側のオブジェクトほど優先度が高くなります。

9. よくある応用パターン③:オブジェクトのシャロー・コピー

元のオブジェクトを変更しないようにコピーを作成する必要があります。

const original = {
  id: 1,
  name: '山田太郎',
  tags: ['javascript', 'react'],
  meta: {
    createdAt: '2024-01-01',
    updatedAt: '2024-01-15'
  }
};

// Object.assign()でシャロー・コピーを作成
const copy = Object.assign({}, original);

// トップレベルのプロパティは独立している
copy.name = '田中次郎';
console.log(original.name); // '山田太郎' - 元のオブジェクトは変更されない

// ただし、ネストされたオブジェクト・配列は参照が共有される
copy.tags.push('nodejs');
console.log(original.tags); // ['javascript', 'react', 'nodejs'] - 元の配列も変更される

copy.meta.updatedAt = '2024-02-01';
console.log(original.meta.updatedAt); // '2024-02-01' - 元のオブジェクトも変更される

Object.assign()はシャロー・コピー(浅いコピー)を実行します。ネストされたオブジェクトや配列については、参照がコピーされるだけです。ネストされたデータも独立したコピーが必要な場合は、ディープ・コピーを検討してください。

10. よくある応用パターン④:スプレッド演算子との使い分け

JavaScriptではObject.assign()の代わりに、スプレッド演算子{...}を使うこともできます。

const defaultConfig = {
  timeout: 30000,
  retries: 3,
  cache: true
};

const userConfig = {
  timeout: 15000
};

// Object.assign()を使う方法
const config1 = Object.assign({}, defaultConfig, userConfig);

// スプレッド演算子を使う方法
const config2 = { ...defaultConfig, ...userConfig };

console.log(config1); // { timeout: 15000, retries: 3, cache: true }
console.log(config2); // { timeout: 15000, retries: 3, cache: true }

// 動作は同じですが、スプレッド演算子の方が読みやすいと考える人も多くいます

モダンなJavaScriptでは、スプレッド演算子の方がより読みやすく、推奨される傾向にあります。ただし、Object.assign()は古いブラウザ環境でのサポートが多い利点があります。プロジェクトの要件に応じて使い分けてください。

11. TypeScriptでのディープマージ実装

実務ではディープ・マージが必要な場面もあります。Object.assign()ではできない場合、カスタム関数を実装します。

interface ConfigObject {
  [key: string]: any;
}

// ディープマージ関数
function deepMerge(target: ConfigObject, source: ConfigObject): ConfigObject {
  const output = Object.assign({}, target);
  
  if (isObject(target) && isObject(source)) {
    Object.keys(source).forEach(key => {
      if (isObject(source[key])) {
        if (!(key in target)) {
          Object.assign(output, { [key]: source[key] });
        } else {
          output[key] = deepMerge(target[key], source[key]);
        }
      } else {
        Object.assign(output, { [key]: source[key] });
      }
    });
  }
  
  return output;
}

function isObject(item: any): boolean {
  return item && typeof item === 'object' && !Array.isArray(item);
}

// 使用例
const apiConfig = {
  database: {
    host: 'localhost',
    port: 5432,
    credentials: {
      username: 'admin',
      password: 'old_password'
    }
  },
  cache: {
    enabled: true,
    ttl: 3600
  }
};

const envConfig = {
  database: {
    host: 'prod.example.com',
    credentials: {
      password: 'new_secure_password'
    }
  },
  cache: {
    ttl: 7200
  }
};

const finalConfig = deepMerge(apiConfig, envConfig);

console.log(finalConfig);
// 出力:
// {
//   database: {
//     host: 'prod.example.com',        // 上書き
//     port: 5432,                      // 保持
//     credentials: {
//       username: 'admin',             // 保持
//       password: 'new_secure_password' // 上書き
//     }
//   },
//   cache: {
//     enabled: true,                   // 保持
//     ttl: 7200                        // 上書き
//   }
// }

ディープ・マージが必要な場合は、再帰的に処理するカスタム関数を用意しましょう。ただし、実務ではLodashなどのユーティリティライブラリを使うことが多いです。

12. 注意点①:シャロー・コピーの限界

前述した通り、Object.assign()はシャロー・コピーです。ネストされたオブジェクトについては気をつけましょう。

// 問題のあるコード
const original = {
  user: {
    name: '田中太郎',
    permissions: ['read', 'write']
  }
};

const copy = Object.assign({}, original);

// これはバグになる
copy.user.permissions.push('delete');
console.log(original.user.permissions); 
// ['read', 'write', 'delete'] - 元のオブジェクトも変更されている

13. 注意点②:Symbol プロパティ

Object.assign()はSymbolプロパティもコピーしますが、enumerable属性がfalseのプロパティはコピーされません。

const source = {
  publicProp: 'visible',
  [Symbol.for('privateProp')]: 'hidden'
};

// Symbolプロパティもコピーされる
const copy = Object.assign({}, source);
console.log(copy[Symbol.for('privateProp')]); // 'hidden'

// ただし、enumerable: falseのプロパティはコピーされない
const target = {};
Object.defineProperty(target, 'hiddenProp', {
  value: 'secret',
  enumerable: false
});

const merged = Object.assign({}, target);
console.log(merged.hiddenProp); // undefined

14. 注意点③:ゲッター・セッター

Object.assign()はゲッター・セッターをゲッター・セッターとしてコピーしません。値を評価してコピーします。

const source = {
  _value: 10,
  get value() {
    console.log('ゲッターが呼ばれました');
    return this._value;
  },
  set value(v) {
    console.log('セッターが呼ばれました');
    this._value = v;
  }
};

// ゲッターが実行されて、値がコピーされる
const copy = Object.assign({}, source);
// コンソール: 'ゲッターが呼ばれました'

console.log(copy.value); // 10(通常のプロパティになっている)

15. 実務的な注意点④:パフォーマンス

非常に大きなオブジェクトや大量のオブジェクトをマージする場合、パフォーマンスに注意が必要です。

// パフォーマンステスト
const largObject = {};
for (let i = 0; i < 10000; i++) {
  largeObject[`prop${i}`] = Math.random();
}

console.time('Object.assign');
for (let i = 0; i < 1000; i++) {
  Object.assign({}, largeObject);
}
console.timeEnd('Object.assign');

// 出力例: Object.assign: 234.56ms

// 代替案:スプレッド演算子
console.time('spread');
for (let i = 0; i < 1000; i++) {
  {...largeObject};
}
console.timeEnd('spread');

// 出力例: spread: 245.23ms
// 差はほぼないが、非常に大きなオブジェクトの場合は異なる結果になる可能性がある

通常のユースケースではパフォーマンスの違いは無視できますが、ループ内で大量のマージを行う場合は測定することをお勧めします。

16. 実務でのベストプラクティス

Object.assign()を効果的に使うためのベストプラクティスをまとめます。

// ✅ 良い例:デフォルト値を適用する
const getUserConfig = (apiResponse) => {
  const defaults = {
    theme: 'light',
    notifications: true,
    language: 'ja'
  };
  return Object.assign({}, defaults, apiResponse);
};

// ✅ 良い例:型安全なTypeScript実装
interface Config {
  [key: string]: string | number | boolean;
}

const mergeConfigs = (base: Config, override: Partial): Config => {
  return Object.assign({}, base, override);
};

// ❌ 避けるべき:元のオブジェクトを直接変更
const badPractice = (target, source) => {
  Object.assign(target, source); // targetが直接変更される
};

// ✅ 良い例:新しいオブジェクトを返す
const goodPractice = (target, source) => {
  return Object.assign({}, target, source); // 新しいオブジェクトが返される
};

// ✅ 良い例:複数のマージが必要な場合は段階的に処理
const complexMerge = (base, layer1, layer2, layer3) => {
  return Object.assign({}, base, layer1, layer2, layer3);
};

// ❌ 避けるべき:ネストされたオブジェクトの独立性を仮定する
const problematicCode = (original) => {
  const copy = Object.assign({}, original);
  copy.nested.value = 'changed';
  // original.nested.valueも変更されている
};

// ✅ 良い例:ネストされたオブジェクトはディープ・コピーを検討
const safeNestedCopy = (original) => {
  return JSON.parse(JSON.stringify(original)); // JSON安全な値の場合
};

17. まとめ

Object.assign()は、JavaScriptの実務開発において必須のメソッドです。デフォルト値の適用、複数のデータソースの統合、状態管理での不変性の確保など、様々な場面で活躍します。

重要なポイントをおさらいします:

  • 基本原則:新しいオブジェクトを作成する場合、第1引数に空のオブジェクト{}を渡す
  • 優先度:右側のオブジェクトほど優先度が高く、左側のプロパティを上書きする
  • シャロー・コピー:ネストされたオブジェクトは参照がコピーされるだけ
  • モダン開発:スプレッド演算子{...}も同じ目的で使える
  • TypeScript:型安全性を確保しながら使うことが重要

Object.assign()とスプレッド演算子を適切に使い分け、可読性と保守性の高いコードを書くことが、実務での成功につながります。

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