🧊 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
(つづく)