🚚

JavaScriptの値渡しと参照渡しを解説

2023/07/02に公開
3

JavaScriptの値渡しと参照渡しを解説します。初心者にとって壁となる概念ですが図解を用いてなるべく分かりやすく解説してみます。

前提知識1:プリミティブ型とオブジェクト

値渡しと参照渡しの解説に入る前にJavaScriptのデータ型・プリミティブ型とオブジェクトの解説をします。JavaScriptは値の型によって値渡しか参照渡しが決まります。

プリミティブ型

プリミティブ型は下記の7種類あります。

  • 文字列
  • 数値
  • 長整数
  • 論理値
  • シンボル
  • null
  • undefined

プリミティブ型はメソッドとプロパティを持たない値となります。宣言した値を変更できない特徴があるため、イミュータブル(immutable)と表現されます。immutableは日本語で「不変」という意味になります。

https://developer.mozilla.org/ja/docs/Glossary/Primitive

オブジェクト

JavaScriptにおいてプリミティブ型でないものはオブジェクトになります。

  • オブジェクト(Object)
  • 関数
  • 配列
  • ラッパーオブジェクト
  • RegExp(正規表現)
  • Date(日時)
  • JSON

オブジェクトは宣言したプロパティやメソッドを後から変更出来るためミュータブル(mutable)と表現されます。mutableは日本語で「可変」という意味になります。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Data_structures

前提知識2:変数とメモリ

プログラム上で使用される値は全てメモリに保存されます。メモリ上にアドレスと呼ばれる16進数の数字で名前を割り当てて値を管理しています。

このメモリに保存されている値を取得する際に必要なのが変数です。変数名を指定すると値が取得出来るのは、変数がメモリのアドレスを保持しているからです

例えば変数aを宣言すると値0がメモリに保存されます。

const a = 0;

ブラウザのメモリには下記の図のように変数の値が保存されます。


※解説の簡略化のためアドレスを「番地」と表現しています。

注意点として、変数は値を直接保持しているのではなく、メモリのアドレスを保持しています。これは「変数はメモリのアドレスへの参照を保持している」と表現されます。

画面上では値が直接受け渡しされて置き換わっているように見えますが、裏側の動きとして変数はメモリのアドレスの参照を受け渡ししています。

このメモリに値を保存する挙動は値渡しと参照渡しを理解する上で非常に重要な概念となります。

値渡し・参照渡しとは

JavaScriptにおいてプリミティブ型の値は値渡しでオブジェクトは参照渡しとなります。この2つの具体的な違いは値をコピーした際の挙動にあります。それぞれの挙動について詳しく解説します。

値渡し(プリミティブ型)

プリミティブ型の値を変数aに代入し、変数aを変数bに代入したとします。

let a = 0;
let b = a;

この時メモリ上では以下の図のような処理が行われています。

注目すべき点は、変数aが参照しているメモリのアドレスがコピーされて、メモリ上の新しく追加された場所に保存されているところです。変数bはメモリ上に新しく追加された場所のアドレスの参照を保持しています。

プリミティブ型の値は別の変数に値が代入されるとメモリ上で、代入元の値のアドレスのコピーが作成されます。このため変数aと変数bは同じ値でもそれぞれ別のアドレスの参照を保持していることになります。

このため、変数aと変数bのどちらかの値を変更してもお互いに影響しあうことはありません。

let a = 0;
let b = a;
console.log(a, b); // 0 0

a = 1;
console.log(a, b); // 1 0

プリミティブ型の値がイミュータブル(immutable)と表現されるのはこの性質あるからです。

値が直接渡されるのではなく参照のコピーが渡されているので「値渡し」というよりは「コピー渡し」という表現の方が正しい気もしますが用語的に「値渡し」の方が広く浸透しているので当記事でも「値渡し」と表現しています。

参照渡し(オブジェクト)

オブジェクトの参照渡しの解説をします。
変数aにpropプロパティと値0を持つオブジェクトを代入し、変数bにコピーしています。

let a = {
  prop: 0
};

let b = a;

この時メモリ上では以下の図のような処理が行われています。

値渡しとの違いは、メモリ上で変数aのオブジェクトが保存されているアドレスのコピーでは無く、オブジェクト実体への参照がコピーされているところです。

