この記事はちゅらデータアドベントカレンダー初日の記事です。
アドベントカレンダーというと毎年けっこう気合の入った記事が目立つのでハードルが高いと感じてしまう人もいるかも知れない。せっかく初日なので「こんなんでいいんだよオラァ」とハードルを下げていきたいと思う。
今回は、JavaScript 経験がそこそこある人なら誰でも辿り着いたことがあるであろうあのディープコピー実装について話す。
ディープコピーとは?
「ディープコピーとは何か?」「ディープコピーとシャローコピーの違いは?」あたりについては 説明が面倒なので ググるとたくさん出てくるので割愛する。 → deepcopy shallowcopy - Google 検索
実装
言わずと知れた クソ ディープコピー実装がこちら。
const cheapDeepCopy = (obj) => JSON.parse(JSON.stringify(obj))
「JSON 文字列に変換して戻す」というだけの簡単アプローチなので何も説明する必要がない。誰でも読める。
あと cheapDeepCopy
って名前をつけておくとなんか語呂が良いし、雑実装であることも明示できて一石二鳥 (?)
また、TypeScript の場合はジェネリクスでいい感じにすると型も引き継げてよい。
const cheapDeepCopy: <T>(obj: T) => T = (obj) => JSON.parse(JSON.stringify(obj))
使用例
// 1. 適当な深いオブジェクトを用意する
const foo = { a: { b: { c: "yay!" } } }
// 2. ディープコピーする
const bar = cheapDeepCopy(foo)
// 3. コピー元オブジェクトを書き換えてみる
foo.a.b.c = "wow!"
// 4. コピー先オブジェクトに影響していないことがわかる
console.log(bar.a.b.c) //=> "yay!"
使ってはいけない例
「雑」「cheap」を謳っているからには決してマトモな実装ではない。
「数値」「文字列」「真偽値」「null」「配列」「(ピュアな)オブジェクト」以外の値 (つまり、JSON で表現できない値) が含まれていた場合は変換時に消滅するか形が変わってしまうため、このような用途ではマトモなディープコピー関数を使ったほうがよい。
const obj = {
undefined: undefined, //=> 消滅する
function: () => {}, //=> 消滅する
date: new Date(2022, 11, 1), //=> 文字列に変換される
}
cheapDeepCopy(obj) //=> { date: '2022-11-30T15:00:00.000Z' }
"マトモ" なディープコピー関数とは?
clone とか入れたら良いんじゃないでしょうか。(詳しくない)
もし immutable に state を更新したい用途であれば最近だと Immer なども見てみると良いかも。(詳しくない)
JSON 変換だと遅くない?
パフォーマンス気にする人はこんな雑なコード書かない。
そこまで遅いわけではない模様。
→ JavaScriptのディープコピー速さ比較 〜7つの手法/ライブラリを比べてみた〜 - Qiita
なぜ今更こんな話題を?
TypeScript がスタンダードになりデータ構造は TypeScript の型で十分に表現できるようになった現代ではもはや class はめっきり使わなくなってしまい、最近「このディープコピーって JSON.stringify
でよくない?」というケースが多くなってきたので。