【JavaScript 入門講座】型とコピーのしくみを徹底解説!(プリミティブ型と非プリミティブ型)

はじめに

JavaScriptでは、すべてのデータは「プリミティブ型」か「非プリミティブ型」のどちらかに分類されます。
この分類は、データの格納・コピー・比較の挙動に大きく影響します。
本講座では、プリミティブ型と非プリミティブ型の違いを起点に、値渡し・参照渡し、シャローコピー・ディープコピー、そしてC言語との比較まで、幅広く丁寧に解説していきます。

🧱 第1章:プリミティブ型とは何か?

JavaScriptのプリミティブ型(Primitive Types)は、基本的なデータ型で、次の7種類があります:

  • number(例:42, 3.14, NaN, Infinity)
  • string(例:“Hello”, ‘A’)
  • boolean(例:true, false)
  • null(明示的な無)
  • undefined(未定義)
  • symbol(一意な識別子)
  • bigint(極めて大きな整数)

🔸 特徴:値としてコピーされる

プリミティブ型は**「値渡し」**です。つまり、変数に代入したり、関数に渡したりするとき、その値のコピーが渡されます

let a = 100;
let b = a;
b = 200;

console.log(a); // → 100(元の値は変わらない)
console.log(b); // → 200

この例では、baのコピーを持っているため、bを変更してもaには影響しません。

✅ 比較は「値」だけをチェック

let x = "apple";
let y = "apple";
console.log(x === y); // → true(内容が同じならtrue)

プリミティブ型は、中身の値が等しければ等しいと判定されます。参照先は無関係です。

📦 第2章:非プリミティブ型(オブジェクト型)とは?

JavaScriptにおける「オブジェクト型(非プリミティブ型)」には以下のようなものが含まれます:

