👀

jsの変数代入時の弊害とその対策をまとめてみた 〜ディープコピー lodash/cloneDeep〜

6 min read 6

はじめに

こんにちは、しゅんです

現場で開発中のアプリのweb serverのコードを読んでいると
「lodash/cloneDeep」というのをrequireしている箇所を見つけました。
パラメーターを使う箇所で呼び出していているのですが
何をしているのか検討がつかず、いろいろ調べました

ググってすぐに「ディープコピー」というのをするものらしい
とわかったのですが、ディープコピーをする目的がよく分からず。

思ったより調査が長引いたのでわかったことをまとめました

異業種転職して1年間sqlしか書かない現場にいて
今年からweb開発現場に勤務し始めた初学者です。
他にも変なことを書いてたら教えていただけると幸いです。

2020-01-08 追記・修正
コメントでご指摘をいただき内容を大きく修正しました。
ディープコピーに関係しているのはjsの「参照渡し」だと思っていましたが、
「参照渡し」も「値渡し」もjsには存在しないとのこと(c++にはある)
正しくは、「jsで変数に値を代入すると"値の参照"が変数に入る」ことが関係している、の様です。
間違った情報をそのままにせずに済みました。(他にも間違えた記載があるかもしれませんが、、)
Honey32さんありがとうございます!

ん?それは参照渡しではないの?と思われた方
c++の参照渡しはもっと複雑で同じものではないようです。
同じ名前を使うと混同してしまうのでやめましょう。
そのせいで数年間諸先輩方の論争が続いてしまったのだと思われます。。

jsの代入についての参考記事

JavaScriptに参照渡し/値渡しなど存在しない
とてもわかりやすいです。自分の勉強のために下に自分なりにまとめてみましたが、
jsの代入に関してはこちらの記事そのまま読む方がわかりやすいです。。

【Javascript】値渡しと参照渡しについてあらためてまとめてみる
→こちらはコメントでご指摘いただく前に参考にさせてもらった記事ですが
古い記事で内容が間違っていた様です

さて、いきなりややこしい内容になってしまいましたが
これから自分なりにわかりやすく整理したいと思います。

整理したいポイント

今回解決しないといけないポイントは以下の3つだと思います。

①jsで変数に代入した時、裏で何が起こっているのかを理解
  .jsで変数に「値」を代入すると"値の参照"が変数に入る
   A:変数に再度「値」を代入した時
   B:変数に「変数」を代入した時

②jsの変数代入特有の弊害を理解
・変数に「変数」を代入したあと、片方の変数に再度「値」を代入した時
・値が「オブジェクト型」のとき

③弊害の対策となるディープコピーを理解

jsで変数に代入した時、裏で何が起こっているのかを理解

jsで変数に「値」を代入すると"値の参照"が変数に入る

参考記事にはこう書いてありました。

var a = 10

というコードを実行すると、まず10という値が生成されます。値ができたのと同時に、その値をどこにしまったのかというメモである、"値の参照"ができ、その関係を図にするとこんな感じになります。そして変数にS1という"値の参照"が入ります。
"値の参照"という言葉を使いますが、これは例えるならたくさんある引き出しのどこに値をしまったかを表すメモのようなものです。

値の参照
S1 10

この記事を呼んで僕の脳内で流れた会話
多分こんな感じです

〜 代入する時 〜
僕「aに10代入したい」
変数管理おばさん「オッケー! 上から2段目右から3つ目のS1って書いてある引き出しに10入れとくね〜」
変数管理おばさん「引換券S1を持って帰って、必要になったら取りにおいで!」
僕「aに引換券S1を貼りつけこ」

〜 aをconsole.logする時 〜
僕「aに貼り付けてある引換券にはS1って書いてあるな」
僕「aをconsole.logして欲しいです」
僕「aに代入した時にもらった引換券はS1です」
変数管理おばさん「はいはい、S1ね、ええっと、S1はこの引き出しか。」
変数管理おばさん「10が入ってるわ!これconsole.logしとくねー!」

A:変数に再度、「値」を代入した時

var a = 10
a = 100
値の参照
S1 10
S2 100

再度値を代入した時、同じ引き出しの10と100を入れ替えるのかと思ってました。
別の引き出しを使うそうです...!
ここは驚きましたね。。
なんでそんなリッチな引き出しの使い方するのと。。

ややこしさの元凶はこいつだと思ってます。
このせいでこの先いろいろややこしいくなるんです。

僕の脳内の会話はこうです
僕「変数aを100に上書きしたい!引換券はS1です」
変数管理おばさん「10捨てるのもったいないなあ、となりの引き出しS2に100入れよか」
変数管理おばさん「S1の引換券返して! S2の引換券あげるから、aの値が必要になったらまた持ってきてー!」
僕:aに貼ってる引換券S1を新しくもらったS2に張り替える

B:変数に「変数」を代入した時

var a = 100
var b = a
変数 値の参照
a S2 100
b S2 100

変数aに変数bを代入すると
変数bにはaと同じ引換券(S2)だけを設定します
どんどんややこしくなりますねー

脳内会話
僕「変数aをbに代入したい!引換券はS2ですー」
変数管理おばさん「そんなんで新しい引き出しは使わんで...!」
変数管理おばさん「同じ引換券S2をbに貼り付けとき!!!」
僕「マジすか、、」

