【JavaScript】配列をディープコピーする方法

更新日:2023/08/03

JavaScriptで配列をディープコピーする方法をお伝えします。
自作コードも紹介しています。

 

JSON.parse()とJSON.stringify()を使用する(非推奨)

オブジェクトをディープコピーする方法として、JSON.parse()とJSON.stringify()を使用する例が挙げられることがあります。
配列はオブジェクトなので、この方法を使用できます。

const array1 = [ 1 , 2,  [ 3 , 4 ] , { a:["a","b"] , b:"c" } ];

const array2 = JSON.parse( JSON.stringify(array1) );

array2[2][1] = "change!";
console.log( array1 ); // 結果: [ 1, 2, [ 3, 4 ], { a: [ 'a', 'b' ], b: 'c' } ]
console.log( array2 ); // 結果: [ 1, 2, [ 3, 'change!' ], { a: [ 'a', 'b' ], b: 'c' } ]

コピー後の配列の内容を変更しても、コピー元の配列の内容はそのままなのでディープコピーできていますね。

でも、あまりおススメしません。
この二つのメソッドを使う方法とおススメしない理由は、次のページを読んでみてください。

 

structuredClone()を使用する

JavaScriptにはオブジェクトをディープコピーしてくれる、structuredClone()という関数があります。
JSON.parse()とJSON.stringify()よりも、こちらの方がおすすめです。

const array1 = [ 1 , 2,  [ 3 , 4 ] , { a:["a","b"] , b:"c" } ];

const array2 = structuredClone( array1 );

array2[2][1] = "change!";
console.log( array1 ); // 結果: [ 1, 2, [ 3, 4 ], { a: [ 'a', 'b' ], b: 'c' } ]
console.log( array2 ); // 結果: [ 1, 2, [ 3, 'change!' ], { a: [ 'a', 'b' ], b: 'c' } ]

JSON.parse()とJSON.stringify()と比較すると、次のような点で優れています。

  • undefine値もコピーしてくれる
  • 循環参照を処理してくれる
  • 一部の組み込みオブジェクトを複製してくれる

ただし、メソッドが含まれているとエラーがスローされる点に注意が必要です。

 

自作する

メソッドでエラーになると困るので、頑張って自作しました。

コード

const copyObject = ((copiedConstructorList)=>{
    const constructorMap = new WeakMap(); // コピー対象コンストラクター用マップ

        // コピー本体
    const copy = (obj,dumy,copiedObjMap) =>{
            // 空のオブジェクトを作成
        const newObj = Array.isArray(obj) ? [] : {};
            // コピー済みオブジェクトマップに作成したオブジェクトを追加
        copiedObjMap.set(obj,newObj);

            // ディスクリプターでループ
        return Object.entries( Object.getOwnPropertyDescriptors( obj ) ) 
            .reduce( (r,[key,dsc]) =>{
                if( dsc.hasOwnProperty( "value" ) ){ // プロパティは値を持っている?
                    const value = dsc.value;
                    if( typeof value === "object" ) { // 値はオブジェクト?
                            // コピー済みチェック(循環参照回避)
                        const registeredObj = copiedObjMap.get( value );
                        if( registeredObj ) dsc.value = registeredObj;
                        else { // コピー済みではない
                            const f = constructorMap.get( Object.getPrototypeOf( value ) ); // 登録済みコンストラクター?
                                // オブジェクトをコピーする関数を呼び出してディスクリプターを変更
                            if( f ) dsc.value = f.callFunc( value , f.constructor , copiedObjMap);
                        }
                    }
                }
                    // ディスクリプターでプロパティ追加
                Object.defineProperty( r , key , dsc);
                return r;
            },newObj );
    };
        // コピー対象コンストラクターの追加関数
    const addCopiedConstructor = ( constructor ,  callFunc )=> constructor 
                ? constructorMap.set( constructor.prototype , {constructor,callFunc} ) : null;

        // 初期コピー対象コンストラクターの追加
    copiedConstructorList.forEach( e =>{
            const func = e[1] ?? copy;
            Array.isArray( e[0] ) ? e[0].forEach( e=>addCopiedConstructor(e,func) ) 
                : addCopiedConstructor(e[0],func);
        });
    
        // 外部公開用関数
    const exportFunc = obj=>copy(obj,null,new WeakMap());
        // コピー対象コンストラクターの追加関数を公開
    exportFunc.addConstructor = addCopiedConstructor;

    return exportFunc;

})( [  // 初期コピー対象コンストラクター [コンストラクター,オブジェクトをコピーする関数] 
        [Object], // 呼び出す関数未定義 ⇒ コピー関数の再帰呼び出し
        [Array],
        [Map,(v,c)=>new c(v.entries())], // 引数: ( プロパティ値 , コンストラクター )
        [Set,(v,c)=>new c(v.values())],
        [[Error,RegExp,ArrayBuffer,DataView],(v,c)=>structuredClone( v )],
        [[Int8Array,Uint8Array,Uint8ClampedArray,Int16Array,Uint16Array,Int32Array,Uint32Array,BigInt64Array,BigUint64Array,Float32Array,Float64Array],
            (v,c)=>new c(v) ],
    ]
);

