🧊 JavaScript のデータ型を体系的に理解する

モダンな Javascript の記述には、Javascript で扱えるデータ型に対する理解が重要です。

JavaScript のデータ型というと、

  • Number
  • String
  • Boolean

などを思い浮かべる方が多いと思います。

しかし実際の Web 開発では、それ以外にも様々なデータ型が利用されています。

データ型には大きく分けて プリミティブ型 と オブジェクト型 が存在します。

さらに ES6 以降は Map / Set などのキー・コレクション、そして Promise による非同期処理 が重要な柱になりました。

この記事では、JavaScriptでよく登場するデータ型を正しく理解するための基礎を整理します。


🧊 プリミティブ型

プリミティブ型は 値そのもの を表し、不変(immutable) です。代入するときは「値のコピー」が渡されます。


Number

JavaScript の Number は “整数型” でも “実数型” でもなく、1 種類だけの数値型です。

const a = 1;
const b = 1.5;
const c = Infinity;
const d = NaN; // 数値演算の失敗を表す特殊値

中身は IEEE 754 倍精度浮動小数点数(64bit) で表現されます。

つまり 整数も小数も同じフォーマットで表現しています。

そのため

  • 整数は 53bit までしか正確に表現できない
  • 小数は 2 進数で表せない値に誤差が出る

という特徴があります。

┌───────────┬──────────────────────┬──────────────────────────────────────────┐
│ 1 bit     │ 11 bits              │ 52 bits                                  │
└───────────┴──────────────────────┴──────────────────────────────────────────┘
  符号        指数                    仮数部(mantissa)

表現可能値

  • 最大値(最大有限値) この値を超えると Infinity (無限値)

→ 1.7976931348623157e+308( Number.MAX_VALUE )

  • 最小値(最小正の値) 0 に最も近い正の値

→ 5e-324( Number.MIN_VALUE )

※ JavaScript の Number は浮動小数点数なので、整数として誤差なく扱える範囲 はもっと狭いです。

  • 最大安全整数

+(2^53 - 1) = +9007199254740991 ( Number.MAX_SAFE_INTEGER )

  • 最小安全整数

-(2^53 - 1) = -9007199254740991 ( Number.MIN_SAFE_INTEGER )

👉 つまり、安全に扱える整数の範囲は、 -(2^53 - 1) 〜 +(2^53 - 1) となります。

console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991
console.log(Number.MAX_SAFE_INTEGER + 1); // 9007199254740992
console.log(Number.MAX_SAFE_INTEGER + 2); // 9007199254740992 ←誤差で同じになる

※ この問題を避けるために後述の BigInt が導入されました。

自動ラッピング

Number 型には、ラッパーオブジェクトとして、後述する Number オブジェクトが存在します。

処理時に、必要によりこのラッパーオブジェクトへの自動ラッピングが行われます。

これにより、下記のような Number オブジェクトのメソッドが使用可能です。

(123).toString(); // "123"
123..toString(); // "123"
123 .toString(); // "123"

Number 型は本来メソッドを持たないのですが、JavaScript エンジンが一時的に ラッパーオブジェクトである Number オブジェクトへラップ(boxing)して処理します。

処理後、そのオブジェクトは破棄されます。


String

文字列は UTF-16 ベースで、こちらも不変です。

const name = "Kitsune";
console.log(name.length); // 6

文字数制限

ECMAScript 仕様は「文字列の最大長」を定義していませんが、実際の JavaScript エンジンは

  • メモリ
  • ガベージコレクション
  • 内部表現(UTF-16)
  • 最適化戦略

などの理由で 実装上の制限を設けています。

エンジン毎の最大文字数例

エンジン 最大文字数(おおよそ)
V8(Chrome / Edge / Node.js) 約 1GB(≒ 10億文字)
SpiderMonkey(Firefox) 約 512MB〜1GB
JavaScriptCore(Safari) 約 500MB〜1GB

※ V8 の内部表現は UTF-16 のため、「約 1GB」というのは「約 5.3 億文字 × 2 バイト」の意味です。

👉 制限を超えると多くのエンジンで、 RangeError: Invalid string length といったエラーが発生しますので、注意が必要です。

テンプレートリテラル

ES6(ECMAScript 2015)で追加されたテンプレートリテラルは、文字列操作をスムーズにし、可読性が上がる仕組みです。

文字列を``で囲って記述します。${…} により変数や式を文字列に展開し埋め込むことが可能です。

const age = 20;
console.log(`Age: ${age}`);

${…} には、いろいろなものを埋め込むことができます。

console.log(`Number: ${123}`);        // "Number: 123"
console.log(`Boolean: ${true}`);      // "Boolean: true"
console.log(`Null: ${null}`);         // "Null: null"
console.log(`Undefined: ${undefined}`); // "Undefined: undefined"
console.log(`Array: ${[1, 2, 3]}`);   // "Array: 1,2,3"
console.log(`Object: ${{ a: 1 }}`);   // "Object: [object Object]"

自動ラッピング

String 型には、ラッパーオブジェクトとして、後述する String オブジェクトが存在します。

処理時に、必要によりこのラッパーオブジェクトへの自動ラッピングが行われます。

これにより、下記のような String オブジェクトのメソッドが使用可能です。

const s = "kitsune";
console.log(s.toUpperCase()); // "KITSUNE"

String 型は本来メソッドを持たないのですが、JavaScript エンジンが一時的に ラッパーオブジェクトである String オブジェクトへラップ(boxing)して処理します。

処理後、そのオブジェクトは破棄されます。


Boolean

真(true) / 偽(false) を示す 2 値をもつ論理値の型です。

let isReady = true;

