JavaScriptの型

JavaScriptの型のおさらいをします。

JavaScriptは動的な言語ですが、型もちゃんとあります。ここでは、JavaScriptにはどんな型があるのか、他の言語の型と比べた場合、どんな注意点があるのかを説明します。

JavaScriptの型の種類

JavaScriptには、7つのプリミティブ型とオブジェクト型があります。

プリミティブ型

プリミティブ型は値を変更できない(=イミュータブル)型です。プリミティブ型は次の7つがあります。

  1. boolean型: trueまたはfalseの真偽値。

  2. number型: 00.1のような数値。

  3. string型: "Hello World"のような文字列。

  4. undefined型: 値が未定義であることを表す型。

  5. null型: 値がないことを表す型。

  6. symbol型: 一意で不変の値。

  7. bigint型: 9007199254740992nのようなnumber型では扱えない大きな整数型。

number型

PHPなどの言語では、数値について整数を表す型(int)と小数を表す型(floatやdouble)の2つの型を持ちます。Javaなどの言語では、整数型をさらに32ビットと64ビットに細分化する言語もあります。JavaScriptには、整数と小数を型レベルで区別するものはありません。どちらもnumber型で表現します。

number型は、IEEE 754の倍精度浮動小数です。64ビットのうち、52ビットが数値の格納に、11ビットが小数の位置に、1ビットが正負符号に使われます。正確に扱える数値は-(2^53 − 1)から2^53 − 1の間です。他言語の64ビット整数型の範囲より狭いので注意しましょう。

number型の範囲より大きい整数を扱う場合はbigint型を使うとよいです。JavaScriptに整数と小数を区別しないnumber型と、整数のみを表現するbigint型が不揃いに存在している理由は、bigint型がES2020であとづけされたためです。

小数の誤差

小数の計算には誤差が生じる場合があるので要注意です。たとえば、0.1 + 0.2は0.3になってほしいところですが、計算結果は0.30000000000000004になります。これはJavaScriptのバグではありません。

0.1 + 0.2 === 0.3 //=> false

number型はIEEE 754という規格に準拠していて、その制約によって生じる現象です。10進数の0.2は有限小数ですが、それを2進数で表すと0.0011...のような循環小数になります。循環小数は小数点以下が無限に続きますが、IEEE 754が扱う小数点以下は有限であるため、循環小数は桁の途中で切り捨てられます。その結果、小数の計算に誤差が生じてしまうわけです。これはちょうど、私達が円周率の計算を筆算するときの制約に似ています。円周率は3.141592...と無限に小数点以下が続きますが、時間も紙面も有限なため、ある程度の誤差は妥協して3.14に丸めて計算するかと思います。ちなみに、2進数で有限小数になる0.5や0.25などの数値だけを扱う計算は誤差なく計算できます。

0.5 + 0.25 === 0.75 //=> true

小数計算の誤差を解決するために、一度整数に桁上げして計算し、もとの桁を下げる方法が考えられます。整数の計算は誤差が生じないという特性に期待した方法です。たとえば、110円の消費税込価格を求める計算を考えてみましょう。110に1.1を掛け算すると、誤差が生じて121円ぴったりにはなりません。

110 * 1.1 //=> 121.00000000000001

そこで、110と桁上げした税率11を掛け算してから、10で割ってみます。すると、うまく計算できます。

110 * 11 / 10 === 121 //=> true

この方法を使う場合は、桁を戻した数値は小数になることがあり、その値には小数計算誤差問題が残り続けることに注意してください。

const price1 = 101 * 11 / 10; // 111.1
const price2 = 103 * 11 / 10; // 113.3
price1 + price2; // 224.39999999999998

小数計算の誤差問題を包括的に解決したい場合は、decimal.jsのような計算誤差がないパッケージを使うのも手です。

string型

Javaなどの言語では、ダブルクォートで文字列リテラル(String型)を表現し、シングルクォートで文字リテラル(char型)を表現するといったように、使うクォートで型が変わります。

一方JavaScriptでは、ダブルクォートでもシングルクォートでもstring型になります。この点はPHPと同じです。またバッククオート(``)を使ってもstring型になります。

