世の中には色々なディープコピーがあると知ったのでまとめました。
ちなみにES2015念頭に書きました。
JSONのAPIを使ってみる
JSON.parse(JSON.stringify(obj))
メリット
簡単。
デメリット
Date
オブジェクト等色んなオブジェクトがうまくコピーできない。
例えば
const obj = {
d: new Date(),
u: undefined
}
obj
=> Object {date: Sat Dec 17 2016 22:48:56 GMT+0900 (JST), u: undefined}
const copyObj = JSON.parse(JSON.stringify(obj))
=> Object {date: "2016-12-17T13:48:56.350Z"}
ということで万能ではないことに注意しなければいけません。
今までなんにもかんがえず使っていたのは内緒です。
再帰的にコピーするのをやってみる
const deepClone = obj => {
let r = {}
for(var name in obj){
console.log(obj[name])
if(isObject(obj[name])){
r[name] = deepClone(obj[name])
}else{
r[name] = obj[name]
}
}
return r
}
メリット
JSON APIのような事態が発生することなく適切なオブジェクトをコピーできます。
デメリット
for in
実装に依存しているのでenumarable falseなプロパティは存在がなかったことにされてしまう。
例えば
const test = {
a: 1,
b: 2
}
Object.defineProperty(test, 'h', {
enumerable: false,
configurable: true,
writable: true,
value: 'hidden'
});
test //Object {a: 1, b: 2, h: "hidden"}
deepClone(test) //Object {a: 1, b: 2}
ちなみにこれはJSON API使用した場合でも発生します
よくよく調べてみるとenumarable: false
なプロパティはObject.assignでもコピーされないようなのであまり気にするなということだと勝手に解釈しました。
さてもう一つ問題があって実は上記のロジックではJSの様々なObjectTypeに耐えられません。
例えばDate
などのオブジェクトにも確実に対応するには以下のようなスクリプトになります。
const deepClone2 = obj => {
if (!(typeof obj === "object")) {
return obj
}
let cloneObj
let Constructor = obj.constructor
switch (Constructor) {
case Date:
cloneObj = new Constructor(obj.getTime())
break
default:
cloneObj = new Constructor()
}
for (let prop in obj) {
cloneObj[prop] = deepClone2(obj[prop]);
}
return cloneObj;
}
しかし実際対応すべき ObjectTypeはDateだけではなく
- Boolean object
- String object
- Date
- RegExp
- Blob
- Array
等々割と多いです。
自前で全てのパターンに対応するようなスクリプトを書くことは少しためらわれます。
必要最低限を用意しておくというのが現実解だと思いました。
jQueryのExtend関数を使ってみる
自前の実装は苦しそうなのでライブラリを使ってみます
jQuery.extend(true, {}, obj)
メリット
簡単。
デメリット
最近ではjQuery使わない状況も多いのでそんなに使えるものでもないのかもしれないです。
また前述と同じような問題?というかそもそもObject, Arrayしか対応していない仕様です。
ということで仕様を理解して使う分には問題なさそうです。
lodashを使ってみる
jQueryのextendのページにもしObject, Array以外をコピーしたいならlodashを見ればいいよとの記述があったのでlodashを使います。
_.cloneDeep(obj)
メリット
簡単。
The structured clone algorithm (後述)に基づいてコピーされるため前述のオブジェクト類に対応できます。
MDNによればThe structured clone algorithm はFunction, Error, DOMはディープコピーできず、lodashも同じようにディープコピーはできないようです。
デメリット
jQueryの場合と同じでライブラリ依存
The structured clone algorithm
The structured clone algorithmとはHTML5にて仕様を定義されている、オブジェクトをシリアライズする仕組みです。
参考のリンクにもあるようにいろんなオブジェクトタイプをコピーできる点でJSON APIより優れています。
これより下のディープコピーの例はこちらのアルゴリズムに沿ってコピーされます。
The structured clone algorithm は仕様上? Function, Error, DOM の要素がJSONにある場合にエラーを吐きます。
ここが少し注意するところです。
さてではこれをどう使うかというとMDNを眺める限りJSONのAPIのように直接呼ぶことができないようなので、オブジェクトのシリアライズを伴うAPIを使用することによって間接的に呼び出しディープコピーを行います。
PostMessageを使ってみる
MessageChannelは元々はコンテキストの違うJS同士でデータをやり取りするものです。このAPIはデータを渡す時にデータをシリアライズするのでデータをコピーできます。
もちろんWebWorkerでも同じことができますが、データのコピーだけが目的ならMessageChannelで十分だと思います。
const deepClone = obj => {
const channel = new MessageChannel()
const inPort = channel.port1
const outPort = channel.port2
return new Promise(resolve => {
inPort.onmessage = data => {
resolve(data.data)
}
outPort.postMessage(obj)
})
}
deepClone(obj).then(console.log)
メリット
非同期なので大きいデータでも安心して実行できます。
デメリット
同期で処理を行いたい場合には非同期であることがデメリットになると思います。
またぱっと見少し複雑かもしれません。
History APIを使ってみる
ブラウザのヒストリーを操作するAPIです。このAPIもデータをシリアライズしてくれます。
const deepClone = obj => {
const currentState = history.state
history.replaceState(obj, null)
const clone = history.state
history.replaceState(currentState, null)
return clone
};
メリット
同期で処理できる
デメリット
サイズ制限があるので640000以上の文字数は対応できません。
PostMessageと以上に一見するとこれがデータをコピーしている処理とは思いもしないのでその点で最もダーティーハック的です。
また無意味にhistoryにアクセスするオーバーヘッドが大きそうなのでやめておいたほうがいいのかなぁってなんとなく思ってます。
まとめ
本当は速度測定までやりたかったんですが、力尽きました。
今まではディープコピーを適当にやっていたのでもう少し考えつつ状況に応じて使い分けていきたいと思いました。
また文中で触れたenumarable false
なプロパティについては今回あげた例ではコピーできる方法がなさそうでした。
そういうものなのでしょうか。実務上なんら影響はありませんが少し疑問に思えました。
個人的にはライブラリ依存したくないのでPostMessageの方法が一番しっくりきました。データコピー用に使うのが正しいのかあまり確証は得られていませんが。
もっといい方法等ご存知でしたら教えてください。