JavaScript

[JavaScript]色々なディープコピー

More than 1 year has passed since last update.

世の中には色々なディープコピーがあると知ったのでまとめました。
ちなみに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でもコピーされないようなのであまり気にするなということだと勝手に解釈しました。

https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Properties_on_the_prototype_chain_and_non-enumerable_properties_cannot_be_copied

さてもう一つ問題があって実は上記のロジックでは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しか対応していない仕様です。

ということで仕様を理解して使う分には問題なさそうです。

https://api.jquery.com/jquery.extend/

lodashを使ってみる

jQueryのextendのページにもしObject, Array以外をコピーしたいならlodashを見ればいいよとの記述があったのでlodashを使います。

https://lodash.com/docs/4.17.2#cloneDeep

_.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より優れています。

https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm

これより下のディープコピーの例はこちらのアルゴリズムに沿ってコピーされます。

The structured clone algorithm は仕様上? Function, Error, DOM の要素がJSONにある場合にエラーを吐きます。
ここが少し注意するところです。

さてではこれをどう使うかというとMDNを眺める限りJSONのAPIのように直接呼ぶことができないようなので、オブジェクトのシリアライズを伴うAPIを使用することによって間接的に呼び出しディープコピーを行います。

PostMessageを使ってみる

MessageChannelは元々はコンテキストの違うJS同士でデータをやり取りするものです。このAPIはデータを渡す時にデータをシリアライズするのでデータをコピーできます。
もちろんWebWorkerでも同じことができますが、データのコピーだけが目的ならMessageChannelで十分だと思います。

https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage

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もデータをシリアライズしてくれます。

https://developer.mozilla.org/en-US/docs/Web/API/History_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の方法が一番しっくりきました。データコピー用に使うのが正しいのかあまり確証は得られていませんが。

もっといい方法等ご存知でしたら教えてください。