オブジェクト実体への参照がコピーされるため、変数aと変数bは同じオブジェクトへの参照を保持している事となります

そのため、変数aか変数bのどちらか一方のオブジェクトのプロパティや値が変更されると変更をしていない変数が保持しているオブジェクトにも変更が反映されます

let a = {
  prop: 0
};

let b = a;

// 同じオブジェクトを参照している
console.log(a, b); // {prop: 0}

// 変数aのpropプロパティの値を変更
a.prop = 1;

// 変数bのオブジェクトも変更される
console.log(a, b); // {prop: 1} {prop: 1}

オブジェクトがミュータブル(mutable)と表現されるのはこの性質あるからです。

オブジェクトは値を後から変更出来てしまう性質があるため、状況によっては困る場面が発生します。これを防ぐためにシャローコピーとディープコピーと呼ばれる、参照元のオブジェクトをコピーするメソッドや構文があります。

※本筋から外れるため当記事でこれ以上触れません。興味がある方は調べてみてください。

状況別の値渡し・参照渡し

特定の状況下で値渡し・参照渡しがどのような挙動をするか解説します。

値の再代入

原則として、値の再代入を行った場合は、値渡しと参照渡しで挙動に違いはありません。なぜなら再代入が行われるとメモリ上に再代入された値が新しく保存され、アドレスが割り当てられるからです。

// プリミティブ型
let num = 0;
num = 1;

console.log(num); // 1

// オブジェクト
let obj = {
  prop: 0
};

obj = {} // 空のオブジェクト
console.log(obj); // {}

メモリ上では下記の図のような処理が行われています。

再代入が行われると、新しくメモリに保存されたアドレスへ参照が切り替わります。そして再代入前の値はJavaScriptエンジンによって自動的に削除されます。

またオブジェクトの再代入は状況によっては少々分かりにくい事があります。

再代入を行うと再代入された値へ参照先が切り替わりますが、再代入前に別の変数にオブジェクトのコピーを行なった場合、コピーされたオブジェクトに再代入による影響は出ません

let obj = {
  prop: 0,
};

// 変数refにobjオブジェクトをコピー
let ref = obj;

// 変数objに空のオブジェクトを再代入
obj = {};

console.log(obj); // {}
console.log(ref); // {prop: 0}

上記コードではobjオブジェクトを変数refにコピーした後に新しいオブジェクトを再代入していますが、変数refが持つオブジェクトに影響は出ていません。

これはコピーされた時点で渡されたのはコピー前の参照先・objオブジェクトであるためです。再代入を行ったとしても再代入前の値は削除されずにメモリ上に残ります。変数refはこの残っているobjオブジェクトの参照先を保持しています

オブジェクトは参照渡しのため、何らかの変更を行うとそのオブジェクトを参照している全ての箇所にも変更が適用されます。

ただ再代入や別の変数にオブジェクトをコピーしているとこのルール通りには行かない動きをする事があるため、注意が必要です。

定数const

変数をconstで宣言すると代入されている値を再代入して変更出来ません。そのためconstは「定数」と表現されることもあります。

// プリミティブ型
const num = 0;
num = 1; // エラー

console.log(num); // エラーのため実行されない

// オブジェクト
const obj = {
  prop: 0
};

obj = {} // エラー
console.log(obj); // エラーのため実行されない

const宣言は値が代入された時にメモリ上のアドレスの参照先を固定します。参照先を固定するので、再代入されてメモリ上に新しい値のアドレスが追加されてもそちらへ参照先を変更する事は出来ません

ただし、オブジェクトに関しては例外があります。オブジェクトの場合、オブジェクト自体の再代入は出来ませんが、プロパティやメソッドの変更は出来ます。

// オブジェクト
const obj = {
  prop: 0,
  method: function () {
    console.log("変更できます!");
  },
};

obj.prop = 1;
obj.method = function () {
  console.log("変更しました!");
};

console.log(obj);
/*
表示結果
{
  prop: 1,
  method: function() {
   console.log("変更しました!");
  }
}
*/

オブジェクトは、オブジェクト実体への参照とは別にプロパティの値への参照とメソッドの関数への参照が存在します。

