【JavaScript】structuredClone()でオブジェクトのディープコピーをする方法

更新日:2023/10/05

JavaScriptでオブジェクトをディープコピーをするとき、structuredClone()を使用すると簡単にコピーできます。
こんなに便利なのがあったなんて!と驚いたので紹介します。

2023/10 内容を整理しました

 

structuredClone()の使い方

まずはstructuredClone()の構文を確認してみます。

structuredClone()の構文

structuredClone(value , transfer )
  • value : コーピー元のオブジェクト
  • transfer : 省略可能。後述
ディープコピーしたオブジェクト

引数にオブジェクトを指定すると、ディープコピーされたオブジェクトが返されます。

structuredClone()の使用例

const originalObj = {
  value1 : 100,
  obj1: {
    value2:"abc",
  }
};
 // ディープコピー
const copyObj = structuredClone(originalObj);
 // オリジナルの値を変更
originalObj.obj1.value2 = 1000;

console.log( originalObj );
// 結果: { value1: 100, obj1: { value2: 1000 } }
console.log( copyObj );
// 結果: { value1: 100, obj1: { value2: 'abc' } }

上のコードはstructuredClone()でコピー後にオリジナルの値を変更しています。
コピー後のオブジェクトはその影響を受けていないので、ディープコピーされているのがわかります。

Object.assign()でのシャローコピーとの差異を確認してみます。

 // シャローコピー
const copyObj = Object.assign(originalObj);

originalObj.obj1.value2 = 1000;

console.log( originalObj );
// 結果: { value1: 100, obj1: { value2: 1000 } }
console.log( copyObj );
// 結果: { value1: 100, obj1: { value2: 1000 } }

コピー後のオブジェクトが、オリジナルオブジェクトの変更の影響を受けています。
originalObj.obj1 と copyObj.obj1 は同じものを共有している、つまりコピーできていないということがわかります。

 

配列のディープコピー

配列もディープコピーしてくれます。

const originalObj = {
  value1 : 100,
  obj1: {
    array:[1,2,3],
  }
};

const copyObj = structuredClone(originalObj);

originalObj.obj1.array[0]=1000;

console.log( originalObj );
 // { value1: 100, obj1: { array: [ 1000, 2, 3 ] } }
console.log( copyObj );
 // { value1: 100, obj1: { array: [ 1, 2, 3 ] } }

上のコードはオリジナルの値を変更していますが、コピー後の配列には影響していません。
つまりディープコピーされています。

 

ArrayBufferのディープコピー

ArrayBufferもディープコピーしてくれます。

const originalObj = {
  value1 : 100,
  obj1: {
    u8Array:new Uint8Array([1,2,3]),
  }
};

const copyObj = structuredClone(originalObj);

originalObj.obj1.array[0]=1000;

console.log( originalObj );
 // { value1: 100, obj1: { u8Array: Uint8Array(3) [ 232, 2, 3 ] } }
console.log( copyObj );
 // { value1: 100, obj1: { u8Array: Uint8Array(3) [ 1, 2, 3 ] } }

上のコードはUint8Arrayの値を変更していますが、他方の配列に影響していませんね。
なお1000を代入したのに値が232なのは、16進数で3E8の下1バイトのみがセットされたからです。

 

Map,Set,Errorのディープコピー

組み込みオブジェクトのMap、Set、Errorもコピー対象です。

const map = new Map( [ [1,"a"] ]);
const set = new Set( [1] );
const error = new Error( "error!" );

const originalObj = { 1:map , 2:set , 3:error };

const copyObj = structuredClone(originalObj);
  // コピー後のオブジェクトを変更
copyObj[1].set( 2 , "b" );
copyObj[2].add( 2 );
copyObj[3].stack ="no error!";

console.log( originalObj );
  // 結果: {1: Map { 1"a" }, 2:  Set [ 1 ], 3: Error: error! }
console.log( copyObj );
  // 結果: {1: Map { 1 → "a", 2 → "b" }, 2:  Set [ 1, 2 ], 3: Error: no error! }

コピー後の配列内オブジェクトに変更を加えても、コピー前のオブジェトに影響しません。
新規でオブジェクトが作成され、それぞれの元オブジェクトが持っていた値がコピーされたのです。

 

structuredClone()の制限

structuredClone()はいくつか制限があります。

関数オブジェクトはコピーできない

structuredClone()は関数オブジェクトをコピーできません。
エラーがスローされます。

