🫥

JavaScriptとReactの比較の仕組みを学ぶ:プリミティブ型・参照型とuseState

2025/01/26に公開

プリミティブ型と参照型

JavaScriptにおける, プリミティブ型と参照型について解説します.

プリミティブ型

以下にプリミティブ型を示します.

  • string
  • number
  • bigInt
  • boolean
  • undefined
  • symbol
  • null

プリミティブ型はimmutableな値で、変更することはできません。ただし、変数に新しい値を再割り当てすることは可能です。

let str = "Hello";
str[0] = "P";

console.log(str[0]); // Hと出力される. 値を変更することはできないため, Pとは出力されない
console.log(str); // Hello

str = "Bye"; // 新しい値を割り当てることはできる
console.log(str); // Bye

オブジェクト(参照型)

プリミティブ型以外のものはすべてオブジェクトになります.

  • 関数
  • オブジェクト
  • 配列など

オブジェクトはmutableな値のため, 変更することができます.


// オブジェクト内の関数を変更する
let greet = {
  func: () => console.log("Hello"),
};
greet.func = () => {
  console.log("Hye");
};
greet.func(); // Hey

// オブジェクトのプロパティを変更する
let obj = {
  params: "hoge",
};
obj.params = "change";
console.log(obj); // { params: 'change' }

// 配列の要素を変更する
let arr = ["H", "e", "l", "l", "o"];
arr[0] = "P";
console.log(arr); // [ 'P', 'e', 'l', 'l', 'o' ]

関数や配列がオブジェクトであるか以下のようなコードで確認してみましょう.

function counter() {
  counter.count++;
}
counter.count = 0; // プロパティとしてカウンタを持つ

counter();
counter();
console.log(counter.count); // 2

let arr = [1, 2, 3];
arr.count = 3; // プロパティとしてカウントを持つ

console.log(arr.count); // 3

オブジェクトのコピーと比較

次に, オブジェクトのコピーと比較について見ていきましょう.

オブジェクトのコピー

オブジェクトをコピーすると, 参照がコピーされます.

const obj = {
  params: "hoge",
};

const copyOfObj = obj;
/* 参照がコピーされる. つまり, obj, copyOfObjは同じ値への参照を保持している(この場合{params:"hoge"}への参照). */
obj.params = "huga"; // paramsの値を変更

console.log(obj); // { params: 'huga' }
console.log(copyOfObj); // { params: 'huga' }
// 同じオブジェクトへの参照を保持していたので, 2つの変数は同じ値を出力する

// プリミティブ型のコピーは値そのものがコピーされる
let a = 1;
let b = a;
a = 2;
console.log(a); // 2
console.log(b); // 1


参照のコピー時のイメージです

オブジェクトの比較

オブジェクトの比較では変数が保持している参照同士の比較が行われます.
この場合, copyOfObj,objは同じ参照を保持しているので, 変数を比較したときに, trueが出力されます.

const obj = {
  params: "hoge",
};
const copyOfObj = obj; // 参照をコピー

console.log(obj === copyOfObj) // true

先ほど述べたように, オブジェクトでは参照同士の比較が行われるので, 以下のような中身が同じでも参照が異なる場合, 変数同士を比較すると, falseが出力されます.

const obj = {
  params: "hoge",
};
const sameObj = {
  params: "hoge",
};
console.log(obj === sameObj); // false

ReactのuseStateのセッター関数について

ReactのuseStateは, 内部で現在のstateの値と新しいstateの値を比較する際にObject.is()メソッドを使用しています. このメソッドは2つの引数を受け取り, 値が異なればfalse, 同じであればtrueを返します。===演算子と似ていますが, いくつか違いがあります.

Object.is()と===の違い

Object.is(), ===では符号付きのゼロNaNの扱いが違います.
===演算子では符号付きゼロの比較ではtrueになりますが, Object.is()ではfalseになります.
NaNの比較では, ===演算子の場合, falseになりますが, Object.is()ではtrueになります.

console.log(0 === -0); // true
console.log(Object.is(0, -0)); // false

console.log(NaN === NaN); // false
console.log(Object.is(NaN, NaN)); // true

参考:
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Object/is
https://ja.react.dev/reference/react/useState#setstate-caveats

配列やオブジェクトのstateを更新するときに新しい配列やオブジェクトを設定する理由

最後に, オブジェクトや関数でstateを更新するときに, 新しい配列やオブジェクト渡す必要があるのかを説明します. 以下のようなコンポーネントを例にします.

useState.jsx
const [student, setStudent] = useState({ name: 'Kato', age: 19 })
useEffect(() => console.log('render'), [student])

return (
    <div>
      <button onClick={() => setStudent(student)}>studentをそのまま, setStudentに渡す</button>
      <button onClick={() => setStudent({ ...student })}>
        studentをスプレッド構文で展開してから, setStudentに渡す
      </button>
    </div>
  )

1つ目のボタンを教えたときには再レンダリングされません.
この場合, 以下のような比較が行われます.
オブジェクト同士の比較になるので, 参照が比較されることになります. よって, この比較はtrueになり, 現在の値更新したい値は同じだと判断されて再レンダリングはされません.

Object.is(student(現在の値), student(更新したい値))

2つ目のボタンを押したときには, 再レンダリングされます.
2つ目のボタンが押されたときには以下のような比較が行わます.
この場合も, オブジェクトの比較になるので, 参照が比較されます. しかし, student{...student}では参照が異なります.
よって, 比較結果はfalseになり再レンダリングされます.

Object.is(student(現在の値), { ...student }(更新したい値))

以下の図のように, オブジェクトや配列は参照を持つため, 参照が同じかどうかで判定が行われます.

メモリの中のイメージ

Discussion