【Javascript】値渡しと参照渡しについてあらためてまとめてみる

  • 142
    いいね
  • 3
    コメント
この記事は最終更新日から1年以上が経過しています。

基本

var a, b;
a = 0;
b = a;
b = 5;
console.log(b); // 5
console.log(a); // 0 (aは変わらない)  

var c, d;
c = [0,1,2];
d = c;
d[0] = 5;
console.log(d) // [5,1,2]
console.log(c) // [5,1,2] (cも変わる)

値渡しと参照渡しについて理解されている方ならなんの問題もないですが、これは上のコードを見て ??????となった方向けの記事です。

結論から言うと、Javascriptにおいて プリミティブ型は値渡し、オブジェクト型は参照渡しとなります。

ざっくり言うと、値渡しはその値そのものの情報を別の変数に渡していて、参照渡しはメモリ上のどこを参照しているかの情報を別の変数に渡しているか、という違いですね。

ちなみに、Javascriptにおけるプリミティブ型とは

  • 数値型
  • 文字列型
  • ブーリアン型
  • null型
  • undefined型

の5つを指します。

これらの型については、最初のコードのa,bと似たような挙動を示します。

var a, b;
a = 'hoge';
b = a;
b = 'fuga';
console.log(b); // 'fuga'
console.log(a); // 'hoge' (aは変わらない)

最初のコードでは配列で示しましたが、連想配列でももちろん参照渡しとなります。

var a, b;
a = {x:0,y:0};
b = a;
b.x = 5;
console.log(b); // {x:5,y:0}
console.log(a); // {x:5,y:0} (aも変わる) 

こちらの勘違い

JavaScriptはオブジェクトについて参照渡しだなんて、信じない

コメントの方で既にさんざん突っ込まれていますが、ここで言われているこれ。

var a, b;
a = ["hoge", "fuga"];
b = a; // 参照渡し?
b = [];
b; // => []
a; // => ["hoge", "fuga"] ←!?!!?!???!?!?

これは結局、

b = [];

のところは

b = new Array();

という意味で、新しい参照先を作り出しているのでこういう挙動になるのはなんの不思議もありません。

あとの方にある

b = {};

これも、

b = new Object();

これですね。

勘違いしやすいところですが、別に中身を空にしているわけではない、と気づけば意味はわかるかと思います。

ちなみに、記事の最後にある

var a, b;
a = 0;
b = a; // 値渡し!
b.hoge = "hoge";

a.hoge; // => undefined

b.hoge; // => undefined

も、そもそも

var a;
a = 0;
a.hoge = "hoge";

a.hoge; // => undefined

なだけだったりするので、あまり値渡し参照渡しとは関係ところでのJSのわかりにくい部分の問題だったり。

オブジェクトを値渡ししたい場合

さて、それでもオブジェクトを値渡ししたい場面も出てくるかもしれません。
その場合は少々ハック的ですが、以下の様な方法が使えます。

配列の場合

var a, b;
a = [0,1,2];
b = a.concat();
console.log(b) // [0,1,2]

b[0] = 5;
console.log(b) // [5,1,2]
console.log(a) // [0,1,2] (aは変わらない)

concat()のもともとの用法ではないですが、空の要素を付け足した別の配列を作り出すイメージです。

もちろん、for文を使って以下の様な書き方が正しいかも知れませんが、

var a, b;
a = [0,1,2];
b = [];
for (var i=0,l=a.length;i<l;i++) {
    var val = a[i];
    b.push(val);
}

多次元配列だった場合などを考えて場合分けをすることを考えると、かなり面倒なことになってしまいます。

連想配列の場合

連想配列の場合はconcat()が使えません。
かと言って、for文を回すとなると、上記のネストの問題が出てきます。

あまりスマートではないかも知れませんが、下記方法が良いのではと考えています。

var a, b;
a = {x:0,y:0};
b = JSON.stringify(a); // JSON文字列化
b = JSON.parse(b); // JSON文字列化したものを戻す
console.log(b); // {x:0,y:0}

b.x = 5;
console.log(b); // {x:5,y:0}
console.log(a); // {x:0,y:0} (aは変わらない)

JSON文字列化して戻す。。
もっと良い方法あれば誰か教えてください。

あ、ライブラリの中には、jQuery.extend()とかAngular.copy()とかでできたりもするようですね。

おまけ

他の言語に慣れている方は「文字列がプリミティブ型」というのにちょっと戸惑われるかも知れません。

var a, b;
a = 'hoge';
b = a;
b = 'fuga';
console.log(b); // 'fuga'
console.log(a); // 'hoge' (aは変わらない)

a = 'hogehoge';
console.log(a); // 'hogehoge'
console.log(b); // 'fuga' (bは変わらない)

JSの言語仕様的には、 文字列値は文字列オブジェクトに暗黙的に型変換されるってのが正確なところみたいです。
かつ、 文字列値も文字列オブジェクトも文字列の内容を書き換えられない(破壊できない)不変型となっている、というのがポイントです。

var a;
a = 'hoge';
console.log(a[0]);  // 'h'
a[0] = 'x';
console.log(a); // 'hoge' ('xoge'にはならない)
var a, b;
a = 'hoge';
b = new String('hoge');

console.log(typeof a); // string
console.log(typeof b); // object

console.log(a == b); // true
console.log(a === b); // false

aとbの2つは別物となりますが、a.lengthとかした瞬間には文字列値がオブジェクトに暗黙に変換されて、、みたいなことが起こってたりもします。

このあたりの話で言うと、僕らのバイブルであるところの『Javascript: The Good Parts』には

new Boolean、new Number、new Stringを使ってはならない。同様に、new Objectとnew Arrayの利用も避けるべきである。

と明記されていますね。