Deep JavaScript
この本のサポートをお願いします: 購入する または 寄付する
(広告です。ブロックしないでください。)

6 オブジェクトと配列のコピー



この章では、JavaScript でオブジェクトと配列をコピーする方法を学びます。

6.1 浅いコピー vs. 深いコピー

データをコピーする「深さ」には2種類あります。

次のセクションでは、両方の種類のコピーについて説明します。残念ながら、JavaScript は浅いコピーのみを組み込みでサポートしています。深いコピーが必要な場合は、自分で実装する必要があります。

6.2 JavaScript における浅いコピー

データを浅くコピーするいくつかの方法を見てみましょう。

6.2.1 スプレッド構文によるプレーンオブジェクトと配列のコピー

コピーを作成するために、オブジェクトリテラル および 配列リテラル にスプレッド構文を使用できます。

const copyOfObject = {...originalObject};
const copyOfArray = [...originalArray];

しかし、スプレッド構文にはいくつかの問題点があります。それらは次のサブセクションで説明します。それらの中には、真の制限であるものもあれば、単なる特殊性であるものもあります。

6.2.1.1 オブジェクトのスプレッド構文ではプロトタイプはコピーされません

例えば

class MyClass {}

const original = new MyClass();
assert.equal(original instanceof MyClass, true);

const copy = {...original};
assert.equal(copy instanceof MyClass, false);

次の2つの式は等価であることに注意してください。

obj instanceof SomeClass
SomeClass.prototype.isPrototypeOf(obj)

したがって、コピーにオリジナルと同じプロトタイプを与えることで、これを修正できます。

class MyClass {}

const original = new MyClass();

const copy = {
  __proto__: Object.getPrototypeOf(original),
  ...original,
};
assert.equal(copy instanceof MyClass, true);

あるいは、`Object.setPrototypeOf()` を使用して、コピーの作成後にコピーのプロトタイプを設定することもできます。

6.2.1.2 多くの組み込みオブジェクトは、オブジェクトのスプレッド構文ではコピーされない特別な「内部スロット」を持っています

このような組み込みオブジェクトの例としては、正規表現や日付があります。これらのコピーを作成すると、それらに格納されているデータのほとんどが失われます。

6.2.1.3 オブジェクトのスプレッド構文では、 own プロパティ (継承されていないプロパティ) のみがコピーされます

プロトタイプチェーン の仕組みを考えると、これは通常正しいアプローチです。しかし、それでも認識しておく必要があります。次の例では、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');
6.2.1.4 オブジェクトのスプレッド構文では、列挙可能なプロパティのみがコピーされます

たとえば、配列インスタンスの 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 “プロパティの列挙可能性” を参照してください。

6.2.1.5 プロパティ属性は、オブジェクトのスプレッド構文では常に忠実にコピーされるとは限りません

プロパティの_属性_ に関係なく、そのコピーは常に書き込み可能で構成可能なデータプロパティになります。

たとえば、ここでは属性 `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 プロパティを転送します (後で示すように)。

6.2.1.6 コピーは浅いです

コピーには、オリジナルの各キーと値のエントリの新しいバージョンがありますが、オリジナルの値自体はコピーされません。例えば

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'}});

深いコピーについては、この章の後半で説明します。

6.2.2 `Object.assign()` による浅いコピー (オプション)

`Object.assign()` は、主にオブジェクトへのスプレッド構文と同様に機能します。つまり、次の2つのコピー方法はほぼ同等です。

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

構文の代わりにメソッドを使用すると、ライブラリを使用して古い 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” を参照してください。)

6.2.3 `Object.getOwnPropertyDescriptors()` と `Object.defineProperties()` による浅いコピー (オプション)

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);

6.3 JavaScript における深いコピー

いよいよ深いコピーに取り組みます。まず、手動で深くコピーし、次に汎用的なアプローチを検討します。

6.3.1 ネストしたスプレッド構文による手動の深いコピー

スプレッド構文をネストすると、深いコピーが得られます。

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);

6.3.2 ハック: JSON を使用した汎用的な深いコピー

これはハックですが、緊急時には迅速なソリューションを提供します。オブジェクト `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$/);

6.3.3 汎用的な深いコピーの実装

次の関数は、値 `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つの問題 (浅いコピー) のみを修正することに注意してください。その他はすべて残ります。プロトタイプはコピーされず、特別なオブジェクトは部分的にのみコピーされ、列挙不可能なプロパティは無視され、ほとんどのプロパティ属性は無視されます。

完全に汎用的なコピーを実装することは、一般に不可能です。すべてのデータがツリーであるとは限らず、すべてのプロパティをコピーしたくない場合などがあります。

6.3.3.1 より簡潔なバージョンの `deepCopy()`

`.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;
  }
}

6.4 参考文献