解説

この関数は、配列だけでなくオブジェクトもディープコピーできます。
そもそも配列もオブジェクトなので、配列に特化する必要はあまりないですね。

ここではWeakMapを二つ使っています。

constructorMapはキーとしてコンストラクターのprototypeプロパティを、値としてオブジェクトをコピーする関数が登録されています。
ここに登録されているコンストラクターから生成されたオブジェクトのみ、コピーされます。
それ以外は、参照がセットされます。

ちなみに、次の関係が成り立ちます。

オブジェクトのプロトタイプチェーン(Object.getPrototypeOf()で取得) = コンストラクターのprototypeプロパティ

copiedObjMapはコピー毎に生成されるマップです。
循環参照での無限ループを避けるために、キーとしてコピー元のオブジェクトを、値としてコピー先のオブジェクトが登録されています。

オブジェクトから取得したプロパティ値がこのマップに登録されていたら、コピーせずに、登録されている値をコピー先にセットします。
これで、コピー先でも循環参照が形成されます。

今回はコピー元プロパティのディスクリプタ―を取得して、それを使用してコピー先プロパティを追加しています。
これにより上書き等の属性やセッター/ゲッターのコピーが可能になっています。

使用例

まずは単純な値を持っている多階層配列です。

const a = [1,2,[3,4,{a:5,b:6}]];
const b = copyObject( a );

b[2][2].b = 100;

console.log( a ); // 結果: [1,2,[3,4,{a:5,b:6}]]
console.log( b ); // 結果: [1,2,[3,4,{a:5,b:100}]]

コピー先を変更しても、コピー元が影響を受けていないのでコピーされているのがわかります。

関数をコピーしてみます。

const a = [ 1 , ()=>{} ];

const b = copyObject( a );

console.log( a[1] === b[1] ); // 結果: true

比較するとtrueでした。
これは、同じ参照先が同じということを意味しています。

次に、コンストラクター登録済みのオブジェクトで確認してみます。

const a = [1,2,[3,new Set([1,2])]];
const b = copyObject( a );

b[2][1].add(100);

console.log( a ); // 結果:  [1,2,[3, Set [1,2] ]]
console.log( b ); // 結果:  [1,2,[3, Set [1,2,100] ]]
console.log( a[2][1] === b[2][1] ); // 結果: false

コピー後のSetオブジェクトに値を追加しても、コピー前のものは変化していませんね。
参照先を比較しても、異なるオブジェクトだとわかります。

次に自作クラスです。

const c1 = class {
    b=100;
}

const a = [1,2,new c1];
const b = copyObject( a );

b[2].b = 200;

console.log( a ); // 結果: [1,2,{b:200}]
console.log( b ); // 結果: [1,2,{b:200}]

コピー先を変更したら、コピー元が影響を受けました。
コピーされていないのがわかります。

自作クラスをコピー対象登録してみます。

const c1 = class {
    b=100;
    clone(){
        const obj = new c1();
        obj.b = this.b;
        return obj;
    }
}
copyObject.addConstructor( c1 , (v,c)=>v.clone() );

const a = [1,2,new c1];
const b = copyObject( a );

b[2].b = 200;

console.log( a ); // 結果: [1,2,{b:100}]
console.log( b ); // 結果: [1,2,{b:200}]

コピー元は影響を受けていませね。
別オブジェクトとしてコピーできています。

セッター/ゲッターをコピーしてみます。

const a = { a:100 , get b(){return this.a;} ,set b(v){ this.a = v} };
const b = copyObject( a );

b.b = 200;

console.log( a ); //  結果: { a: 100, b: Getter & Setter }
console.log( b ); //  結果: { a: 200, b: Getter & Setter }

コピー後も、セッター/ゲッターとして機能していますね。

最後に、属性コピーを確認してみます。

const a = { a:100 };
Object.defineProperty(a,"b",{enumerable:false,value:5});

const b = copyObject( a );

console.log( Object.keys( a ) ); // 結果: [ "a" ]
console.log( Object.keys( b ) ); // 結果: [ "a" ]

Object.keys()は、enumerable属性がtrueのプロパティを抽出します。
コピー後のオブジェクトがコピー前と同じ結果になっているので、属性もコピーされているのがわかります。

更新日:2023/08/03

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

スポンサーリンク

記事の内容について

null

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

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

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

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

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

 

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