JavaScript のデータ型とデータ構造
プログラミング言語には、どれにも組み込みデータ構造がありますが、ふつうは言語ごとに異なります。この記事では、JavaScript で使用可能な組み込みデータ構造の一覧と、他のデータ構造の構築にも使えるように、それらがどのような性質を持ち合わせているかについて述べることにします。
言語概要では、一般的なデータ型を同様にまとめていますが、もっと他の言語との比較も行っています。
動的かつ弱い型付け
JavaScript は動的言語であり、動的型付けの言語です。JavaScript では、変数が直接的に特定のデータ型に関連付けられているわけではなく、どの変数にもあらゆる型の値を代入(および再代入)することができます。
let foo = 42; // foo は数値型になった
foo = "bar"; // foo は文字列型になった
foo = true; // foo は論理型になった
JavaScriptは弱い型付けの言語でもあります。これは、処理に不一致の型が含まれる場合、型エラーを発生させるのではなく、暗黙の型変換を可能にすることを意味しています。
const foo = 42; // foo は数値型
const result = foo + "1"; // JavaScript は、foo を文字列に変換し、他のオペランドと連結することができます
console.log(result); // 421
暗黙の型変換はとても便利ですが、想定外の変換が発生したり、想定外の方向(例えば、文字列から数値ではなく、数値から文字列)で変換が発生したりすると、微妙なバグを作成する可能性があります。シンボルと長整数については、JavaScript は意図的に特定の暗黙の型変換を禁止してきました。
プリミティブ値
オブジェクトを除くすべての型は、言語の最下層で直接表現される不変値を定義しています。これらの型の値を プリミティブ値 と呼びます。
null
を除くすべてのプリミティブ型は、typeof
演算子で確認することができます。typeof null
は "object"
を返すので、null
であることを確認するには === null
を使用しなければなりません。
null
と undefined
を除くすべてのプリミティブ型には、対応するオブジェクトラッパー型があり、そのオブジェクトはプリミティブ値の操作を行うために有用なメソッドを提供しています。例えば、Number
オブジェクトは toExponential()
などのメソッドを提供しています。プリミティブ値に対してプロパティでアクセスすると、JavaScript は自動的に値を対応するラッパーオブジェクトにラップし、代わりにそのオブジェクトのプロパティにアクセスします。しかし、null
や undefined
のプロパティにアクセスすると TypeError
例外が発生するため、オプショナルチェーン演算子を導入する必要があります。
型 | typeof の返値 |
オブジェクトラッパー |
---|---|---|
Null 型 | "object" |
なし |
Undefined 型 | "undefined" |
なし |
論理型 | "boolean" |
Boolean |
数値型 | "number" |
Number |
長整数型 | "bigint" |
BigInt |
文字列型 | "string" |
String |
シンボル型 | "symbol" |
Symbol |
オブジェクトラッパークラスのリファレンスページには、プリミティブ型そのものの意味づけの詳細な説明だけでなく、それぞれの型で利用できるメソッドやプロパティの詳細な情報が掲載されています。
Null 型
Null 型には、値が null
の 1 つしかありません。
Undefined 型
Undefined 型には、値が undefined
の 1 つしかありません。
概念的には、undefined
は 値 がないことを示し、null
は オブジェクト がないことを示します(typeof null === "object"
であることの説明にもなるでしょう)。言語では通常、値がない場合は undefined
が既定値となります。
- 値がない
return
文 (return;
) は、暗黙的にundefined
を返します。 - 存在しないオブジェクト プロパティ (
obj.iDontExist
) にアクセスするとundefined
が返されます。 - 初期化を伴わない変数宣言 (
let x;
) は、暗黙的にその変数をundefined
に初期化します。 Array.prototype.find()
やMap.prototype.get()
など、多くのメソッドは要素が見つからないとundefined
を返します。
コア言語では、null
が使用される頻度はかなり低くなっています。最も重要な場所は、プロトタイプチェーンの終わりです。その後、Object.getPrototypeOf()
、Object.create()
など、プロトタイプとやりとりするメソッドは undefined
に代わり null
を受け入れるか返します。
null
はキーワードですが、undefined
は通常の識別子で、たまたまグローバルプロパティであると見なされます。実際には、undefined
は再定義されたり、シャドウ化されたりすることはないので、この違いは小さいです。
論理型
数値型
数値型 (Number
) は IEEE 754 の倍精度浮動小数点形式の値です。2-1074 (Number.MIN_VALUE
) から 21023 × (2 - 2-52) (Number.MAX_VALUE
) の正の浮動小数点数、および同じ範囲の負の浮動小数点数の値が格納できるようになっていますが、安全に格納できるのは -(253 − 1) (Number.MIN_SAFE_INTEGER
) から 253 − 1 (Number.MAX_SAFE_INTEGER
) の範囲です。この範囲を外れると、JavaScript は整数を安全に表現できなくなり、代わりに倍精度浮動小数点数の近似値で表現されます。数値が安全な整数の範囲内かどうかは Number.isSafeInteger()
を用いて調べることができます。
表現可能な範囲外の値は、自動的に次のように変換されます。
Number.MAX_VALUE
より大きな正の数は+Infinity
に変換されます。Number.MIN_VALUE
より小さな正の数は+0
に変換されます。- -
Number.MAX_VALUE
より小さな負の数は-Infinity
に変換されます。 - -
Number.MIN_VALUE
より大きな負の数は-0
に変換されます。
+Infinity
と -Infinity
は数学的な無限大と同じような振る舞いをしますが、若干の違いがあります。詳しくは Number.POSITIVE_INFINITY
と Number.NEGATIVE_INFINITY
を参照してください.
数値型には、複数の表現を持つ値が1つだけあります。0
は -0
と +0
の両方で表します(0
は +0
の別名です)。実際には、異なる表現にほとんど違いはありません。例えば、+0 === -0
は true
です。ただし、ゼロで割ったときには気づくことができるようになっています。
console.log(42 / +0); // Infinity
console.log(42 / -0); // -Infinity
NaN
("Nnot a Number") は、特殊な数値の一種で、演算操作の結果が数値として発生しない場合によく遭遇します。また、JavaScript で唯一、それ自身と等しくない値でもあります。
数値は概念的には「数学的な値」であり、常に暗黙のうちに浮動小数点`でエンコードされていますが、JavaScriptではビット演算子を提供しています。ビット演算子を運営する場合、最初の数値は 32 ビット整数に変換されます。
メモ: ビット演算子でビットマスクを使用すれば、 1 つの数値で複数の論理値を表現することも可能ですが、 JavaScript は(論理型の配列や名前付きプロパティに論理値が割り当てられたオブジェクトのような)論理値の集合を表現する手段を提供しているため、この行いは悪い習慣として見なされています。ビットマスクはコードの可読性、わかりやすさ、保守性を大きく損ないます。
ローカルストレージの制限に対処しようとするときや、極端な用途(ネットワーク上の各ビットがカウントされる場合など)のように、非常に制約された環境では、このような技術を使用する必要がある場合があります。この技術は、サイズを最適化するために導ける最後の手段である場合にのみ考えることができます。
長整数型
長整数型 (BigInt
) は、任意の精度で整数を表現できる JavaScript の数値プリミティブです。長整数型を使えば、数値型で扱うことができる安全な整数の限界 (Number.MAX_SAFE_INTEGER
) を超える、大きな整数を安全に格納して操作することができます。
長整数型は、整数の末尾に n
を追加するか、 BigInt()
関数を呼び出すことで作成します。
この例は、Number.MAX_SAFE_INTEGER
をインクリメントすると期待される結果が返ってくることを示しています。
// 長整数型
const x = BigInt(Number.MAX_SAFE_INTEGER); // 9007199254740991n
x + 1n === x + 2n; // false。9007199254740992n と 9007199254740993n は等しくない
// 数値型
Number.MAX_SAFE_INTEGER + 1 === Number.MAX_SAFE_INTEGER + 2; // true。両方とも 9007199254740992
長整数型は、整数型と同じように +
, *
, -
, **
, %
などの演算子を使用することができます。唯一使えないものは >>>
です。長整数型は数値型と数学的な値が同じであっても厳密等価にはなりませんが、等価にはなります。
長整数値は、常に複数の値より正確であるわけでも、常に正確でないわけでもありません。長整数は小数値を表すことはできませんが、大きな整数をより正確に表すことができるからです。どちらの種類も他の種類を内包しておらず、相互に置換可能なものではありません。算術式で長整数値を通常の数値と混合した場合、または、暗黙的に変換しようとした場合、 TypeError
が発生します。
文字列型
文字列型 (String
) は、テキストデータを表し、UTF-16 コード単位 を表す 16 ビット符号なし整数値のシーケンスとしてエンコードさます。文字列の各要素は、文字列の中の位置を占めます。最初の要素は位置 0
にあり、次の要素は位置 1
にある、という具合になります。文字列の length は、その中の UTF-16 コード単位の個数で、実際の Unicode 文字数とは異なる場合があります。詳細は String
のリファレンスページを参照してください。
JavaScriptの文字列は不変です。つまり、一度文字列が作成されると、それを変更することはできません。文字列メソッドは、現在の文字列の内容に基づいて新しい文字列を作成します。例えば、次のような場面です。
substring()
を使用して元の文字列の部分文字列を作成する。- 2 つの文字列を、連結演算子 (
+
) またはconcat()
を用いて連結する。
「文字列に型付けした」コードに注意!
複雑なデータを表現するために文字列を使用したい思うこともあるでしょう。これには短期的なメリットがあります。
- 結合することで、複合文字列を簡単に作成できます。
- 文字列はデバッグしやすいです(出力される情報は常に文字列に含まれているものです)。
- 文字列は多くの API(入力フィールド、ローカルストレージの値、
fetch()
のResponse.text()
を使用したレスポンス、など)において共通分母であり、文字列だけで作業したいという誘惑に駆られることがあります。
規則さえあれば、どのようなデータ構造でも文字列で表現することが可能ですが、これは良い考えとは言えません。例えば、区切り文字を使用することでリストを模倣することができますが(JavaScript の配列の方が適しています)、残念なことに区切り文字がリストの要素で使用されてしまった場合、リストが壊れてしまいます。エスケープした文字を使用することでこの問題に対処することは可能ですが、その規則をすべてに用意する必要がある上、不必要なメンテナンスの負担を生み出します。
文字列はテキストデータには向いていますが、複雑なデータを表す場合は文字列を解釈し、適切な抽象化を用いる必要があります。
シンボル型
シンボル (Symbol
) は一意で不変のプリミティブ値であり、オブジェクトのプロパティのキーとして使用することができます。一部のプログラミング言語では、「アトム」と呼ばれています。シンボルの目的は、他のコードのキーと衝突しないことが保証された固有のプロパティキーを作成することです。
オブジェクト
プロパティ
JavaScript では、オブジェクトはプロパティの集合として見ることができます。オブジェクトリテラル構文は、初期化される限定された一連のプロパティです。その後でプロパティは追加したり削除したりすることができます。プロパティのキーは、文字列またはシンボルのどちらかです。他の型(数値など)を使用してオブジェクトに索引を付ける場合、値は暗黙的に文字列に変換されます。プロパティの値は、他のオブジェクトを含め、どのような種類の値でもよいので、複雑なデータ構造を構築することが可能です。
オブジェクトプロパティには、データプロパティとアクセサープロパティの 2 種類があります。それぞれのプロパティには、対応する属性があります。それぞれの属性は、JavaScript エンジンが内部でアクセスしますが、Object.defineProperty()
で設定したり、Object.getOwnPropertyDescriptor()
で読み取ったりすることができます。様々なニュアンスについては、Object.defineProperty()
のページで詳しく解説しています。
データプロパティ
データプロパティは、キーと値を関連付けます。以下の属性で記述することができます。
value
-
プロパティの get アクセスによって取得される値です。JavaScript の任意の値を指定することができます。
writable
-
プロパティに代入することで変更可能かどうかを示す論理値です。
enumerable
-
プロパティが
for...in
ループで列挙可能かどうかを示す論理値です。列挙可能性が他の機能や構文とどのようにやり取りするかについては、プロパティの列挙可能性と所有権も参照してください。 configurable
-
論理値で、プロパティの削除、アクセサプロパティへの変更、属性の変更が可能かどうかを示します。
アクセサープロパティ
キーを、値を取り出したり保存したりするための 1 つまたは 2 つのアクセサー関数 (get
および set
) と関連づけるものです。
メモ: アクセサー プロパティ であり、アクセサー メソッド ではないことを認識することが重要です。関数を値として用いることで、JavaScript オブジェクトにクラスのようなアクセサーを表すことができますが、それはオブジェクトをクラスにするわけではありません。
アクセサープロパティには、以下の属性があります。
get
-
空の引数リストで呼び出される関数で、値への取得アクセスが行われるたびに、プロパティ値を取得します。ゲッターも参照してください。
undefined
にすることができます。 set
-
割り当てる値を格納した引数で呼び出される関数です。指定したプロパティを変更しようとしたときに実行されます。セッターも参照してください。
undefined
にすることができます。 enumerable
-
プロパティが
for...in
ループで列挙可能かどうかを示す論理値です。列挙可能性が他の機能や構文とどのようにやり取りするかについては、プロパティの列挙可能性と所有権も参照してください。 configurable
-
論理値で、プロパティの削除、アクセサプロパティへの変更、属性の変更が可能かどうかを示します。
オブジェクトのプロトタイプは、他のオブジェクトや null
を指しています。これは概念的にはオブジェクトの隠しプロパティで、一般的には [[Prototype]]
として表されます。オブジェクトの [[Prototype]]
のプロパティは、オブジェクト自身にもアクセスすることができます。
オブジェクトはアドホックなキーと値のペアであるため、マップとしてよく使用されます。しかし、人間工学、セキュリティ、パフォーマンスの課題がある場合があります。任意のデータを格納するためには、代わりに Map
を使用してください。Map
のリファレンスには、キーと値の関連性を格納するためのプレーンオブジェクトとマップの間のメリットとデメリットについてより詳しい議論が含まれています。
日付
日付を表現する場合は、JavaScript に組み込まれた Date
ユーティリティを使用するのが最適です。
添字付きコレクション: 配列および型付き配列
配列は、整数値をキーにするプロパティと length
プロパティの間に特殊な関係の存在する、標準オブジェクトです。
さらに、配列は Array.prototype
を継承しており、配列を操作するための便利なメソッドを提供しています。例えば、 indexOf()
(配列中の値の検索) や push()
(配列への要素の追加) などです。これにより、配列はリストや集合を表現するのに最適な候補となります。
型付き配列は、基盤となるバイナリーデータバッファーの配列風のビューを表現し、配列と同様の意味づけを持つメソッドを数多く提供します。「型付き配列」は Int8Array
、Float32Array
などの様々なデータ構造の総称である。より詳しい情報は型付き配列のページを調べてください。型付き配列は、よく ArrayBuffer
や DataView
と併用して使用します。
キー付きコレクション: Map, Set, WeakMap, WeakSet
これらのデータ構造は、オブジェクトへの参照をキーとして扱います。Set
と WeakSet
はオブジェクトの集合を表し、Map
と WeakMap
はオブジェクトに値を関連付けます。
自分で Map
や Set
を実装することもできます。しかし、オブジェクトは(例えば <
"less than" の意味で)比較することができず、エンジンもオブジェクトのハッシュ関数を公開していないので、検索性能は必然的に線形になります。これらのネイティブ実装(WeakMap
を含む)の検索性能は、一定時間に対してほぼ対数となります。
通常、DOM ノードにデータをバインドするには、オブジェクトに直接プロパティを設定するか、 data-*
属性を使用します。これらの手法は同じコンテクストで実行されるあらゆるスクリプトからデータの利用が可能であるため、不都合な面を持ち合わせていました。 Map
や WeakMap
を使うと、オブジェクトへのプライベートなデータバインドを簡単に行うことができます。
WeakMap
および WeakSet
では、キーとしてオブジェクトまたは未登録のシンボルのいずれかであるガベージコレクターで回収可能な値のみが許可され、キーがコレクション内に残っていても、キーが回収される場合があります。これらは、メモリー使用の最適化に固有の仕様として使用します。
構造化データ: JSON
JSON (JavaScript Object Notation) は JavaScript から派生した、汎用データ構造をもつ軽量なデータ交換形式であり、多くのプログラミング言語で使用されています。JSON は、異なる環境や言語間でも移行可能な普遍的なデータ構造を構築します。詳しくは JSON
を参照してください。
標準ライブラリーに含まれる他のオブジェクト
JavaScript には組み込みオブジェクトの標準ライブラリーがあります。オブジェクトの詳細については、リファレンスを参照してください。
型変換
前述のように、JavaScript は弱い型付け言語です。つまり、ある型の値を使用する際に、別の型が期待される場合でも、言語が適切な型に変換してくれる場合が多いのです。そのために、JavaScript では、いくつかの変換ルールを定義しています。
プリミティブ変換
プリミティブ変換処理は、プリミティブ値が期待されるものの、実際の入力する種類に強い希望がない場合に使用します。文字列、数値、長整数が同じように受け入れられる場合がほとんどです。例を示します。
Date()
コンストラクターは、Date
インスタンスでない引数を 1 つ受け取ります。文字列は日付文字列を表し、数値はタイムスタンプを表します。+
演算子は、一方のオペランドが文字列の場合、文字列の連結を行い、それ以外の場合は数値の加算を行います。==
演算子は、オペランドの一方がプリミティブで、もう一方がオブジェクトの場合、オブジェクトは入力する種類が決まっていないプリミティブ値に変換されます。
この操作は、値が既にプリミティブである場合、変換を行いません。オブジェクトは、その [Symbol.toPrimitive]()
("default"
をヒントとして)、valueOf()
、toString()
の順にメソッドが呼び出されてプリミティブに変換されます。プリミティブ変換では toString()
の前に valueOf()
が呼び出されますが、これは数値変換の動作と同様であり、文字列変換とは異なっていることに注意ください。
[Symbol.toPrimitive]()
メソッドは、存在する場合、プリミティブを返す必要があります。オブジェクトを返すと TypeError
になります。valueOf()
と toString()
については、一方がオブジェクトを返す場合、その返値は無視され、代わりにもう一方の返値が使用されます。どちらも存在しなかった場合、またはどちらもプリミティブ値を返さなかった場合は TypeError
が発生します。例として、以下のコードで説明します。
console.log({} + []); // "[object Object]"
{}
にも []
にも [Symbol.toPrimitive]()
メソッドはありません。{}
と []
は両方とも valueOf()
を Object.prototype.valueOf
から継承しており、これはオブジェクト自体を返します。返値がオブジェクトなので、これは無視されます。従って、代わりに toString()
が呼び出されます。 {}.toString()
は "[object Object]"
を返し、一方 [].toString()
っは ""
を返すので、結果はこれらを結合した "[object Object]"
となります。
プリミティブ型に変換する場合は、常に [Symbol.toPrimitive]()
メソッドが優先されます。プリミティブ型の変換は、一般に valueOf()
が優先的に呼び出されるため、数値の変換と同じように振る舞います。しかし、独自の [Symbol.toPrimitive]()
メソッドを持つオブジェクトは、任意のプリミティブ型を返すことができます。組み込みオブジェクトの中で、Date
と Symbol
オブジェクトのみが、[Symbol.toPrimitive]()
メソッドを上書きします。Date.prototype[Symbol.toPrimitive]()
は "default"
ヒントを "string"
であるかのように扱い、Symbol. prototype[Symbol.toPrimitive]()
はヒントを無視し、常にシンボルを返します。
数値変換
数値の型には数値型と長整数型の 2 種類があります。言語が数値か長整数かを具体的に指定する場合もあります(Array.prototype.slice()
は添字が数値でなければならないなど)。他にも、どちらかを許容し、オペランドの種類によって異なる処理を行う場合もあります。他にも暗黙の変換を許さない厳密な強制処理については、数値型への変換や長整数型への変換を参照して下さい。
数値変換は、数値変換とほぼ同じですが、長整数の場合はは TypeError
を発生させずにそのまま返す点が異なります。すべての算術演算子は、数値型と長整数型の両方がオーバーロードされているため、数値変換が行わわれます。唯一の例外は単項プラスで、これは常に数値型への変換を行います。
その他の変換
すべてのデータ型には、Null、Undefined、シンボルを除き、それぞれの変換処理があります。詳しくは、文字列への変換、論理型への変換、オブジェクトへの変換を参照してください。
お気づきかもしれませんが、オブジェクトをプリミティブに変換する経路は 3 つあります。
- プリミティブ変換:
[Symbol.toPrimitive]("default")
→valueOf()
→toString()
- 数値変換、数値型への変換、長整数型への変換:
[Symbol.toPrimitive]("number")
→valueOf()
→toString()
- 文字列変換:
[Symbol.toPrimitive]("string")
→toString()
→valueOf()
すべての場合において、[Symbol.toPrimitive]()
が存在する場合は、呼び出し可能でプリミティブを返す必要があり、valueOf
や toString
が呼び出し可能でないかオブジェクトを返さない場合は無視されます。この処理の終わりには、成功すれば結果がプリミティブであることが保証されます。結果として得られるプリミティブは、コンテキストに応じてさらなる変換が行われることがあります。
関連情報
- JavaScript Data Structures and Algorithms (Oleksii Trekhleb)
- Computer Science in JavaScript (Nicholas C. Zakas)