論理値は、真:1、偽:0 と表現されることも多く、boolean は数字で保管されているイメージがありますが、必要に応じて数値に変換されて 1 / 0 になるだけです。

ECMAScript の仕様では、Boolean は次のように定義されています:

  • true / false の 2 値だけを持つプリミティブ
  • 内部的には「論理値(logical value)」として扱われる
  • 数値やバイナリとしての保存形式は仕様に書かれておらず、「実装依存」

自動ラッピング

Boolean 型には、ラッパーオブジェクトとして、後述する Boolean オブジェクトが存在します。

処理時に、必要によりこのラッパーオブジェクトへの自動ラッピングが行われます。

これにより、下記のような Boolean オブジェクトのメソッドが使用可能です。

const b = true;
console.log(b.valueOf()); // true
console.log(Number(b));   // 1
console.log(b.toString()); // "true"

Boolean 型は本来メソッドを持たないのですが、JavaScript エンジンが一時的に ラッパーオブジェクトである Boolean オブジェクトへラップ(boxing)して処理します。

処理後、そのオブジェクトは破棄されます。


BigInt

ES6 以降の 2019年に追加された、Number で扱えない非常に大きな整数を扱うための型です。

const big = 123456789012345678901234567890n;

末尾にnを付加して指定すると BigInt として認識されます。

安全整数の限界を超える例

console.log(9007199254740993);   // 9007199254740992 ←誤差
console.log(9007199254740993n); // 9007199254740993n ←正確

👉 Number では、誤差が発生しますが、BigInt を使用すると正確です。

内部構造

BigInt は 任意精度整数(Arbitrary Precision Integer) です。

内部的には、巨大な整数を「複数の機械語ワード(32bit or 64bit)」の配列として保持する可変長の仕組みです。

word[0] = 下位64bit
word[1] = 次の64bit
word[2] = 次の64bit
      :

その為、演算処理は、Numberに比べ、桁数が増えるほど、線形〜準線形に遅くなります。

Number は CPU のハードウェア命令で高速に計算されますが、BigInt は桁ごとに処理するため、桁数が増えるほど遅くなります。

演算速度の目安:

桁数 速度 実用性
1〜100桁 ほぼ Number と同じ 実用的
100〜1000桁 少し遅い 実用的
1000〜10000桁 明確に遅い 場合による
10000〜100000桁 重い 数学用途
100000桁以上 極端に遅い 専用ライブラリ推奨

※ CPU の整数演算を 何回も繰り返して桁を処理する為、1000桁の加算は、1000回の桁処理が必要となります。

向いている用途

  • ID やハッシュ値の扱い
  • 暗号系の大きな整数
  • 64bit を超える整数の正確な計算
  • 金額(小数を使わない場合)
  • 桁数が大きいが「そこまで巨大ではない」整数

向かない用途

  • 浮動小数点
  • 物理計算
  • グラフ計算
  • 大量の BigInt を高速に処理する用途
  • 数十万桁以上の数学計算
  • JSON や Web API と直接やり取りする用途(BigInt は JSON に出せない)

Symbol

ES6(ECMAScript 2015)で追加された、一意の識別子(Symbol)や、説明文ごとに一意の共有識別子(Symbol.for)を作るための型です。

作成時には、パラメータとして「説明文」を指定できます。説明文は、 toString() 用のラベルとして使用されます。

const id = Symbol("project-id");
console.log(id); // Symbol(project-id)

外部から渡されるオブジェクトに、内部専用のプロパティを追加したい場合に、既存のプロパティと「衝突しない値」として使用されます。

const id = Symbol("project-id");
const user = {
  id: "001",
  name: "Kon"
};
user[id] = "p001";

console.log(user[id]);   // "p001"  ← Symbol プロパティの値
console.log(user["id"]); // "001"   ← 文字列プロアティの値

この2つは完全に別物です。

  • user["id"] は文字列 "id" プロパティ
  • user[id] は Symbol プロパティ

これにより、外部から渡されるオブジェクトに左右されない “内部専用のプロパティ” を安全に追加することが可能です。

👉 フレームワークやライブラリ作成時に、重宝します。

Symbol を利用したプロパティには、下記の特徴があります。

  • 外部の文字列プロパティと 絶対に衝突しない
  • JSON に出ない
  • Object.keys に出ない
  • ユーザーが文字列プロパティで上書きしても、Symbol プロパティは影響を受けない

※ ライブラリ内部の「隠しプロパティ」を作りたい場合に有効です。

これに加え、Symbol() で作成した Symbol は、下記の性質を持っています。

  • 呼び出される毎に毎回ユニークな識別子
  • ファイルごとに独立した管理
console.log(Symbol("id") === Symbol("id")); // false

同じ、説明文で作成した Symbol を共通化したい場合は、Symbol() の代わりに、Symbol.for() が使用できます。

Symbol.for() は、Symbol() と異なり、同一説明文で呼び出された場合、 新たに作成することはせず、既存の値を戻します。

console.log(Symbol.for("meta") === Symbol.for("meta")); // true

つまり、Symbol.for() で作成した Symbol は、下記の性質を持っています。

  • 説明文ごとに共有される識別子
  • JavaScript 全体で共通の管理

👉 main.js と sub.jsといった複数ファイル間でも、Symbol の共有が可能となります。


undefined

「値が設定されていない」状態を示す型です。

let x;
console.log(x); // undefined


null

「意図的に値がない」ことを示す型です。

let y = null; // 変数のクリア

変数がオブジェクトの場合、参照が解放されます。

let obj = {
  id: 1,
  data: "aaaaa"
};

obj = null;
console.log(obj); // null

(つづく)