以前にJavaScriptで配列を値渡しでコピーする記事を投稿したことがありますが、今回はシャローコピー(Shallow copy:浅いコピー)とディープコピー(Deep copy:深いコピー)についてそれぞれ見てみます。
シャローコピー
シャローコピーは名前の通り浅いコピーで、配列内に別の配列やオブジェクトがあるなどといった入れ子構造の配列をコピーした場合、入れ子の中は参照渡しになります。
まずは例として、入れ子になっていない配列のコピーを4種類試してみます。
const array = [1, 2, 3, 4, 5]; // スプレッド構文でコピー const copy1 = [...array]; copy1[0] = 'spread'; console.log(copy1); // ['spread', 2, 3, 4, 5] // from()でコピー const copy2 = Array.from(array); copy2[1] = 'from'; console.log(copy2); // [1, 'from', 3, 4, 5] // slice()でコピー const copy3 = array.slice(); copy3[2] = 'slice'; console.log(copy3); // [1, 2, 'slice', 4, 5] // concat()でコピー const copy4 = array.concat(); copy4[3] = 'concat'; console.log(copy4); // [1, 2, 3, 'concat', 5] // コピーした配列の値を変更しても、元の配列は変更されない console.log(array); // [1, 2, 3, 4, 5]
この場合はコピー後の配列の値を変更しても、元の配列に影響はありません。
シャローコピーのデモページ
次に入れ子構造になっている配列のコピーを試してみます。
const array = [[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]]; // スプレッド構文でコピー const copy1 = [...array]; copy1[0][0] = 'spread'; console.log(copy1); // [['spread', 2, 3, 4, 5], [6, 7, 8, 9, 10]] // from()でコピー const copy2 = Array.from(array); copy2[0][1] = 'from'; console.log(copy2); // [['spread', 'from', 3, 4, 5], [6, 7, 8, 9, 10]] // slice()でコピー const copy3 = array.slice(); copy3[0][2] = 'slice'; console.log(copy3); // [['spread', 'from', 'slice', 4, 5], [6, 7, 8, 9, 10]] // concat()でコピー const copy4 = array.concat(); copy4[0][3] = 'concat'; console.log(copy4); // [['spread', 'from', 'slice', 'concat', 5], [6, 7, 8, 9, 10]] // コピーした配列の値を変更した影響で、元の配列も変更される console.log(array); // [['spread', 'from', 'slice', 'concat', 5], [6, 7, 8, 9, 10]]
この場合の入れ子の配列部分は参照渡しになるため、コピーした配列内で入れ子になっている配列の値を変更すると、元の配列も変更されました。
シャローコピーのデモページ2
JavaScriptで用意されているコピー操作は、シャローコピーになるものがほとんどようです。
ディープコピー
ディープコピーはシャローコピーとは逆の深いコピーで、入れ子構造の配列をコピーした場合でも、入れ子の中は参照渡しになりません。
ディープコピーを作成する方法の一つが、JSON.parse()とJSON.stringify()を使用する方法です。
const array = [[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]]; const copy = JSON.parse(JSON.stringify(array)); copy[0][0] = 'deepcopy'; console.log(copy); // [['deepcopy', 2, 3, 4, 5], [6, 7, 8, 9, 10]] // コピーした配列の値を変更しても、元の配列は変更されない console.log(array); // [[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]]
この方法であれば入れ子になっている配列の値を変更しても、元の配列の値は変更されません。
ディープコピーのデモページ
ただ、上記内容の場合は問題ないのですが、関数やDateオブジェクトなど、内容によってはうまくコピーできないものもある点注意が必要です。
const array = [ { 'label': 'testA', 'date': new Date(), 'function': function() { console.log('a'); }, 'element': document.body, 'undefined': undefined } ]; console.log(array); // [{label: 'testA', date: Mon Jan 23 2023 01:20:36 GMT+0900 (日本標準時), element: body, undefined: undefined, function: ƒ}] const copy = JSON.parse(JSON.stringify(array)); console.log(copy); // [{label: 'testA', date: '2023-01-22T16:20:36.432Z', element: {…}}]
ディープコピーを作成する別の方法で、structuredClone()を使用する方法もあります。
structuredClone()はIEで非対応、Safariも15.4から対応となっています。
const array = [ { 'array': [1, 2, 3], 'label': 'testA', 'date': new Date(), 'undefined': undefined } ]; const copy = structuredClone(array); copy[0].array[0] = 'structuredClone'; console.log(copy); // [{array: ['structuredClone', 2, 3], label: 'testA', date: Mon Jan 23 2023 01:20:36 GMT+0900 (日本標準時), undefined: undefined}] // コピーした配列の値を変更しても、元の配列は変更されない console.log(array); // [{array: [1, 2, 3], label: 'testA', date: Mon Jan 23 2023 01:20:36 GMT+0900 (日本標準時), undefined: undefined}]
structuredClone()を使った方法の場合も前述の例と同じく、内容によってはうまくコピーできないものもある点注意が必要です。
const array = [ { 'array': [1, 2, 3], 'label': 'testA', 'date': new Date(), 'function': function() { console.log('a'); }, 'undefined': undefined } ]; const copy = structuredClone(array); // Uncaught DOMException: Failed to execute 'structuredClone' on 'Window': function() {console.log('a');} could not be cloned. const array2 = [ { 'array': [1, 2, 3], 'label': 'testA', 'date': new Date(), 'element': document.body, 'undefined': undefined } ]; const copy2 = structuredClone(array2); // Uncaught DOMException: Failed to execute 'structuredClone' on 'Window': HTMLBodyElement object could not be cloned.
最後にmap()などを使って新しく配列を生成する方法ですが、それだけだとシャローコピーになってしまうので、入れ子の配列やオブジェクトがある場合は新しく作成して値を設定します。
const array = [ { 'label': 'testA', 'date': new Date(), 'function': function() { console.log('a'); }, 'element': document.body, 'undefined': undefined } ]; const copy = array.map(function(element) { return {...element} }); copy[0].label = 'deepcopy'; console.log(copy); // [{label: 'deepcopy', date: Mon Jan 23 2023 01:20:36 GMT+0900 (日本標準時), element: body, undefined: undefined, function: ƒ}] // コピーした配列の値を変更しても、元の配列は変更されない console.log(array); // [{label: 'testA', date: Mon Jan 23 2023 01:20:36 GMT+0900 (日本標準時), element: body, undefined: undefined, function: ƒ}]
この場合、配列の内容に応じてmap()での処理を自前で実装する形になります。
ディープコピーのデモページ3
コメントが承認されるまで時間がかかります。