- JavaScript
JavaScriptオブジェクトのコピー|structuredClone入門
JavaScriptでオブジェクトをコピーする方法は、1階層目だけを複製する「浅いコピー」(スプレッド構文・Object.assign)と、ネストした中身まで複製する「深いコピー」(structuredClone など)の2種類です。ネストしたデータをまるごと複製したいときは、標準関数の structuredClone(obj) が第一候補になります。
この記事では、スプレッド構文・Object.assign・JSON 往復・structuredClone・lodash の cloneDeep の5方式を、Date や Map、関数、循環参照、クラスのインスタンス、DOM 要素といった型ごとに比べた対応表を用意しました。浅いコピーでネストした値が一緒に書き換わる様子、structuredClone が DataCloneError で止まる例とその回避策、速度の計測値まで、Node.js 22.22.2(DOM 要素は Chromium 141)で実際に動かした結果つきで解説します。lodash は 4.18.1 を使いました。
オブジェクトコピーの早見表
5方式 × 型別の対応表
○ は「独立した複製ができる」、△ は「コピー先と元で同じ実体を共有する」、× は「形が変わる・消える・エラーになる」です。プロパティの値として各型を持つオブジェクト({ v: 値 })をコピーした結果です。
| 値の型 | スプレッド {...obj} |
Object.assign |
JSON 往復 | structuredClone |
lodash cloneDeep |
|---|---|---|---|---|---|
| ネストしたオブジェクト・配列 | △ 共有 | △ 共有 | ○ | ○ | ○ |
| Date | △ 共有 | △ 共有 | × 文字列になる | ○ | ○ |
| Map | △ 共有 | △ 共有 | × 空の {} になる |
○ | ○ |
| Set | △ 共有 | △ 共有 | × 空の {} になる |
○ | ○ |
| undefined | ○ | ○ | × キーごと消える | ○ | ○ |
| 関数 | ○(同じ関数を指す) | ○(同じ関数を指す) | × キーごと消える | × DataCloneError | ○(同じ関数を指す) |
| 循環参照 | △ 元を指したまま | △ 元を指したまま | × TypeError | ○ 循環を保って複製 | ○ 循環を保って複製 |
| クラスのインスタンス | △ 共有 | △ 共有 | × ただのオブジェクトになる | × ただのオブジェクトになる | ○ クラスとメソッドを保つ |
| DOM 要素 | △ 共有 | △ 共有 | × 空の {} になる |
× DataCloneError | △ 共有(複製されない) |
目的別の選び方
| やりたいこと | 書き方 |
|---|---|
| 1階層だけのオブジェクトをコピー | { ...obj } |
| 配列をコピー(要素は数値・文字列) | [...arr] |
| ネストしたデータをまるごとコピー | structuredClone(obj) |
| 一部だけ変えた新しいオブジェクトを作る | { ...obj, size: { ...obj.size, w: 200 } } |
| 関数を含むオブジェクトを深くコピー | 関数を分けてから structuredClone、または lodash の cloneDeep |
| クラスのインスタンスをコピー | クラスに clone() メソッドを用意する |
| DOM 要素を複製 | el.cloneNode(true) |
| 配列を並べ替えた新しい配列を得る | arr.toSorted() |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「JavaScriptオブジェクトのコピー|structuredClone入門」
代入・浅いコピー・深いコピーの違い
代入はコピーではない
const a = { n: 1 };
const b = a;
b.n = 2;
console.log('代入:', a, a === b);
代入: { n: 2 } true
const b = a は、同じオブジェクトにもう1つ名前を付けただけです。 b を書き換えると a も変わります。別のオブジェクトにしたいときにコピーが必要になります。
浅いコピーでネストした値が一緒に書き換わる
スプレッド構文でコピーしたあと、コピー側だけを書き換えてみます。
// shallow.mjs (Node.js 22.22.2 で確認)
const original = {
name: 'ノート',
tags: ['文具', '新着'],
size: { w: 148, h: 210 },
};
const copy = { ...original }; // 浅いコピー
copy.name = '手帳'; // 1 階層目:元は変わらない
copy.tags.push('セール'); // 2 階層目:元も変わる
copy.size.w = 105; // 2 階層目:元も変わる
console.log('original:', original);
console.log('copy :', copy);
console.log(original.tags === copy.tags, original.size === copy.size);
original: { name: 'ノート', tags: [ '文具', '新着', 'セール' ], size: { w: 105, h: 210 } }
copy : { name: '手帳', tags: [ '文具', '新着', 'セール' ], size: { w: 105, h: 210 } }
true true
name は元のままですが、tags と size は元のオブジェクトまで書き換わっています。 浅いコピーは1階層目のプロパティを新しいオブジェクトに移すだけで、tags や size の中身は元と同じ実体を指しているからです(最後の行の true true がその証拠です)。
flowchart LR
subgraph 浅いコピー
O1[original] --> T1[tags 配列]
C1[copy] --> T1
O1 --> S1[size オブジェクト]
C1 --> S1
end
subgraph 深いコピー
O2[original] --> T2[tags 配列]
C2[deep] --> T3[tags 配列の複製]
O2 --> S2[size オブジェクト]
C2 --> S3[size の複製]
end
structuredClone なら元は変わらない
const deep = structuredClone(original);
deep.tags.push('限定');
deep.size.h = 999;
console.log('original:', original);
console.log(original.tags === deep.tags);
original: { name: 'ノート', tags: [ '文具', '新着', 'セール' ], size: { w: 105, h: 210 } }
false
deep に「限定」を追加し h を 999 にしても、original は直前の状態のままです。structuredClone はネストした配列やオブジェクトも別の実体として複製します。
配列のコピーも同じ考え方
オブジェクトの配列で、よく使う書き方を比べました。左が「配列そのものが元と同じか」、右が「先頭の要素が元と同じか」です。
const arr = [{ id: 1 }, { id: 2 }];
| 書き方 | 配列が同じ実体か | 要素が同じ実体か | 種類 |
|---|---|---|---|
[...arr] |
false | true | 浅い |
arr.slice() |
false | true | 浅い |
Array.from(arr) |
false | true | 浅い |
arr.map(x => x) |
false | true | 浅い |
arr.map(x => ({ ...x })) |
false | false | 要素を1階層ぶん複製 |
structuredClone(arr) |
false | false | 深い |
[...arr] や slice() は配列の入れ物だけを新しくします。 要素がオブジェクトなら、コピー先の要素を書き換えると元の要素も変わります。要素が数値や文字列だけの配列なら、浅いコピーで十分です。
浅いコピーの書き方(スプレッド構文と Object.assign)
const copy1 = { ...original };
const copy2 = Object.assign({}, original);
どちらも1階層目のプロパティを新しいオブジェクトに写します。Object.assign({}, original).tags === original.tags は true でした(中身は共有されます)。
| 観点 | スプレッド { ...obj } |
Object.assign(target, obj) |
|---|---|---|
| 新しいオブジェクトを作る | 常に作る | 第1引数に {} を渡せば作る |
| 既存のオブジェクトに書き足す | できない | Object.assign(existing, obj) でできる |
| プロパティの上書き・追加と同時に | { ...obj, name: '手帳' } |
Object.assign({}, obj, { name: '手帳' }) |
| コピーされる範囲 | 1階層目(列挙できる自前のプロパティ) | 同じ |
ネストした一部だけを変える(階層ごとにスプレッド)
状態を書き換えずに更新したいときは、変える階層だけをスプレッドで作り直します。
const next = { ...original, size: { ...original.size, w: 200 } };
console.log(next.size.w, original.size.w, next.tags === original.tags);
200 105 true
変えた size は別の実体になり、変えていない tags は共有されたままです。全体を深くコピーするより作るものが少なく、「どこを変えたか」も読み取りやすい書き方です。階層が深くなって書くのが大変になったら、structuredClone でまるごと複製してから書き換える方法に切り替えます。
getter は値に置き換わる
const src = { get now() { return 'G'; } };
console.log(Object.getOwnPropertyDescriptor({ ...src }, 'now').value);
console.log(typeof Object.getOwnPropertyDescriptor(structuredClone(src), 'now').get);
G
undefined
スプレッドでも structuredClone でも、getter は「その時点の値」として写され、getter そのものは引き継がれません。
深いコピーの3つの方法を比べる
型別の実行結果
冒頭の対応表のもとになった出力です。{ v: 値 } を各方式でコピーし、コピー後の v を表示しています。
// matrix.mjs(抜粋) npm i lodash で lodash 4.18.1 を入れて実行
import _ from 'lodash';
class Point {
constructor(x, y) { this.x = x; this.y = y; }
norm() { return Math.hypot(this.x, this.y); }
}
const methods = {
spread: v => ({ ...v }),
assign: v => Object.assign({}, v),
json: v => JSON.parse(JSON.stringify(v)),
structuredClone: v => structuredClone(v),
cloneDeep: v => _.cloneDeep(v),
};
Date spread Date(2026-10-03T00:00:00.000Z) [同じ参照]
Date json string:"2026-10-03T00:00:00.000Z"
Date structuredClone Date(2026-10-03T00:00:00.000Z)
Date cloneDeep Date(2026-10-03T00:00:00.000Z)
Map spread Map(1) [同じ参照] 中身同じ参照
Map json object:{}
Map structuredClone Map(1) 中身も別
Map cloneDeep Map(1) 中身も別
Set json object:{}
Set structuredClone Set(2)
undefined json (キーごと消える)
undefined structuredClone undefined
function spread function
function json (キーごと消える)
function structuredClone DataCloneError: () => 1 could not be cloned.
function cloneDeep function
class spread Point [同じ参照] norm=function
class json object:{"x":3,"y":4} norm=undefined
class structuredClone object:{"x":3,"y":4} norm=undefined
class cloneDeep Point norm=function
RegExp json object:{}
RegExp structuredClone /a+/gi
BigInt json TypeError: Do not know how to serialize a BigInt
BigInt structuredClone bigint:"10n"
Symbol値 json (キーごと消える)
Symbol値 structuredClone DataCloneError: Symbol(s) could not be cloned.
Error json object:{}
Error structuredClone RangeError:範囲外
Error cloneDeep RangeError:範囲外 [同じ参照]
(assign の行は spread と同じ結果だったため省いています。表示の「Date(…)」「Map(1)」などは、型がわかるように整形したものです。)
循環参照(自分自身を指すプロパティを持つオブジェクト)の結果です。
const o = { name: 'a' };
o.self = o;
spread 元を指したまま(浅い)
assign 元を指したまま(浅い)
json TypeError: Converting circular structure to JSON
structuredClone 循環を保ったまま複製
cloneDeep 循環を保ったまま複製
対応表に載せなかった型を補足します。
| 値の型 | JSON 往復 | structuredClone |
lodash cloneDeep |
|---|---|---|---|
| RegExp | × 空の {} |
○ | ○ |
| BigInt | × TypeError | ○ | ○ |
| Symbol(値として) | × キーごと消える | × DataCloneError | ○(同じ Symbol) |
| Error | × 空の {} |
○ 種類とメッセージを保つ | △ 元と同じ実体を共有 |
| NaN / Infinity | × null になる | ○ | ○ |
| Symbol をキーにしたプロパティ | × 消える | × 消える | ○ 残る |
JSON 往復(JSON.parse と JSON.stringify)
const copy = JSON.parse(JSON.stringify(obj));
一度文字列にしてから読み戻す方法です。JSON で表せる値(文字列・数値・真偽値・null・配列・単純なオブジェクト)だけのデータなら、きれいに深いコピーができます。 表せない値は次のように変わります。
console.log(JSON.stringify({ d: new Date(0) }), typeof JSON.parse(JSON.stringify({ d: new Date(0) })).d);
console.log(JSON.stringify([undefined, () => 1, NaN]));
{"d":"1970-01-01T00:00:00.000Z"} string
[null,null,null]
| 元の値 | JSON 往復後 |
|---|---|
| Date | ISO 形式の文字列(Date には戻らない) |
| undefined・関数(オブジェクトの値) | キーごと消える |
| undefined・関数・NaN(配列の要素) | null |
| Map・Set・RegExp・Error | 空の {} |
| BigInt | TypeError で止まる |
| 循環参照 | TypeError で止まる |
API のレスポンスなど、もともと JSON だったデータの複製には今でも使いやすい方法です。
structuredClone
const copy = structuredClone(obj);
追加のインストールなしで使える標準の関数です。 Date・Map・Set・RegExp・BigInt・Error・循環参照をそのまま複製できます。同じオブジェクトを2か所から参照している場合、その関係も保たれます。
const shared = { n: 1 };
const pair = { a: shared, b: shared };
console.log((x => x.a === x.b)(structuredClone(pair))); // true
console.log((x => x.a === x.b)(JSON.parse(JSON.stringify(pair)))); // false
structuredClone では複製後も a と b が同じ実体を指し、JSON 往復では別々の実体になりました。
複製できないものは、関数、Symbol、DOM 要素、WeakMap、Promise、Proxy などです(次の章で実際のエラーを示します)。また、クラスのインスタンスはデータだけのオブジェクトになり、メソッドは引き継がれません。凍結(Object.freeze)の状態も引き継がれず、Object.isFrozen(structuredClone(frozen)) は false でした。
lodash の cloneDeep
import _ from 'lodash';
const copy = _.cloneDeep(obj);
関数を含んでいても止まらず、クラスのインスタンスもクラスを保ったまま複製できるのが特長です。
--- トップがクラス(new Point(3, 4) を各方式でコピー)
spread false undefined {"x":3,"y":4}
assign false undefined {"x":3,"y":4}
json false undefined {"x":3,"y":4}
structuredClone false undefined {"x":3,"y":4}
cloneDeep true function {"x":3,"y":4}
左から「instanceof Point か」「norm メソッドの型」「中身」です。cloneDeep だけが Point のままで、norm も呼べます。
関数は複製されるのではなく、同じ関数がそのまま入ります。Error と DOM 要素は元と同じ実体が入る(複製されない)点は覚えておきます。パッケージの追加が必要なので、すでに lodash を使っている場合や、関数・クラスを含むデータを深くコピーしたい場合に向いています。
どれを選ぶかの判断表
| データの特徴 | おすすめ | 理由 |
|---|---|---|
| 1階層だけ、または中身を書き換えない | スプレッド構文 | 最も軽い |
| ネストあり、値は JSON で表せるものだけ | structuredClone |
標準で書けて意図が明確 |
| Date・Map・Set・循環参照を含む | structuredClone |
型を保ったまま複製できる |
| 関数を含む | 関数を分けて structuredClone、または cloneDeep |
structuredClone は関数で止まる |
| クラスのインスタンスを含む | クラスに clone() を用意、または cloneDeep |
メソッドを保つため |
| 一部だけ変えたい | 階層ごとのスプレッド | 変えない部分は共有できる |
DataCloneError を再現して回避する
エラーになる例
関数をプロパティに持つオブジェクトを structuredClone に渡します。
// err.mjs
const task = { title: '棚卸し', done: false, onDone: () => console.log('完了') };
try {
structuredClone(task);
} catch (err) {
console.log(err.name);
console.log(err.message);
console.log(err instanceof DOMException, err.code, err.constructor.name);
}
DataCloneError
() => console.log('完了') could not be cloned.
true 25 DOMException
エラーの name は DataCloneError、種類は DOMException(code は 25)です。 メッセージには複製できなかった値が表示されるので、原因のプロパティを探す手がかりになります。
ほかの値でも試しました。
Symbol → DataCloneError: Symbol(x) could not be cloned.
メソッド → DataCloneError: m() {} could not be cloned.
WeakMap → DataCloneError: #<WeakMap> could not be cloned.
Promise → DataCloneError: #<Promise> could not be cloned.
クラス(メソッドはprototype) OK {}
Proxy → DataCloneError: #<Object> could not be cloned.
Proxy配列 → DataCloneError: [object Array] could not be cloned.
| 渡した値 | 結果 |
|---|---|
| 関数・メソッドを自前のプロパティに持つ | DataCloneError |
| Symbol を値に持つ | DataCloneError |
| WeakMap・Promise | DataCloneError |
| Proxy で包んだオブジェクト・配列 | DataCloneError |
| クラスのインスタンス(メソッドはクラス側にある) | エラーにならず、データだけ複製される |
Proxy の行は、リアクティブな状態管理(値の変更を検知するために Proxy で包む仕組み)を使っているときに出会いやすいエラーです。
回避策1:関数を分けてからコピーする
const { onDone, ...data } = task;
const c1 = { ...structuredClone(data), onDone };
console.log(c1, c1.onDone === task.onDone);
{ title: '棚卸し', done: false, onDone: [Function: onDone] } true
分割代入で関数だけを取り出し、残りを structuredClone してから関数を戻します。 関数は状態を持たないことが多いので、同じ関数を指していて困る場面は多くありません。
回避策2:関数を取り除く関数を通す
ネストの深いところに関数が散らばっているなら、関数だけを落としてから複製します。JSON 往復と違い、Date や Map はそのまま保てます。
function stripFunctions(v, seen = new WeakMap()) {
if (typeof v === 'function') return undefined;
if (v === null || typeof v !== 'object') return v;
if (v instanceof Date || v instanceof RegExp || v instanceof Map || v instanceof Set) return v;
if (seen.has(v)) return seen.get(v);
const out = Array.isArray(v) ? [] : {};
seen.set(v, out);
for (const [k, x] of Object.entries(v)) {
const y = stripFunctions(x, seen);
if (y !== undefined || typeof x !== 'function') out[k] = y;
}
return out;
}
const big = { when: new Date(0), list: [{ id: 1, cb() {} }], fn() {} };
console.log(structuredClone(stripFunctions(big)));
{ when: 1970-01-01T00:00:00.000Z, list: [ { id: 1 } ] }
(Map や Set の中に関数が入っている場合までは扱っていない、簡易版です。)
回避策3:クラスには clone メソッドを用意する
structuredClone でクラスのインスタンスを複製すると、メソッドが使えなくなります。
class Cart {
constructor(items = []) { this.items = items; }
total() { return this.items.reduce((s, i) => s + i.price * i.qty, 0); }
clone() { return new Cart(structuredClone(this.items)); }
}
const cart = new Cart([{ price: 300, qty: 2 }]);
const sc = structuredClone(cart);
console.log(sc instanceof Cart, typeof sc.total);
const cc = cart.clone();
cc.items[0].qty = 5;
console.log(cc instanceof Cart, cc.total(), cart.total());
false undefined
true 1500 600
データ部分だけを structuredClone し、コンストラクタで組み立て直す clone メソッドを持たせると、クラスもメソッドも保ったまま、中身は独立した複製になります(複製側の合計は 1500、元は 600 のままです)。
クラスを変更できないときは、プロトタイプを付け直す書き方もあります。
const re = Object.assign(Object.create(Object.getPrototypeOf(cart)), structuredClone(cart));
console.log(re instanceof Cart, re.total()); // true 600
回避策4:Proxy は素のオブジェクトに戻してから
const prox = new Proxy({ a: { b: 1 } }, {});
console.log(structuredClone({ ...prox }));
{ a: { b: 1 } }
この例では、スプレッドで1階層ぶん素のオブジェクトに写してから渡すと複製できました。ネストした中身も Proxy で包まれている仕組みの場合は、そのライブラリが用意している「素のオブジェクトに戻す関数」を使ってから structuredClone に渡します(本記事では特定のライブラリでの確認は未実行です)。
回避策の選び方
| 状況 | 対応 |
|---|---|
| 関数のプロパティが1〜2個で場所がわかっている | 分割代入で分けてから structuredClone |
| 関数があちこちにある | stripFunctions のような前処理、または lodash の cloneDeep |
| クラスのインスタンス | clone メソッドを用意 |
| Proxy で包まれている | 素のオブジェクトに戻してから渡す |
| DOM 要素を含む | 要素は cloneNode、データは別に持つ |
DOM 要素のコピーは cloneNode を使う
ブラウザ(Playwright で起動した Chromium 141)で確かめました。次の HTML に対して実行しています。
<div id="box"><span>hi</span></div>
const el = document.getElementById('box');
structuredClone(el); // (1)
JSON.stringify({ el }); // (2)
({ ...{ el } }).el === el; // (3)
el.cloneNode(true).outerHTML; // (4)
el.cloneNode(false).outerHTML; // (5)
_.cloneDeep({ el }).el === el; // (6) lodash
| 番号 | 結果 |
|---|---|
| (1) | DataCloneError: Failed to execute 'structuredClone' on 'Window': HTMLDivElement object could not be cloned. |
| (2) | {"el":{}}(空のオブジェクトになる) |
| (3) | true(同じ要素を指す) |
| (4) | 子要素まで含めた複製(元とは別の要素) |
| (5) | 子要素を含まない、外側の div だけの複製 |
| (6) | true(lodash は要素を複製せず、同じ要素を入れる) |
DOM 要素を複製する専用のメソッドが cloneNode です。 引数に true を渡すと子孫まで、false(または省略)だとその要素だけを複製します。複製した要素はまだ画面にないので、appendChild などで追加して使います。なお、ブラウザでは DataCloneError のメッセージの先頭に「Failed to execute 'structuredClone' on 'Window':」が付き、Node.js とは文面が少し違います。
配列を書き換えずに操作する非破壊メソッド
コピーが必要になる理由の1つは、sort や splice が元の配列を書き換えることです。
const s = [3, 1, 2];
const r = s.sort();
console.log(r === s, s); // true [ 1, 2, 3 ]
sort の戻り値は元の配列そのもので、元の配列も並べ替えられています。以前は [...arr].sort() とコピーしてから並べ替えていましたが、元を変えずに新しい配列を返すメソッドが用意されています。
// nondestructive.mjs
const nums = [3, 1, 2];
| 式 | 結果 | 実行後の nums | 元を書き換えるメソッド |
|---|---|---|---|
nums.toSorted() |
[1,2,3] |
[3,1,2] |
sort() |
nums.toSorted((a, b) => b - a) |
[3,2,1] |
[3,1,2] |
sort(fn) |
nums.toReversed() |
[2,1,3] |
[3,1,2] |
reverse() |
nums.toSpliced(1, 1) |
[3,2] |
[3,1,2] |
splice(1, 1) |
nums.toSpliced(1, 0, 9) |
[3,9,1,2] |
[3,1,2] |
splice(1, 0, 9) |
nums.with(0, 99) |
[99,1,2] |
[3,1,2] |
nums[0] = 99 |
注意点が2つあります。
- これらが返すのは浅いコピーです。 オブジェクトの配列を toSorted しても、要素は元と同じ実体です(
objs.toSorted(...)[0] === objs[1]が true になることを確認しました)。要素の中身まで独立させたいときは structuredClone と組み合わせます。 - with は範囲外の位置を渡すと
RangeError: Invalid index : 10になります。
速度を計測する
同じデータを各方式でコピーし、1回あたりの時間を計りました。
// bench.mjs (lodash 4.18.1 を npm i lodash で入れておく)
import _ from 'lodash';
const make = n => ({ items: Array.from({ length: n }, (_, i) => ({ id: i, name: 'item' + i, tags: ['a', 'b'], size: { w: i, h: i * 2 } })) });
function bench(label, fn, data, loops) {
fn(data); // ウォームアップ
const t0 = performance.now();
for (let i = 0; i < loops; i++) fn(data);
const ms = (performance.now() - t0) / loops;
console.log(`${label.padEnd(20)} ${ms.toFixed(3).padStart(9)} ms/回`);
}
for (const [n, loops] of [[100, 2000], [10_000, 30]]) {
const data = make(n);
console.log(`--- 要素 ${n} 件(JSON にすると約 ${Math.round(JSON.stringify(data).length / 1024)} KB)`);
bench('スプレッド(浅い)', d => ({ ...d }), data, loops);
bench('JSON 往復', d => JSON.parse(JSON.stringify(d)), data, loops);
bench('structuredClone', d => structuredClone(d), data, loops);
bench('lodash cloneDeep', d => _.cloneDeep(d), data, loops);
}
Node.js 22.22.2 での結果です(10,000 件は2回実行した値を並べています)。
| 方式 | 100 件(約 6 KB) | 10,000 件(約 724 KB)1回目 | 同 2回目 |
|---|---|---|---|
| スプレッド(浅い) | 0.000 ms | 0.000 ms | 0.000 ms |
| JSON 往復 | 0.080 ms | 26.113 ms | 13.233 ms |
| structuredClone | 0.190 ms | 27.928 ms | 23.794 ms |
| lodash cloneDeep | 0.333 ms | 39.696 ms | 41.118 ms |
読み取れることは次のとおりです。
- 浅いコピーは件数に関係なくほぼ一瞬です(1階層目の参照を写すだけだからです)。
- 深いコピー3方式は、今回のデータ(JSON で表せる値だけ)では JSON 往復、structuredClone、cloneDeep の順でした。実行ごとのばらつきもあり、差は数倍の範囲に収まっています。
- 数値は環境とデータの形で変わります。速さよりも「必要な型を正しく複製できるか」で選び、深いコピーの回数そのものを減らす(変える階層だけスプレッドで作り直す)ほうが効果は大きくなります。
対応バージョン
MDN の互換性データ(npm パッケージ @mdn/browser-compat-data 8.1.4)を今回取得して確認した、最初に対応したバージョンです。
| 機能 | Chrome | Edge | Firefox | Safari | Node.js | Deno |
|---|---|---|---|---|---|---|
structuredClone |
98 | 98 | 94 | 15.4 | 17.0.0 | 1.14 |
toSorted / toReversed / toSpliced / with |
110 | 110 | 115 | 16 | 20.0.0 | 1.31 |
MDN の structuredClone のページ(今回開いて確認)では、2022年3月以降の主要ブラウザで広く使える機能(Baseline: Widely available)と案内されています。スプレッド構文と Object.assign、JSON 往復はそれ以前から使える書き方です。
structuredClone には第2引数に { transfer: [...] } を渡して、ArrayBuffer などを複製ではなく「移動」させる機能もあります。
const buf = new ArrayBuffer(8);
const moved = structuredClone(buf, { transfer: [buf] });
console.log(buf.byteLength, moved.byteLength); // 0 8
移動したあと、元の buf は長さ 0 になりました。大きなバイナリデータを複製せずに受け渡したいときに使います。
つまずきやすい点の一覧
| 症状 | 原因 | 対応 |
|---|---|---|
| コピーしたはずなのに元も変わる | 浅いコピーでネストした中身を共有している | structuredClone か、変える階層をスプレッドで作り直す |
const b = a のあと a も変わる |
代入はコピーではない | { ...a } や structuredClone(a) |
| DataCloneError で止まる | 関数・Symbol・DOM 要素・Proxy などを含む | 関数を分ける/素のオブジェクトに戻す |
| コピー後にメソッドが呼べない | structuredClone・JSON 往復はクラスを保たない | clone() メソッドを用意、または cloneDeep |
| コピー後に日付が文字列になっている | JSON 往復を使っている | structuredClone に置き換える |
| コピー後にプロパティが減っている | JSON 往復で undefined・関数が消えた | structuredClone に置き換える |
Converting circular structure to JSON |
循環参照を JSON にしようとした | structuredClone に置き換える |
| sort したら元の配列も変わった | sort は元を書き換える | toSorted() |
| toSorted した配列の要素を変えたら元も変わった | 返るのは浅いコピー | 要素を { ...x } や structuredClone で複製 |
よくある質問
Q. スプレッド構文と structuredClone はどう使い分けますか? A. コピーしたあとに書き換えるのが1階層目だけならスプレッド構文、ネストした中身まで書き換えるなら structuredClone です。迷ったときは「コピー先のどの階層を書き換えるか」を考えると決まります。計測のとおり浅いコピーは非常に軽いので、必要のないところまで深くコピーしないのもよい判断です。
Q. JSON.parse(JSON.stringify(obj)) は使わないほうがよいですか? A. JSON で表せる値だけのデータなら、今でも正しく動き、今回の計測では速さも十分でした。Date が文字列になる、undefined や関数が消える、循環参照で止まる、といった性質があるので、それらを含む可能性があるなら structuredClone が安心です。
Q. structuredClone でクラスのインスタンスをコピーするとどうなりますか? A. データ(自前のプロパティ)だけを持つ、ただのオブジェクトになります。instanceof は false になり、メソッドは呼べません。エラーにはならないので気づきにくい点です。クラスに clone メソッドを用意するか、lodash の cloneDeep を使うとクラスを保てます。
Q. const で宣言すれば書き換えを防げますか? A. const が防ぐのは変数への再代入だけで、オブジェクトの中身は書き換えられます。中身の書き換えを防ぎたいときは Object.freeze がありますが、こちらも1階層目だけに効きます。「元のデータを書き換えない」ことを保つには、コピーしてから変更する、非破壊メソッドを使う、という書き方をそろえるのが確実です。
まとめ
- 代入はコピーではありません。別のオブジェクトにするには、浅いコピーか深いコピーを使います。
- スプレッド構文と Object.assign は浅いコピーで、ネストした配列やオブジェクトは元と共有されます。
- ネストごと複製するなら structuredClone(obj) が標準の方法です。Date・Map・Set・循環参照も保てます。
- structuredClone は関数・Symbol・DOM 要素・Proxy で DataCloneError になります。関数を分ける、素のオブジェクトに戻す、といった前処理で対応できます。
- クラスのインスタンスは clone メソッド、DOM 要素は cloneNode(true) と、専用の方法を使います。
- JSON 往復は JSON で表せるデータ向けです。Date は文字列に、undefined と関数は消えます。
- toSorted・toReversed・toSpliced・with を使うと、配列をコピーせずに「元を変えない操作」が書けます(返るのは浅いコピーです)。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「JavaScriptオブジェクトのコピー|structuredClone入門」
株式会社RJCでは、JavaScriptのオブジェクトコピーとstructuredCloneの使い方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。