😀

Javascriptにおけるプリミティブ型とオブジェクト(参照型)の違い

2023/12/22に公開
2

Javascriptのデータ型にはプリミティブ型オブジェクト(参照型) があります。

以下はJavascript Primer - データ型とリテラルからの引用です。

プリミティブ型の値は、一度作成したらその値自体を変更できないというイミュータブル(immutable)の特性を持ちます。 JavaScriptでは、文字列も一度作成したら変更できないイミュータブルの特性を持ち、プリミティブ型の一種として扱われます。

一方、プリミティブ型ではないものをオブジェクト(複合型)と呼び、 オブジェクトは複数のプリミティブ型の値またはオブジェクトからなる集合です。 オブジェクトは、一度作成した後もその値自体を変更できるためミュータブル(mutable)の特性を持ちます。 オブジェクトは、値そのものではなく値への参照を経由して操作されるため、参照型のデータとも言います。

この説明は初見では意味不明かと思います。少なくとも私はそうでした。

プリミティブ型の特徴

letでの宣言

上述の引用において、特にプリミティブ型の「文字列も一度作成したら変更できない」という部分が最も???となりました。
たとえばletによる宣言を使うと以下のように書けるじゃん、と。

let a = "hoge";
a = "fuga";

しかし、この処理は "hoge"を"fuga"に書き換える処理ではありません。
そして、そもそも書き換えることは出来ません。その書き換え不可能性を「イミュータブルである」と言います。
出来ることは見に行く場所を変えること、この例でいえば、"hoge"とは別の場所に"fuga"を作成し、変数aが見に行く場所を"hoge"から"fuga"に変えることだけです。

プロセスとして書くと以下のような感じかと思います。

  1. "a"の作成
    最初に "hoge" という文字列がメモリ上のある場所(アドレス)に作成されます。この時点で、変数 a はこの文字列 "a" が格納されているメモリのアドレスを指します。

  2. "fuga"の作成
    次に "a = "fuga" とすると、新しい文字列 "aa" がメモリ上の別の場所(新しいアドレス)に作成されます。

  3. 参照の更新
    その後、変数 a の参照(つまり、変数 a が指し示すメモリ上の場所)は、元の "hoge" から新しく作成された "fuga" に更新されます。これにより、変数a は "fuga" という新しい文字列を指すようになります。

constでの宣言