🔹 オブジェクト型の例

  • 配列(Array
  • 関数(Function
  • オブジェクトリテラル({}
  • 日付(Date)など

これらはすべて「参照によって扱われる」という性質を持っています。

🔸 参照渡しの実態

オブジェクト型の変数には、「データそのもの」ではなく「データの場所(参照)」が格納されています。

let obj1 = { name: "Alice" };
let obj2 = obj1;
obj2.name = "Bob";
console.log(obj1.name); // "Bob"(obj1も変わってしまう)

obj2に代入されたのは、obj1の参照(メモリアドレス)であり、同じオブジェクトを指しているため、どちらを変更しても中身は同じです。

⚠️ typeof の挙動に注意

JavaScriptで値の型を調べる際、typeof演算子を使いますが、非プリミティブ型には注意が必要です。

typeof {}          // "object"
typeof []          // "object"
typeof null        // "object" ← これに注意!
typeof function(){} // "function"

特に null"object" と判定されるのは、JavaScriptの初期の仕様ミスの名残であり、現在でも修正されず残っています。

次章では、このオブジェクト型におけるコピー操作「シャローコピー」と「ディープコピー」について詳しく解説します。

🌀 第3章:シャローコピー vs ディープコピー

非プリミティブ型を扱う上で避けて通れないのが「コピーの問題」です。 オブジェクトや配列をコピーしたつもりが、思わぬ副作用を引き起こすことがあります。

🔸 シャローコピー(浅いコピー)

シャローコピーとは、オブジェクトの「第一階層」のみをコピーする方法です。

const original = { name: "Alice", scores: [100, 90] };
const shallow = { ...original };

shallow.name = "Bob";
shallow.scores[0] = 0;

console.log(original.name); // "Alice"(OK)
console.log(original.scores[0]); // 0(NG! 配列も共有されている)

オブジェクトの中にあるオブジェクト(配列含む)は参照のままなので、中身を変更すると元のオブジェクトに影響してしまいます。

Object.assign() もスプレッド構文(...)も、基本的にはこの挙動です。

🔹 ディープコピー(深いコピー)

ディープコピーでは、すべてのネストされたオブジェクトや配列も再帰的にコピーします。

方法①:JSON.parse(JSON.stringify(…))

const deep = JSON.parse(JSON.stringify(original));
  • 注意:関数や undefinedSymbol などは失われます。

方法②:structuredClone(モダンな方法)

const clone = structuredClone(original);
  • undefinedDate も扱えますが、非対応ブラウザでは使えません。

方法③:ライブラリを使う(lodash など)

import cloneDeep from 'lodash/cloneDeep';
const deep = cloneDeep(original);
  • 安定した挙動で、多くのケースに対応できます。

🔸 再帰的コピーとは?

ディープコピーの本質は、「中身の中身まで辿って、新しいメモリ領域に複製する」ことです。 再帰処理を使ってオブジェクトや配列を走査し、それぞれを新しく作り直す仕組みです。

次章では、こうした「型の判定」と「型安全な処理」の考え方について触れていきます。

🧠 第4章:C言語における類似概念

JavaScriptの参照渡しやディープコピーの考え方は、C言語におけるポインタ操作やメモリ管理と深い関係があります。

🔹 ポインタと参照の比較

C言語では、int* ptr のように明示的にポインタ(アドレス)を扱います。JavaScriptでは明示しませんが、オブジェクト型は内部的に「参照(ポインタ)」のように動作します。

int a = 10;
int* p = &a;
*p = 20;
// a の値は 20 に変更される
let obj = { value: 10 };
let ref = obj;
ref.value = 20;
// obj.value も 20 に変更される

このように「元のデータを別の場所から操作できる」という点で、JavaScriptの参照渡しとC言語のポインタは類似しています。

🔸 sizeof によるメモリサイズ

C言語では sizeof を使って型のメモリサイズを取得します:

printf("%lu", sizeof(int)); // 例: 4バイト

JavaScriptでは型サイズを直接取得する手段はありませんが、ブラウザやエンジンによって内部的にメモリサイズは確保されています。

🔸 アライメント(境界調整)

Cでは構造体のメンバ配置に「アライメント(境界調整)」が行われます。 これはCPUが高速にアクセスできるよう、適切な位置にデータを配置するための最適化です。

例えば:

struct S {
  char a;
  int b;
};

sizeof(S)sizeof(char) + sizeof(int) より大きくなることがあります。

JavaScriptではこのような最適化は抽象化されており、開発者が意識する必要はありません。

🔹 JavaScriptの抽象的なメモリ管理

C言語では malloc/free によるメモリ確保・解放が必須ですが、JavaScriptは**ガーベジコレクション(GC)**により自動でメモリ管理されます。 そのため、安全かつ簡易に変数を扱えますが、裏側では複雑な設計が動いています。

こうした背景を理解しておくことで、JavaScriptで思わぬバグやパフォーマンス低下を回避する力になります。


第5章:比較と代入の落とし穴

🔸 === 比較で混乱しやすい「値 vs 参照」

let a = { x: 1 };
let b = { x: 1 };
console.log(a === b); // false(別々のオブジェクト)

let c = a;
console.log(a === c); // true(同じ参照)

見た目が同じでも、オブジェクトは参照で比較されるため === では false になります。

🔹 配列・オブジェクトの代入時の落とし穴

const state = { count: 0 };
const copy = state;
copy.count++;
console.log(state.count); // 1(元も変わる)

これはVueやReactなどのUIライブラリで特に問題になります。状態管理で参照が共有されていると、思わぬ再レンダリングやバグの原因になります。

🔸 実際の開発で起きやすいバグ例(状態の破壊)

  • コンポーネントに渡した props を直接書き換えてしまう
  • Reduxのreducerで状態オブジェクトを直接変更してしまう
  • computedやwatch内でオブジェクトの中身を直接変更して副作用が起きる

✅ 回避するために

  • スプレッド構文や structuredClone() を使ってコピーする
  • immutable な設計思想を意識する
  • オブジェクトの「再利用」ではなく「再生成」を心がける

これで、JavaScriptにおける「型とコピー」の挙動が体系的に理解できたはずです。次の実践編では、実際の開発中に役立つユースケースやTipsを紹介していきます。