const originalObj = {
  func:function(){},
};

const copyObj = structuredClone(originalObj);
 // Uncaught DOMException: Function object could not be cloned.

つまり、メソッドが含まれている可能性があるときは、structuredClone()を使用できません

プライベートフィールドはコピーできない

class構文のプライベートフィールドはコピーできません。

const classA = class {
    value1= 'abc';
    #value2 = 1000;
};

const originalObj = new classA();
const copyObj = structuredClone(originalObj);

console.log( originalObj );
  // 結果: { value1: "abc", #value2: 1000 }
console.log( copyObj );
  // 結果: { value1: "abc" }

そもそもメソッドがコピーできないということは、constructor()もコピーできません。
class構文を使う意味がないですね。

プロトタイプチェーンはコピーできない

プロトタイプチェーンもコピーしてくれません。

  // コンストラクター関数の定義
const constructorfunc = function(){
    this.value=1;
};
constructorfunc.prototype={ 
  func: function(){ return this.value; } 
}

const originalObj = new constructorfunc();
const copyObj = structuredClone(originalObj);

console.log( originalObj.func() );
  // 結果: 1
console.log( copyObj.func() );
  // 結果: TypeError: copyObj.func is not a function

プロトタイプチェーンがコピーされていないので、copyObj.func の値が undefined になり、undefinedを()で実行しようとしてエラーになります。

なお、コピー後のプロトタイプチェーンは、規定値のObject.prototypeを参照しています。

 // プロトタイプチェーンを取得して Object.prototype と比較
console.log( Object.getPrototypeOf(copyObj) === Object.prototype );
  // 結果: true

セッターやゲッターはコピーできない

セッターやゲッターは削除され、その時点での値でプロパティが作成されます。

const originalObj = {
    _value:1,
    set value( v ){ this._value = v; },
    get value( ){ return this._value; },
};

const copyObj = structuredClone(originalObj);

console.log( originalObj );
  // { _value: 1, value: [Setter] }
console.log( copyObj );
  // { _value: 1, value: 1 }

また、プロパティ記述子を変更している場合は、変更分が無効になります。

他にも条件がありますが、structuredClone()はプロパティ値のみのオブジェクトに使用した方がよさそうです。

 

循環参照の扱い

structuredClone()は、プロパティ値の循環参照にも対応しています。

const originalObj = { obj:{}};
originalObj.obj.obj = originalObj; // 循環参照の作成

const copyObj = structuredClone(originalObj);

console.log( originalObj.obj.obj === originalObj );
  // 結果: true
console.log( copyObj.obj.obj === copyObj );
  // 結果: true

 

structuredClone()の第二引数について

structuredClone()の第二引数は、元となるオブジェクトの内容をコピー先に転送して、元となるオブジェクトの内容をクリアします。
ただし、元となるオブジェクトに指定できるのは、転送可能オブジェクト(Transferable objects)のみという制限があります。

第二引数は、次の形式で指定します。

{ transfer:[ 転送可能オブジェクト,転送可能オブジェクト,... ] }

転送可能オブジェクトは、主にArrayBufferを指します。
これはJavaScriptの組み込みオブジェクトですね。

その他にWebAPI等は、内部に[[Detached]] という名前のスロットを持っているものが、転送可能オブジェクトです。
一部ですが次のようなものがあります。

ReadableStream、WritableStream、TransformStream、ImageBitmapなど

structuredClone()の第二引数で転送可能オブジェクトオブジェクトは、コピー先にセットされた後、初期化されたオブジェクトがコピー元にセットされます。

const originalObj = {
  value1 : 100,
  obj1: {
    u8Array:new Uint8Array(10),
  }
};

const copyObj = structuredClone(originalObj
    ,{ transfer : [ originalObj.obj1.u8Array.buffer ] });

console.log( originalObj );
 // { value1: 100, obj1: { u8Array: Uint8Array(0) [] } }

console.log( copyObj );
 // {
 //  value1: 100,
 //  obj1: {
 //    u8Array: Uint8Array(10) [
 //      0, 0, 0, 0, 0,
 //      0, 0, 0, 0, 0
 //    ]
 //  }
 // }

更新日:2023/10/05

書いた人(管理人):けーちゃん

スポンサーリンク

記事の内容について

null

こんにちはけーちゃんです。
説明するのって難しいですね。

「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。

裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。

掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。

ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php

 

このサイトは、リンクフリーです。大歓迎です。