// これらはすべてstring型
"Hello"
'Hello'
`Hello`

undefined型

undefinedは値が定義されていないことを表す型です。変数を宣言した際に、値を代入しない場合、undefinedが代わりに代入されます。

let value;
console.log(value); //=> undefined

undefinedと似たものにnullもあります。nullは値がないことを表す型です。この2つは同じではありません。

null === undefined //=> false

これは豆知識ですが、undefinedはリテラルではなく、ただのグローバル変数です。そのため、undefinedという変数をローカルスコープに定義することができます。

let undefined = "TEST";
console.log(undefined); //=> "TEST"

undefinedという変数を定義するのは、コードの読み手にとって意外性が増すばかりで、まったくもって有用性がないので実際のコーディングではやってはいけません。

オブジェクト型

プリミティブ型以外のものはすべてオブジェクト型です。オブジェクト型には、クラスから作ったインスタンスだけでなく、クラスそのものや配列、正規表現もあります。

プリミティブ型は値が同じであれば、同一のものと判定できますが、オブジェクト型はプロパティの値が同じであっても、インスタンスが異なると同一のものとは判定されません。

const value1 = 123;
const value2 = 123;
console.log(value1 == value2); //=> true
const object1 = { value: 123 };
const object2 = { value: 123 };
console.log(object1 == object2); //=> false

配列

JavaScriptの配列はオブジェクト型であるため、配列の中身が同じでも、オブジェクトのインスタンスが異なると==では期待する比較ができないので注意が必要です。この点はPythonのリストと同じです。

const list1 = [1, 2, 3];
const list2 = [1, 2, 3];
console.log(list1 == list2); //=> false

PHPでは配列(インデックス配列)は要素の内容で等価比較できますが、JavaScriptでは同じようにはできないので注意しましょう。

<?php
$list1 = [1, 2, 3];
$list2 = [1, 2, 3];
var_dump($list1 === $list2); //=> bool(true)

このような配列の中身を比べるための演算子やメソッドはJavaScriptにはないため、中身を比較したいときにはlodashのisEqualなどのパッケージを使うのがお勧めです。

typeof演算子

typeof演算子では値の型を調べることができます。

typeof true; //=> "boolean"
typeof 0; //=> "number"
typeof "Hello World"; //=> "string"
typeof undefined; //=> "undefined"
typeof null; //=> "object"
typeof Symbol(); //=> "symbol"
typeof 1n; //=> "bigint"
typeof [1, 2, 3]; //=> "object"
typeof { a: 1, b: 2 }; //=> "object"

typeof演算子で特筆すべきなのは、値がnullの場合です。typeof nullの演算結果は"null"ではなく"object"です。誤解が起きやすい部分なので注意しましょう。特に値がオブジェクトかどうかを判定したいときは、typeof null"object"になることを意識して書かないと思いがけない不具合になることがあります。

// まずい実装
function isObject(value) {
return typeof value === "object"; // valueがnullになる場合を考慮していない
}
isObject(null); // 戻り値がtrueになってしまう

typeof nullを考慮した実装は次のようになります。

function isObject(value) {
return value !== null && typeof value === "object";
}

型強制(type coercion)

JavaScriptにはデータ型がありますが、型が異なる2つの値に対し演算してもエラーにならない場合があります。たとえば、string型の"10"からnumber型の1を減算した場合、number型の9が計算結果として出てきます。

"10" - 1; //=> 9

これは型強制(type coercion)と呼ばれる仕組みがあるためです。型強制とは、型が異なる2つの値を処理するとき、暗黙的に別の型へ変換されることを言います。

上の例では、string型の"10"がnumber型の10に型強制された上で、- 1が演算されたため9が計算結果になるわけです。

型に厳しい言語では、型が異なる値同士の演算ができない言語もあるので、そのような言語に慣れている方は特に注意してください。

ちなみに、どんな型に型強制されるかは演算子によっても異なるので注意が必要です。たとえば、string型の"10"にnumber型の1を加算する場合は、string型の"101"が計算結果になります。これは、number型の1がstring型の"1"に型強制された上で、"10" + "1"の文字列結合の演算になるためです。

"10" + 1; //=> "101"