変数に「変数」を代入したあと、”代入元”の変数に「値」を代入した時

「A:変数に再度値を代入」と「B:変数に変数を代入」が組み合わさったバージョン
ややこしいさは増しますが、新たなルールはもうありません。
ABのルールだけ頭に入れておけば整理できます

var a = 100
var b = a
a = 1000
console.log(b) // 100

このときbが100のまま変わらない理由がもう説明できます。

bにaを代入した直後の状況

変数 値の参照
a S2 100
b S2 100

aに再度値を代入

変数 値の参照
a S3 1000
b S2 100

こんな感じですかね

bにaを代入した直後の僕の状況:
 aとbを持っていて、どっちにも引換券S2が貼ってある
僕 「変数aを1000に上書きしたい! 引換券はS2です!」
変数管理おばさん 「隣のS3に1000入れるから、aの引換券S2返して。S3あげる!!」
僕 「aにS3貼っとこう」

jsの変数代入特有の弊害を理解

値が「オブジェクト型」のとき

jsの変数代入方式が弊害となるケースがこれです。

var c = { val: 10 }
var d = c
c.val = 100
console.log(d) // { val: 100 }

cを上書きしただけなのにdも変わってる!?
変数上書きするときは新しい引き出し作られてないの!?
新しいルール!?
となるとおもいますが、新しいルールはありません。

この記事でこれまでに書いてきた内容は「変数へ代入」する時の話ですが
c.valに100を代入することは、「変数へ代入」ではないとのこと。

上書きしたのは変数cではなく、値として入っているオブジェクトの一部のプロパティ。
変数cにはオブジェクト1が入っていて中身が変わっても同じオブジェクトとみなすようです。

図にするとこう
c.val = 100終了後

変数 値の参照
c T1 オブジェクト1
b T1 オブジェクト1

変数管理おばさんは、オブジェクトの中身まではみません。
中身が変わっても同じオブジェクト1

流れはこんな感じ。

僕 「変数cにオブジェクト1を代入したい!」
変数管理おばさん 「T1にオブジェクト1しまっといたで!」

僕 「変数dにcを代入したい!」
変数管理おばさん 「T1の引換券dに貼っとき!!」

僕 (オブジェクト1の中身のvalを修正したいな)
僕 「変数cの中身みたい! 引換券はT1」
変数管理おばさん 「T1の引き出しにオブジェクト1入ってたで! 中身は知らんから確認してみ!」
僕 (オブジェクト1のvalを100に更新!)
僕 「おばちゃんありがとう!オブジェクト1返す!」
変数管理おばさん 「ほなまたT1にしまっとくな!」

=>cもdも、変わらずT1の引換券だから、cもdも変更されたオブジェクト1になる

オブジェクト型について

ここでいうオブジェクト型は、「プリミティブ型以外の全て」をさすそうです
なのでリストでも同様の動きになります。
リストの中身を変更しても新しい引き出しは生まれないので、
同じ引き出しの引換券を持ってる変数は全て変更されてしまいます。

関数のパラメータとしてオブジェクトを渡す時のケースも理解

function main () {
    let aaa = ["test", "test2"];
    let bbb = child(aaa);
    console.log(aaa);      //  => [ 'test3', 'test2' ]
    console.log(bbb);      //  => [ 'test3', 'test2' ]
}

function child (aaa) {
    bbb = aaa
    bbb[0] = "test3"
    return bbb
}

main();

上記ケースでは
親子関係の関数が2つ、子供が親で定義した関数aaaをパラメーターとして受け取っています。
子供の中で、aaaをbbbに代入し、bbbを上書きしてます
すると、aaaも上書きされてしまっているのがわかります。

このような意図しない変更が起きないような工夫が必要です!

弊害の対策となるディープコピーを理解(lodash/cloneDeep)

やっと本題です!
ディープコピーとは何か

こちらの記事がわかりやすいです!
JavaScriptでディープコピーしたい時

ディープコピーは、
新たな引き出しに中身が全く同じコピーを作ることだと理解しました。

子関数が受け取ったパラメータがオブジェクト型のデータの場合に
そのまま使わずにディープコピーしたものを利用すれば、
別の引き出しになるので、参照元が勝手に上書きされることはありません。

ディープコピーとは何か?についてはこちらがわかりやすいです
ディープコピーとシャローコピーの違い

そうじゃないコピーをシャローコピーっていうらしいです。

const clonedeep = require('lodash/cloneDeep');

function main () {
    let aaa = ["test", "test2"];
    let bbb = child(aaa);
    console.log(aaa);  // => [ 'test', 'test2' ]
    console.log(bbb);   // => [ 'test3', 'test2' ]
}

function child (aaa) {
    // bbb = aaa
    bbb = clonedeep(aaa) // ここでディープコピーした別のオブジェクトを代入している!
    bbb[0] = "test3"
    b = 0
    if (b == 0) {
    return bbb
    }
}

main();

lodash/cloneDeepというパッケージを利用すれば、
オブジェクト型のデータを簡単にディープコピーすることができます

別の実体なのでbbbは上書きし放題。aaaに何の影響もありません。

以上、lodash/cloneDeepってなんだ!?!?
から勉強した結果のまとめでした!