const宣言はオブジェクトの実体への参照は固定しますが、プロパティやメソッドの参照は固定しないため、再代入による変更が可能です

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/const

関数の引数

関数の引数として変数を渡した場合に値渡しと参照渡しで挙動に変化はありません。

// プリミティブ型の場合
let a = 0;

function fn(num) {
  num = 1; // 変数aの値を変更
  console.log(a, num); // 0 1
}

fn(a);

// オブジェクトの場合
let b = {
  prop: 0
}

// プロパティの値を変更
function fn2(obj) {
  obj.prop = 1; // 変数bのオブジェクトのプロパティの値を変更
  console.log(b, obj); // {prop: 1} {prop: 1}
}

fn2(b);

// オブジェクトの再代入
function fn3(obj) {
  obj = {}; // 変数bのオブジェクトを変更
  console.log(b, obj); // {prop: 1} {}
}

fn3(b);

関数の引数はletで宣言された変数と同じように関数内で再代入や値の変更が出来ます。値が変更される場合その値の型で値渡し・参照渡しとなり挙動に違いが出ます。

オブジェクトの分割代入

分割代入はオブジェクトのプロパティやメソッドの名前を直接指定して変数に代入できる記法です。

const obj = {
  prop: 0,
  method: function () {
    console.log("こんにちは");
  }
};

// 分割代入でプロパティ名とオブジェクト名を直接指定することでそれぞれ値を取得
let {prop, method} = obj;
console.log(prop, method); // 0 function(){...}

この分割代入でオブジェクトから取り出した値を変更しても元のオブジェクトに影響はありません。なぜなら、分割代入を行うと変数には、メモリ上のプロパティやメソッドの値の参照のコピーが渡されるからです。挙動としては再代入同じです。

そのため、下記のコードを実行すると分割代入元のobj変数のオブジェクトは変更されません。

const obj = {
  prop: 0,
  method: function () {
    console.log("こんにちは");
  }
};

// 分割代入でプロパティ名とオブジェクト名を直接指定することでそれぞれ値を取得
let {prop, method} = obj;

// 値を変更
prop = 1;

// 関数を変更
method = function() {
  console.log("さようなら");
}

console.log(obj); // {prop: 0, method: function(){...}}
console.log(prop, method); // 1 function(){...}

これは関数の引数においても同じです。

const obj = {
  prop: 0,
  method: function () {
    console.log("こんにちは");
  }
};

function fn({ prop, method }) {
  // 値を変更
  prop = 1;

  // 関数を変更
  method = function () {
    console.log("さようなら");
  };

  console.log(obj); // {prop: 0, method: function(){...}}
  console.log(prop, method); // 1 function(){...}
}

fn(obj);

ただオブジェクトが入れ子になっている場合は、同じ挙動とはならないため注意しましょう。

const obj = {
  nest: {
    prop: 0,
    method: function () {
      console.log("こんにちは");
    }
  }
};

// obj変数のオブジェクトのnestオブジェクトを分割代入
let { nest } = obj;

// 値を変更
nest.prop = 1;

// 関数を変更
nest.method = function () {
  console.log("さようなら");
};

console.log(obj); // nest: {prop: 1, method: function(){...}}
console.log(nest.prop, nest.method); // 1 function(){...}

この場合、分割代入として渡ってくる値はnestオブジェクトとなり、変数nestが保持している値はnestオブジェクトの参照となります。そのため分割代入元となるobj変数に代入されているオブジェクトと同じ参照を持っていることになります

同じオブジェクトの参照を保持しているのでプロパティやメソッドを変更すると分割代入元のオブジェクトにも変更が反映されます。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment

まとめ

JavaScriptの値渡しと参照渡しについて解説しました。どの値を参照しているのかで返ってくる値が変わるため、普段から意識してコーディングすると良い訓練になると思います。

長い記事でしたが最後までお読み頂きありがとうございました。

Discussion

junerjuner

値渡し / 参照渡し と プリミティブ型 / オブジェクト型 は別の概念です。前者は関数の変数の渡し方の話で後者は型の話で全くの別です。

プリミティブ型は イミュータブルであって メソッドが生えていないことが保証されているだけであり、実装が参照型でないことは仕様として保証されていません。(値型であっても、参照型であってもイミュータブルである限りは同様の動作になる為