JavaScriptにおける配列やオブジェクトに格納されているオブジェクトは、実体ではなく、実体への参照(リファレンス)を保持しています。これはメモリ使用量の面で有利に働くこともありますが、同時に思わぬ落とし穴も存在します。

浅いコピーにご用心

コピーには浅いコピー(Shallow Copy)と深いコピー(Deep Copy)があります。浅いコピーは最上位オブジェクトのみを新しくしたもので、参照が必要な状況でよく使われます。

const listA = [{ name: 'Alice' }, { name: 'Bob' }]
const listB = [...listA]

listBlistAの浅いコピーです。中身はlistAのものとまったく同一で、メモリ上のアドレスも一致していますが、入れ物だけが別物です。ここでlistBの最初の要素の名前を変更してみましょう。

listB[0].name = 'Charly'
 
console.log(listA[0])  // Object { name: "Charly" }
console.log(listB[0])  // Object { name: "Charly" }

するとlistAの最初の要素の名前も変わってしまいます。この特徴は不便にも感じられますが、便利な局面もあります。

配列のすべての要素に対して処理を行いたいと考えているものの、メモリや実行時間等のなんらかの制約によって一度に処理しきれないことがあります。そうなるとある一定のサイズごと(チャンク)に分割して処理しなければなりません。こういうときには、少しずつ細切れにするための配列を浅いコピーで作ると便利です。

const CHUNK_SIZE = 100
 
function sendUserData(users) {
    let usersLeft = [...users]
    while (usersLeft.length > 0) {
        const chunk = usersLeft.slice(0, CHUNK_SIZE)
	    // ...処理...
        usersLeft = usersLeft.slice(CHUNK_SIZE)
    }
}

一方で、元のデータに対する変更を避けるために、まったくのコピーを作りたいこともあります。そういうときには、再帰的にコピーする深いコピーが必要です。

よくある深いコピー

あるオブジェクトをコピーする方法でよく紹介されているのは、JSONを使った方法です。

const listA = [{ name: 'Alice' }, { name: 'Bob' }]
const listB = JSON.parse(JSON.stringify())

これはコピーしたいデータが、JSONと互換性のあるデータ型で構成されている場合には有効な手段ですが、いつでも使える手段ではありません。

近年ではもう少し適用できるデータ型が広いstructuredClone()もあります。JSONを使った方法よりは適用できるデータ型の範囲は広くなりますが、やはり複雑な複合データ型やユーザー定義のデータには適用できません。結論として、深いコピーをするにはプログラマが定義しなければなりません。

コピーメソッドを作る

JavaScriptでは深いコピーをする一般的なやり方はありません。なので、ディープコピーが必要なオブジェクトに対しては自分で定義することになります。

class User {
    constructor(name, age) {
        this.name = name
        this.age = age
    }
 
    clone() {
        return new User(this.name, this.age)
    }
}
 
// 14歳のアリスを作成する
const a = new User('Alice', 14)
 
// そのコピーを作る(倫理的な問題はさておき)
const b = a.clone()

時と場合によっては、一部だけを変更したコピーを作りたいこともあります。そういうときには、オプションの一覧を引数として受け取るようにするといいでしょう。

class User {
    // ...省略...
	clone() {
	    return this.cloneWith()
	}
 
    cloneWith({ name, age } = {}) {
        return new User(
            name !== undefined ? name : this.name,
            age !== undefined ? age : this.age
        )
    }
    // ...省略...
}

第4回 処理を分割する