オブジェクトを別のオブジェクトへコピーしたり、複数のオブジェクトを1つにまとめたりする場面は、設定値のマージやデフォルト値の適用などでよく登場します。こうした「コピー・結合」に使えるのが Object.assign() です。一方で、いったん作ったオブジェクトを後から書き換えられないようにしたいときには Object.freeze() が役立ちます。この記事では、両者の基本的な使い方と、どちらも「浅い(shallow)」処理である点、スプレッド構文との違い、strict mode での挙動までをコード例とともに解説します。
目次
Object.assign() でオブジェクトを結合・コピーする
Object.assign() は、1つ以上の「元(ソース)」オブジェクトが持つプロパティを、「対象(ターゲット)」オブジェクトへコピーするメソッドです。書式は Object.assign(target, ...sources) で、戻り値はコピー先である target そのものです。まずは複数のオブジェクトを1つにまとめる例を見てみましょう。
const a = { x: 1, y: 2 };
const b = { y: 3, z: 4 };
// a に b のプロパティをコピーして結合する
const result = Object.assign(a, b);
console.log(result); // => { x: 1, y: 3, z: 4 }
// 戻り値は target(a)そのもの。a 自身も書き換わっている
console.log(result === a); // => true
console.log(a); // => { x: 1, y: 3, z: 4 }
同じ名前のプロパティ(この例では y)があるときは、後ろのソースの値で上書きされます。左から右へ順にコピーしていくため、あとに書いたオブジェクトの値が優先される、と覚えておくとよいでしょう。
ここで注意したいのは、Object.assign() が第1引数の target を直接書き換える点です。上の例では元の a の中身も変わってしまっています。元のオブジェクトを壊したくない場合は、次のように空のオブジェクト {} を target に渡します。
const a = { x: 1, y: 2 };
const b = { y: 3, z: 4 };
// 空オブジェクトを target にすれば、a・b はそのまま残る
const merged = Object.assign({}, a, b);
console.log(merged); // => { x: 1, y: 3, z: 4 }
console.log(a); // => { x: 1, y: 2 }(元のまま)
この Object.assign({}, obj) という書き方は、オブジェクトを複製(浅いコピー)する定番のイディオムとしてよく使われます。
デフォルト値をマージする
後ろのソースが優先される性質は、デフォルト値に対してユーザー指定の値を上書きする用途と相性が良いです。関数のオプション引数などで、既定値を用意しつつ、渡された設定だけを差し替える、という書き方ができます。
function createUser(options) {
const defaults = {
role: 'guest',
active: true,
theme: 'light',
};
// defaults を土台に、options で指定された項目だけ上書き
return Object.assign({}, defaults, options);
}
console.log(createUser({ role: 'admin' }));
// => { role: 'admin', active: true, theme: 'light' }
console.log(createUser({ theme: 'dark', active: false }));
// => { role: 'guest', active: false, theme: 'dark' }
defaults を先に、options を後ろに置くことで、指定された項目だけがデフォルトを上書きします。target に {} を使っているので、defaults オブジェクト自体は書き換わりません。
コピーは「浅い」ことに注意する
Object.assign() のコピーは浅いコピー(shallow copy)です。これは、プロパティの値がオブジェクトや配列のときに、その中身まで複製されず、参照(同じものを指す矢印)だけがコピーされることを意味します。そのため、コピー先でネストしたオブジェクトを書き換えると、コピー元にも影響が及びます。
const original = {
name: 'Taro',
settings: { theme: 'dark' }, // ネストしたオブジェクト
};
const copy = Object.assign({}, original);
// トップレベルのプロパティは独立している
copy.name = 'Jiro';
console.log(original.name); // => 'Taro'(影響なし)
// ネストしたオブジェクトは参照が共有されている
copy.settings.theme = 'light';
console.log(original.settings.theme); // => 'light'(元も変わる!)
name のような文字列のプロパティは独立してコピーされますが、settings のようなオブジェクトは同じ実体を共有します。ネストした部分まで完全に切り離したい場合は、structuredClone() によるディープコピーや、階層ごとに個別にコピーする方法を検討してください。
スプレッド構文との違い
オブジェクトのコピー・結合は、スプレッド構文(...)でも書けます。{ ...a, ...b } は Object.assign({}, a, b) とほぼ同じ結果になり、こちらも浅いコピーです。
const a = { x: 1, y: 2 };
const b = { y: 3, z: 4 };
// スプレッド構文でのマージ(新しいオブジェクトを作る)
const merged = { ...a, ...b };
console.log(merged); // => { x: 1, y: 3, z: 4 }
大きな違いは、スプレッド構文は必ず新しいオブジェクトを作るのに対し、Object.assign() は第1引数の target を直接書き換えられる点です。既存のオブジェクトに後からプロパティを足したいときは Object.assign(existing, extra) が向いています。逆に、常に新しいオブジェクトを組み立てるなら、簡潔に書けるスプレッド構文が読みやすいでしょう。両者は目的に応じて使い分けます。
Object.freeze() でオブジェクトを凍結する
Object.freeze() は、渡したオブジェクトを凍結(freeze)して、それ以上変更できないようにするメソッドです。凍結されたオブジェクトは、プロパティの追加・変更・削除がすべて禁止されます。定数のように扱いたい設定オブジェクトなどを、うっかり書き換えてしまう事故を防げます。
const config = {
apiUrl: 'https://example.com',
timeout: 3000,
};
// オブジェクトを凍結する。戻り値は凍結された同じオブジェクト
Object.freeze(config);
config.timeout = 5000; // 変更しようとする → 無視される
config.retry = 3; // 追加しようとする → 無視される
delete config.apiUrl; // 削除しようとする → 無視される
console.log(config);
// => { apiUrl: 'https://example.com', timeout: 3000 }
上の例では、変更・追加・削除のどれを試みても config の中身は変わっていません。通常のモード(非 strict mode)では、こうした操作はエラーにならず静かに無視されるのが特徴です。エラーが出ないぶん、意図せず「書き換えたつもりが反映されない」ということも起こり得るので、凍結したオブジェクトであることを意識しておきましょう。
Object.isFrozen() で凍結されているか確認する
あるオブジェクトが凍結済みかどうかは、Object.isFrozen() で調べられます。凍結されていれば true、そうでなければ false を返します。
const point = { x: 10, y: 20 };
console.log(Object.isFrozen(point)); // => false
Object.freeze(point);
console.log(Object.isFrozen(point)); // => true
書き換える前に Object.isFrozen() でチェックしておけば、凍結オブジェクトを誤って更新しようとするコードを事前に防げます。
freeze も「浅い」凍結にとどまる
Object.assign() と同じく、Object.freeze() も浅い(shallow)処理です。凍結されるのはトップレベルのプロパティだけで、ネストしたオブジェクトの中身は凍結されません。そのため、入れ子になったオブジェクトのプロパティは、依然として書き換えられてしまいます。
const user = {
name: 'Taro',
profile: { age: 20 }, // ネストしたオブジェクト
};
Object.freeze(user);
user.name = 'Jiro'; // トップレベル → 変更されない
user.profile.age = 30; // ネストの中身 → 変更できてしまう
console.log(user.name); // => 'Taro'
console.log(user.profile.age); // => 30(凍結されていない)
console.log(Object.isFrozen(user)); // => true
console.log(Object.isFrozen(user.profile)); // => false
ネストした部分まで完全に凍結したい場合は、各階層のオブジェクトに対して再帰的に Object.freeze() を呼び出す「ディープフリーズ」を自分で実装する必要があります。次のように、プロパティを走査してオブジェクトなら再帰的に凍結します。
function deepFreeze(obj) {
// 各プロパティを調べ、オブジェクトなら先に凍結する
Object.values(obj).forEach((value) => {
if (value !== null && typeof value === 'object') {
deepFreeze(value);
}
});
return Object.freeze(obj);
}
const user = { name: 'Taro', profile: { age: 20 } };
deepFreeze(user);
user.profile.age = 30; // 無視される
console.log(user.profile.age); // => 20
console.log(Object.isFrozen(user.profile)); // => true
strict mode では変更がエラーになる
凍結されたオブジェクトへの書き換えは、通常のモードでは静かに無視されますが、strict mode(厳格モード)では TypeError が発生します。ES モジュールや関数内で 'use strict' を宣言している場合が該当します。
'use strict';
const config = { timeout: 3000 };
Object.freeze(config);
// strict mode では、凍結オブジェクトへの代入は例外になる
config.timeout = 5000;
// => TypeError: Cannot assign to read only property 'timeout' of object
エラーになるのは一見不便に思えますが、むしろ「凍結したはずのオブジェクトを書き換えようとしている」というミスにすぐ気づけるという利点があります。なお、モジュール(<script type="module"> や .mjs)の中のコードは自動的に strict mode で動くため、明示しなくてもこの挙動になります。
まとめ
Object.assign(target, ...sources) は、ソースのプロパティを target へコピーして結合するメソッドで、後ろのソースが優先されるためデフォルト値のマージに便利です。Object.assign({}, obj) の形にすれば元のオブジェクトを壊さずに浅いコピーを作れます。Object.freeze() はオブジェクトを凍結し、プロパティの追加・変更・削除を禁止します。凍結状態は Object.isFrozen() で確認でき、strict mode では変更しようとすると TypeError になります。どちらも浅い処理で、ネストしたオブジェクトまでは対象にならない点だけ意識しておけば、安全なコピーとイミュータブルなデータ管理に役立ちます。