この章では、JavaScript でオブジェクトと配列をコピーする方法を学びます。
データをコピーする「深さ」には2種類あります。
次のセクションでは、両方の種類のコピーについて説明します。残念ながら、JavaScript は浅いコピーのみを組み込みでサポートしています。深いコピーが必要な場合は、自分で実装する必要があります。
データを浅くコピーするいくつかの方法を見てみましょう。
コピーを作成するために、オブジェクトリテラル および 配列リテラル にスプレッド構文を使用できます。
しかし、スプレッド構文にはいくつかの問題点があります。それらは次のサブセクションで説明します。それらの中には、真の制限であるものもあれば、単なる特殊性であるものもあります。
例えば
class MyClass {}
const original = new MyClass();
assert.equal(original instanceof MyClass, true);
const copy = {...original};
assert.equal(copy instanceof MyClass, false);
次の2つの式は等価であることに注意してください。
したがって、コピーにオリジナルと同じプロトタイプを与えることで、これを修正できます。
class MyClass {}
const original = new MyClass();
const copy = {
__proto__: Object.getPrototypeOf(original),
...original,
};
assert.equal(copy instanceof MyClass, true);
あるいは、`Object.setPrototypeOf()` を使用して、コピーの作成後にコピーのプロトタイプを設定することもできます。
このような組み込みオブジェクトの例としては、正規表現や日付があります。これらのコピーを作成すると、それらに格納されているデータのほとんどが失われます。
プロトタイプチェーン の仕組みを考えると、これは通常正しいアプローチです。しかし、それでも認識しておく必要があります。次の例では、own プロパティのみをコピーし、プロトタイプを保持しないため、`original` の継承されたプロパティ `.inheritedProp` は `copy` では使用できません。
const proto = { inheritedProp: 'a' };
const original = {__proto__: proto, ownProp: 'b' };
assert.equal(original.inheritedProp, 'a');
assert.equal(original.ownProp, 'b');
const copy = {...original};
assert.equal(copy.inheritedProp, undefined);
assert.equal(copy.ownProp, 'b');
たとえば、配列インスタンスの own プロパティ `.length` は列挙可能ではなく、コピーされません。次の例では、オブジェクトのスプレッド構文 (行 A) を使用して配列 `arr` をコピーしています。
const arr = ['a', 'b'];
assert.equal(arr.length, 2);
assert.equal({}.hasOwnProperty.call(arr, 'length'), true);
const copy = {...arr}; // (A)
assert.equal({}.hasOwnProperty.call(copy, 'length'), false);
ほとんどのプロパティは列挙可能であるため、これもめったに制限にはなりません。列挙不可能なプロパティをコピーする必要がある場合は、`Object.getOwnPropertyDescriptors()` と `Object.defineProperties()` を使用してオブジェクトをコピーできます (その方法は後で説明します)。
列挙可能性の詳細については、§12 “プロパティの列挙可能性” を参照してください。
プロパティの_属性_ に関係なく、そのコピーは常に書き込み可能で構成可能なデータプロパティになります。
たとえば、ここでは属性 `writable` と `configurable` が `false` であるプロパティ `original.prop` を作成します。
const original = Object.defineProperties(
{}, {
prop: {
value: 1,
writable: false,
configurable: false,
enumerable: true,
},
});
assert.deepEqual(original, {prop: 1});
`.prop` をコピーすると、`writable` と `configurable` はどちらも `true` になります。
const copy = {...original};
// Attributes `writable` and `configurable` of copy are different:
assert.deepEqual(
Object.getOwnPropertyDescriptors(copy),
{
prop: {
value: 1,
writable: true,
configurable: true,
enumerable: true,
},
});
結果として、ゲッターとセッターも忠実にコピーされません。
const original = {
get myGetter() { return 123 },
set mySetter(x) {},
};
assert.deepEqual({...original}, {
myGetter: 123, // not a getter anymore!
mySetter: undefined,
});
前述の `Object.getOwnPropertyDescriptors()` と `Object.defineProperties()` は、常にすべての属性がそのままの own プロパティを転送します (後で示すように)。
コピーには、オリジナルの各キーと値のエントリの新しいバージョンがありますが、オリジナルの値自体はコピーされません。例えば
const original = {name: 'Jane', work: {employer: 'Acme'}};
const copy = {...original};
// Property .name is a copy: changing the copy
// does not affect the original
copy.name = 'John';
assert.deepEqual(original,
{name: 'Jane', work: {employer: 'Acme'}});
assert.deepEqual(copy,
{name: 'John', work: {employer: 'Acme'}});
// The value of .work is shared: changing the copy
// affects the original
copy.work.employer = 'Spectre';
assert.deepEqual(
original, {name: 'Jane', work: {employer: 'Spectre'}});
assert.deepEqual(
copy, {name: 'John', work: {employer: 'Spectre'}});
深いコピーについては、この章の後半で説明します。
`Object.assign()` は、主にオブジェクトへのスプレッド構文と同様に機能します。つまり、次の2つのコピー方法はほぼ同等です。
構文の代わりにメソッドを使用すると、ライブラリを使用して古い JavaScript エンジンでポリフィルできるという利点があります。
ただし、`Object.assign()` はスプレッド構文と完全に同じではありません。比較的微妙な点で1つ異なります。プロパティの作成方法が異なります。
とりわけ、代入は own セッターと継承されたセッターを呼び出しますが、定義は呼び出しません (代入と定義の詳細)。この違いはめったに気づかれません。次のコードは例ですが、不自然です。
const original = {['__proto__']: null}; // (A)
const copy1 = {...original};
// copy1 has the own property '__proto__'
assert.deepEqual(
Object.keys(copy1), ['__proto__']);
const copy2 = Object.assign({}, original);
// copy2 has the prototype null
assert.equal(Object.getPrototypeOf(copy2), null);
行 A で計算されたプロパティキーを使用することにより、`.__proto__` を own プロパティとして作成し、継承されたセッターを呼び出しません。ただし、`Object.assign()` がそのプロパティをコピーすると、セッターが呼び出されます。( `.__proto__` の詳細については、“JavaScript for impatient programmers” を参照してください。)
JavaScript では、_プロパティ記述子_ (プロパティ属性を指定するオブジェクト) を使用してプロパティを作成できます。たとえば、すでに動作を確認した `Object.defineProperties()` を使用します。そのメソッドを `Object.getOwnPropertyDescriptors()` と組み合わせると、より忠実にコピーできます。
function copyAllOwnProperties(original) {
return Object.defineProperties(
{}, Object.getOwnPropertyDescriptors(original));
}
これにより、スプレッド構文を使用してオブジェクトをコピーする際の2つの問題が解消されます。
まず、own プロパティのすべての属性が正しくコピーされます。したがって、own ゲッターと own セッターをコピーできるようになりました。
const original = {
get myGetter() { return 123 },
set mySetter(x) {},
};
assert.deepEqual(copyAllOwnProperties(original), original);
次に、`Object.getOwnPropertyDescriptors()`のおかげで、列挙不可能なプロパティもコピーされます。
const arr = ['a', 'b'];
assert.equal(arr.length, 2);
assert.equal({}.hasOwnProperty.call(arr, 'length'), true);
const copy = copyAllOwnProperties(arr);
assert.equal({}.hasOwnProperty.call(copy, 'length'), true);
いよいよ深いコピーに取り組みます。まず、手動で深くコピーし、次に汎用的なアプローチを検討します。
スプレッド構文をネストすると、深いコピーが得られます。
const original = {name: 'Jane', work: {employer: 'Acme'}};
const copy = {name: original.name, work: {...original.work}};
// We copied successfully:
assert.deepEqual(original, copy);
// The copy is deep:
assert.ok(original.work !== copy.work);
これはハックですが、緊急時には迅速なソリューションを提供します。オブジェクト `original` を深くコピーするには、まず JSON 文字列に変換し、その JSON 文字列を解析します。
function jsonDeepCopy(original) {
return JSON.parse(JSON.stringify(original));
}
const original = {name: 'Jane', work: {employer: 'Acme'}};
const copy = jsonDeepCopy(original);
assert.deepEqual(original, copy);
このアプローチの重大な欠点は、JSON でサポートされているキーと値を持つプロパティのみをコピーできることです。
サポートされていないキーと値は単に無視されます。
assert.deepEqual(
jsonDeepCopy({
// Symbols are not supported as keys
[Symbol('a')]: 'abc',
// Unsupported value
b: function () {},
// Unsupported value
c: undefined,
}),
{} // empty object
);
その他のものは例外を引き起こします。
assert.throws(
() => jsonDeepCopy({a: 123n}),
/^TypeError: Do not know how to serialize a BigInt$/);
次の関数は、値 `original` を汎用的に深くコピーします。
function deepCopy(original) {
if (Array.isArray(original)) {
const copy = [];
for (const [index, value] of original.entries()) {
copy[index] = deepCopy(value);
}
return copy;
} else if (typeof original === 'object' && original !== null) {
const copy = {};
for (const [key, value] of Object.entries(original)) {
copy[key] = deepCopy(value);
}
return copy;
} else {
// Primitive value: atomic, no need to copy
return original;
}
}
関数は3つのケースを処理します。
`deepCopy()` を試してみましょう。
const original = {a: 1, b: {c: 2, d: {e: 3}}};
const copy = deepCopy(original);
// Are copy and original deeply equal?
assert.deepEqual(copy, original);
// Did we really copy all levels
// (equal content, but different objects)?
assert.ok(copy !== original);
assert.ok(copy.b !== original.b);
assert.ok(copy.b.d !== original.b.d);
`deepCopy()` は、スプレッド構文の1つの問題 (浅いコピー) のみを修正することに注意してください。その他はすべて残ります。プロトタイプはコピーされず、特別なオブジェクトは部分的にのみコピーされ、列挙不可能なプロパティは無視され、ほとんどのプロパティ属性は無視されます。
完全に汎用的なコピーを実装することは、一般に不可能です。すべてのデータがツリーであるとは限らず、すべてのプロパティをコピーしたくない場合などがあります。
`.map()` と `Object.fromEntries()` を使用すると、以前の `deepCopy()` の実装をより簡潔にすることができます。
function deepCopy(original) {
if (Array.isArray(original)) {
return original.map(elem => deepCopy(elem));
} else if (typeof original === 'object' && original !== null) {
return Object.fromEntries(
Object.entries(original)
.map(([k, v]) => [k, deepCopy(v)]));
} else {
// Primitive value: atomic, no need to copy
return original;
}
}