こんにちは、フロントエンドエンジニアの小松です。
入社以来、JavaScriptを書くことは日常茶飯事ですが、その中で謎現象に出会うたびに何度頭を抱えたことか…
そうした経験を記事にすることで同じ場面に遭遇した方々のお役に立てればと思い筆を執りました。
というわけで、今回はJavaScriptで変数にオブジェクトを代入したら参照先が同期されてしまう現象についてご紹介したいと思います!
目次
背景
とあるサービスのフロントエンド開発の中で、既存のオブジェクトを新規の変数に代入して、新たな変数に対して変更処理を行いました。
『いや、そもそもその設計自体どうなの?』感はありますが、自分の場合はvue.jsで変数に格納したオブジェクトをあちこちで使いまわしていたのです。
そうした中、変数Aを元に作成した変数Bに格納したオブジェクトを変更したら、変数Aのオブジェクトまで変更されていることに気がつきました。
その時自分はvue.jsを触り始めたばかりの時期で『$emitもv-modelもしていないのに親要素のdataと子要素のdataが同期されている!!偶然にもvue.jsをハックしてやったぜ!!』とテンションを上げつつ控えめのドヤ顔で同じチームの山本に見せたところ、冷静に諭されたので原因を分析することにしました。
再現
最近視聴した「けものフレンズ」に感銘を受けたので、以下「けものフレンズ」の内容を引用して説明していきます。
知らない方のために最低限の事前情報を記載しておきますmm
ストーリー
「動物が人間の姿をした「フレンズ」と呼ばれる少女たちが暮らすサファリパーク型動物園「ジャパリパーク」を舞台に、パークに迷い込んだ少女の正体や仲間がいる場所を知るための冒険と旅を描く」 wikipediaより引用
JavaScriptに置き換えるとしたら…
- オブジェクト = 各フレンズを構成する情報
- 変数 = 各フレンズの実体
前置きが長くなりました…
再現のために端折っていますが、簡単なサンプルを用意しました。
順を追っていくと…
{name: "かばんちゃん", type: "human, comment: "うわー食べないでくださいー"}
というオブジェクトを生成して、user1
に格納しました。
次にuser2
という新規変数にuser1
を代入して、user2
のオブジェクトの中身を変更しました(かばんちゃんを元にサーバルちゃんを作りました)
これでサーバルちゃんとかばんちゃんが出揃いましたので、ここで一つ、下記を実行してかばんちゃんから一声頂くことにしましょう。
え、あれっ?
かばんちゃんを呼んだつもりが、サーバルちゃんが呼び出されてしまった
原因
さて、なぜこのようなことが起きてしまったのか。
それは、JavaScriptのメモリと変数の参照渡しが原因でした!
以下、かなりざっくりとした感じで書いていきます。
(詳しい部分についてはかなり深堀して下さっている記事が出回っているので、仕様の詳細については割愛します)
{name: "かばんちゃん", type: "human, comment: "うわー食べないでくださいー"}
というオブジェクトを生成して、user1
に格納しました。
JavaScriptは生成されたかばんちゃんのオブジェクトをメモリ上に確保します。
その後user1
という変数に値を格納したメモリ領域の参照先を格納します。
つまり、user1
という変数の中にかばんちゃんがいるのではなく、あくまでかばんちゃんが住んでいる住所が入っているイメージです。
※ここでいう住所はプログラミングの世界では「ポインタ」と呼ばれています
その後…
次に
user2
という新規変数にuser1
を代入して、…
user2
にuser1
という変数を代入したと言いましたが、実際にはuser2
の中にuser1
に格納されているかばんちゃんの情報が格納されているメモリ領域のポインタを渡したということになります。
ということは、この時点でuser1
とuser2
は変数名こそ違えど、同じかばんちゃんの住所を参照していることになります。
そして最後に
…、
user2
のオブジェクトの中身を変更しました(かばんちゃんからサーバルちゃんを作りました)
つまりこれは、user2
の中に格納されている参照先に存在する値対して変更をかけていることになります。
結果的にuser2
はuser1
と同じアドレスを持っているので、user2
を変更した結果user1
に対して変更をかけてしまったということです。
なんということでしょう、例えるなら新しいサンドスター(=変数)を元にサーバルちゃんを生み出したつもりが、実はかばんちゃん自体を上書きしてしまっていたのです…!
対応方法
これでは知らずしてかばんちゃん自身を上書きしてジャパリパークに生まれてしまったサーバルちゃんが可哀想なので、この状態を回避する対応方法を書いておきます。
1. そもそも既存のオブジェクトを元に新しいオブジェクトを作らない
正直今回のサンプルみたいなケースは滅多にないかと思います。
大量のオブジェクト・変数を作る際はforやらeachやらで元データを回して作成するでしょうし、きちんと設計された実装であれば遭遇することはないかと思います。
ですが、自分のケースのようにフレームワーク上であちこちに共通の値としてオブジェクトを渡した結果、別の箇所で元dataを上書きしてしまうなんてこともあり得ますので、データの流れについてはオブジェクトに限らず気をつけておきたいところです。
2. オブジェクトを文字列にしてから再度オブジェクトにして格納する
同じアドレスを参照していようが、それを一度文字列にしてしまえば別物として扱えるというやや強引な発想です。
下記実行してもらえれば、無事かばんちゃんとサーバルちゃんが共存している様を確認できます。
3. 空のオブジェクトにマージする
こちらも上記と同じ結果になるのですが、jQueryのメソッドである$.extend
を使って、空のオブジェクトに対して元のオブジェクトをマージすることで回避できます。
4. 変数に新しく複合型のデータを代入する
難しく書いているようですが、要は他の変数を介さずに新規にオブジェクトをセットする方法です。
変数にはあくまでメモリ上のポインタが格納されているので、変数に対して新たにプリミティブなオブジェクトを渡してあげれば回避できます。
(実際こんな実装はしないと思いますが…)
あとがき
いかがでしたでしょうか、これはJavaScriptを長く書く人にとっては当たり前のようなことでも、初めて出会うとなかなか混乱する現象かと思います。
自分もまだまだ修行中のため誤った認識があるかもしれませんが、このように言語の根本となる仕様の部分への理解を深めることはとても大切だなと感じました。