上記はletでの宣言での処理でしたが、constについても触れておきます。
変数の基本みたいな内容なので説明の必要はないと思いますが、a="fuga"はTypeError: Assignment to constant variable.`となります。

const a = "hoge";
a = "fuga";

上述の説明を踏襲すれば、変数aが見に行く場所は"hoge"から"fuga"に変えることはできません、ということです。

オブジェクト(参照型)の特徴

プリミティブ型とは異なり、オブジェクト(参照型)は、上記引用の通り値そのものではなく値への参照を経由して操作されます
この表現の仕方も、初めて触れる場合小難しく感じるかと思いますが、プリミティブ型のconst宣言との違いを確認することから始めたいと思います。

例えば配列をconstで変数宣言しても要素追加することができます。
要素を追加しても特にエラーは起きません。

const arr = [1, 2, 3];
arr.push(4); 

以下プロセスです。

  1. 配列の作成
    最初に [1, 2, 3] という配列がメモリ上のある場所に作成されるます。この時点で、constで宣言された変数 arr はこの配列が格納されているメモリのアドレス(参照)を指します。

  2. 配列の変更(push メソッドを使用)
    arr.push(4) を実行すると、既存の配列 [1, 2, 3] に新しい要素 4 が追加され、配列は [1, 2, 3, 4] になります。ここで重要なのは、メモリ上の配列の内容が変更されることですが、配列自体のメモリ上のアドレス(参照)は変更されません。

プリミティブ型の値の場合は、変数は直接その値が格納されている場所を見に行くという形でしたが、オブジェクト(参照型)というのは、変数が見に行った場所には実際にデータが置いてある場所のアドレスが格納されています。そのアドレスを元に実データにアクセスします。

配列をconst宣言した場合、何を変更できないかというと、その実際にデータが置いてある場所のアドレスです。一方、実際のデータは書き換えることができます。
この実際のデータである配列の中身を書き換えることが可能であることをミュータブルであると呼びます。

配列の比較

今見てきた通り、変数には実際の配列が格納されているアドレスが格納されており、配列を比較する際にはそのアドレスが比較します。

以下のように書くと1行目の["ichiro"]と2行目の["ichiro"]は異なる場所に作成され、arr1とarr2の中身は全く同じにも関わらず参照先のアドレスは異なるので、比較するとfalseとなり、"different"がコンソールに表示されます。

const arr1 = ["ichiro"];
const arr2 = ["ichiro"];
if(arr1 === arr2){
  console.log("same");
} else {
  console.log("different"); //こちらが出力される
}

そして、以下はTypeError: Assignment to constant variable.となります。

const arr1 = ["ichiro"];
const arr2 = ["ichiro"];
arr2 = arr1;

letでの宣言

let宣言をした場合はarr2=arr1とう代入はエラーになりません。

const arr1 = ["ichiro"];
let arr2 = ["ichiro"];
arr2 = arr1;

if(arr1 === arr2){
  console.log("same"); //こちらが出力される
} else {
  console.log("different");
}

letの場合は、変数に格納されている実際のデータのアドレスを書き換えることができます。
そして、もちろん実際のデータそのものも書き換えることができます。

二つのデータ型の処理における共通点と異なる点

上記をまとめておきます。

  • 共通する点
    プリミティブ型もオブジェクト(参照型)も、変数を宣言した際に、その変数は特定のアドレスに参照するようになる。(見に行く場所が決まる)

  • 異なる点

    • プリミティブ型 : 変数が参照するアドレスには「値」が直接格納されている。(変数 -> 実データ) (※すぐ下にて説明します)
    • オブジェクト型 : 変数が参照するアドレスには、実際のデータが格納されている「アドレス」が格納されている。(変数 -> 実データのアドレス -> 実データ)

<上記打ち消し線部分についての説明>

コメントでいただきましたが、「値」が直接格納されているかは仕様としては保証していません。
(開発する上プリミティブがどう実装されているかを意識する必要はありませんが、記述としては間違っていました)

仕様として保証されている(MDN - Primitive)のは以下となります。

  • イミュータブルである。
  • メソッドを持たない
  • プロパティを持たない
  • プロパティやメソッドはもたないが、持っているように振る舞う。それはラッパーオブジェクトを通じて行う。

たとえば、以下が成立するのは、ラッパーオブジェクトによります。

let greeting = 'hello';
console.log(greeting.length); // 5 lengthプロパティにアクセス出来てように振る舞う
console.log(greeting.toUpperCase()); // HELLO toUsserCase()メソッドを利用出来ているように振る舞う

おまけ constの意味

上記を踏まえると、constによる定数宣言は一般的な「定数」という意味ではなく厳密な定義を確認しておいた方がよさそうです。

mdn - constでは以下のように記載されています。

const 宣言は、値への読み取り専用の参照を作成します。これは、定数に保持されている値は不変ではなく、その変数の識別子が再代入できないということです。たとえば、定数の中身がオブジェクトの場合、オブジェクトの内容(プロパティなど)は変更可能です。

「識別子が再代入できない」というのがなんとなく分かりづらい気がしますが、上記を踏まえてパラフレーズしてみようと思います。

constで宣言した変数は、プリミティブ型の場合は実際のデータそのものを指し、オブジェクト(参照型)は実際のデータが格納されているアドレスを指します。

識別子が再代入できないというのは、その変数を宣言した時に代入したものを変えられない、という意味です。

つまり、プリミティブ型の場合はデータそのものを再代入できないのであり、オブジェクト(参照型)の場合は、参照先のアドレスを再代入できない、ということです。そして参照先のアドレスは再代入できないが、そのアドレスが指す具体的なコンテンツは編集可能であるということです。

これらのデータ型による違いは例えば、ReactのuseStateにおける挙動の違いとしてみることができます。
https://zenn.dev/unkeleven/articles/eee1af3ba049ab

以上となります。

Discussion

junerjuner

プリミティブ型 : 変数が参照するアドレスには「値」が直接格納されている。 (変数 -> 実データ)

仕様として保証されているのは イミュータブルであること と メソッドを持たず、メソッドはラッパーオブジェクトを経由するところまででは……?実装が値型であることは保証されていなかったと思われます。(実装が参照型であったとしても特殊なラッパーオブジェクトの挙動とイミュータブルを満たせれば同様の動作となる為

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

unk elevenunk eleven

おっしゃる通りかと。ECMAScriptもチラっと見てみましたが、値型で実装しなければならないとは書いてなかったです。後ほど記事を修正します。
ご指摘大変助かります。ありがとうございました!