JavaScript の object の copy で忘れないように
Contents
JavaScript の object のコピーは参照渡し
またしてもいまさらながら、JavaScript のオブジェクトのコピーは参照渡しだということは、忘れようもないことなのだけれど。
JavaScript の場合、文字列、数値、真偽値、null などはそれ以上単純化されないシンプルな値としてプリミティブ値とされ、そうじゃない、配列、連想配列、Date()、Function() などなど値を複数持つことができるものはみんなオブジェクトということ。ただし、null は typeof で object が出てくるんだな、これが。
で、そのオブジェクトを別の変数に代入したりなんかして、いわゆるコピーした場合には、そのコピーは参照渡しであるということは忘れてはいけないこと、というか忘れようもないことなのだけれど。
参照渡しというのは、実際の値が複製されるのではなく、そのオブジェクトへのアドレスがコピーされるということで、要はコピーされた変数が複数あっても、それらが紐付けられているメモリ上の対象は実は同じ一つのもの、ということだと。
まぁ、これは実際にやってみれば話が早くわかりやすいのである。
多次元の連想配列をつくって、それを別の変数へとコピーし、元の変数と複製された変数、それぞれに要素の値を変更してみると。
結果は下記のごとく当然のことながら、元々同じ一つのものに紐付けられているのだから、両方とも同じように変更されて同じ内容を表示するということになる。
でもって、これは連想配列に限ったことではなく、シンプルな配列でも全く同じ。同じオブジェクトなのだから。
配列のコピーにおける slice() と concat()
それなら、参照渡しではなく、値渡しによる完全な複製を作りたい場合はどうするかということになる。
とりあえず配列だけに限ってということであれば、すぐに slice() と concat() にたどり着く。
と、いうことで、slice() も concat() も希望通りの結果が得られる。
だが、しかし、これは一次元の配列とその要素に連想配列を含まない場合だけに限ってのことである。
MDN Web Docs の slice() と concat() のページを見ると、それら両方のページに下記のごとく同様の説明書きがある。
「元の配列を変更せず、元の配列から要素をシャローコピー (1 段階の深さのコピー) した新しい配列を返します。元の配列の要素は以下のように返される配列にコピーされます。
(実際のオブジェクトではない) オブジェクトの参照については、オブジェクトの参照を新しい配列にコピーします。元の配列も新しい配列も同じオブジェクトを参照します。参照されたオブジェクトが修正された場合、その変更は新しい配列と元の配列の両方に現れます。」
要は、完全な値渡しの複製としてコピーされるのは一次元のプリミティブな値だけということになる。さっそくやってみる。
一次元のプリミティブな値はそれぞれ独立しているが、一次元?の連想配列と二次元の配列の要素は共に連動して変更されてしまっている。
ちなみにもう一つ Array.from() を使う方法もあるとのことで、同じくして試してみたが、結果は3つとも全く同様だった。と、いうかこれはどれも内部的には同じことをやらせているのかもしれない。
で、当然のことながらこれら、slice()、concat()、Array.from() はもとより連想配列にはつかえない。
スプレッド構文(…)、Object.assign() ではいかに?
たしかスプレッド構文が使えるのは ES6 からだったと思うけれど、これでも配列のコピーはできたはず。ちなみにこれは連想配列でも使える。
が、MDN Web Docs のページを読んでいくと、「浅いコピー (プロトタイプを除く) の作成や・・・」という記述が目に入ってくる。
と、いうことでこれまた同じ結果ばかり続き、どちらにしても浅いコピーということになる。
そしてもう一つ、両方ともに使えるもので Object.assign({}, object ) がある。けれど、これもかのページの説明にも書いてあるけれど、やはり浅いシャローコピーとのことで、もしや!と思って試してはみたけれど、得られたのは全く同じ結果だけだった。おつかれさん。
JSON.parse( JSON.stringify()) か JQuery か lodash か
まぁ、これまでそんなに多次元の配列やらをコピーして使うということもなかったとは思うものの、いざ、できないの!ってことになるとなんとかそれが出来る方法を見つけたくなる。jQuery やら lodash なるライブラリを使えば出来るとのことなんだけれど、あまり外部のものをロードして使うというのは気が進まない。そうなると、選択肢はもう、JSON.parse( JSON.stringify()) しかなくなってしまうようだ。それはそれで良いのだけれど、なんだか見るからに重そうな感じがするし、若干、問題もあるとのことで。
ちなみに、Firefox においては、ver. 94 から、Chrome では ver.98 から structuredClone() というそれ用の関数を備えているそうで、試してみるとたしかに使えた。が、この記事を書いている 2021年12月 の時点では、それが使えるのは、Firefox だけである。
それにしても、現状において標準でそれができる関数があたりまえになっていないというのが、なんとも不思議な感じがする。必要とされてこなかったのだろうな~と。
まぁ、それが無いのだったら、いっそ自分で作ってしまおうかと思い、なんとかそれなりのものが出来たように思う。
基本的に、JSON.parse( JSON.stringify()) よりも速く動作しなければ意味がないので、そのあたりでちょっと時間も手間もかかってしまった。
なんてことはなく、渡されたデータが配列か連想配列かで分岐し、それぞれループさせ、その中でまた配列か連想配列が出てきたら再起させるというだけのもの。自分でいくつかテストした分においては、ちゃんと機能していたけれど、テスト数が少ないのでなにかしら不具合があるのかもしれない。
渡されたデータが配列であるか連想配列であるかを判定するのに、最もやりやすいのは Object.prototype.toString.call() を使う方法なのだろうと思う。これなら、typeof のようにどれもこれも object ではなく、配列なら [object Array] で連想配列なら [object Object] だし、Date なら [object Date] で、null なら [object Null] と、そんな具合にそれぞれ完全に判定することができる。ただ、実際にやってみたところ、これがなかなか重くなってしまう。ベンチで計測してみると、案の定、JSON.parse( JSON.stringify()) よりも鈍重になってしまったほど。
これでは自作する意味がなくなってしまうので、と、なんとかあれこれと調べては試してみる。
結果、この constructor を使う方法が一番軽くすむよう。実際に、渡す配列なり連想配列なりに、オブジェクトである、Date()、Function() などの、配列と連想配列以外のオブジェクトが含まれないと保証できる場合に限っては、typeof を使えば圧倒的に速くなった。まぁ、そこのところは、自分で使う分には状況によって使い分ければ済むことである。
しかし、それにしても思ったよりも簡単なものが出来てしまった。こんな簡単なものなら標準で備えていないわけがないと思われるので、やはりこれだと何かしら不足があるのだろうな~?と、いうわけで自分で使う分ならという前提のもの。
で、ちなみにベンチマークの結果はこんな感じ。それぞれ10万回のループ。きちんと回数を決めて計測して平均をだしたわけではないけれど、だいたいいつもこんな数値が出ていた。なお、使ったデータはというと、4次元まであり要素として配列を含む少し複雑な連想配列、シンプルな連想配列とそしてシンプルな配列の三つ。以下に示した。ちなみに、それぞれの配列に要素として含まれている変数 num はループにより変化させた。
clone_deep_obj | JSON.parse.stringify | structuredClone | ||
Firefox | objt | 240 | 317 | 519 |
Chrome | objt | 219 | 346 | 616 |
Firefox | objs | 50.2 | 134 | 262 |
Chrome | objs | 86.8 | 141 | – |
Firefox | arya | 20.8 | 50.4 | 184 |
Chrome | arya | 40.4 | 77.0 | – |
やはりというかあたりまえなんだろうけれど、連想配列は重いな~。
Chrome における structuredClone の値は、Chrome DEV ver.99 での値。
オブジェクトの場合の === 同値演算子
あと、もう一つ大事なことを忘れてはいけない。
オブジェクトの場合、同値演算子 === での判定においては、配列、連想配列にかかわらず、同じ参照アドレスを持っているもの同士ではないと true とはならない。たとえ、含んでいる要素が全く同じでも参照しているアドレスが異なれば false となる。
全く同じ値を持っている同士でも、false になってしまう感覚というのが、ちょっと妙な感じもするけれど、他の言語においてもこういうことはあるのだろうか。まぁ、php だとそんなことはないな。python はどうだったか。
Post : 2021/12/21 00:04
Comments feed
Trackback URL : https://strix.main.jp/wp-trackback.php?p=169389