ことの発端
ある日のこと、入りたての新人くんが質問をしてきました。
新人くん「typeof演算子で取得できる値が覚えられないですー泣」
やさしい先輩を演じている僕はちゃあんと教えてあげました。
ぼく「typeof演算子はね、プリミティブ型ならその型の文字列、オブジェクト型なら object が取得できるんだよー。」
ぼく「あ、でもね、null だけは特別で、プリミティブ型なのに object が返ってくるから注意しなねー(どやあ)」
新人くん「なるほど!ありがとうございます!」
お礼を言って去っていった新人くん。しばらくしてまたやってきました。
新人くん「あの後typeof演算子について、少し調べました。BigintとかSymbolとか自分知らなかったです。」
ぼく「お、その2つも大事だからちゃんと理解しておくんだよー。」
新人くん「はいっ!ありがとうございます!(輝く瞳)」
学習意欲が高い新人くんに感心する良い先輩を演じておりましたが、実は自分、お恥ずかしながら Bigint も Symbol も「それ何だっけ?」状態でした。
せっかく新人くんがくれた学びの機会なので、次聞かれたときはドヤ顔でこたえられるようにしっかり理解しておこうと思ったのでした。
typeof 演算子の復習
まずはtypeof演算子がどんな値を返すのか復習です。
console.log(typeof "hoge"); // "string"
console.log(typeof 100); // "number"
console.log(typeof true); // "boolean"
console.log(typeof undefined); // "undefined"
console.log(typeof null); // "object"
console.log(typeof {}); // "object"
console.log(typeof []); // "object"
console.log(typeof function(){}); // "function"
ゴチャゴチャになることがよくあるのは僕だけでしょうか。
console.log(typeof [])
なんか "Array"
とか取得できそうですができません。配列かどうかの判定にはArray.isArray
メソッドを使いましょう。
基本的には、 プリミティブ型はその型の文字列、オブジェクト型は "object" と覚えておけばいいと思います。配列はオブジェクト型なので 当然object
になるわけです。
ただし、null だけは "null"
ではなく、"object"
になります。おかしいだろと言いたいところですが、これはもう昔っからの仕様(バグ)ということらしいので、そういうものなんだと覚えましょう。
BigIntとは?
名前からも容易に想像できますが、number 型で扱えない大きい数値を表現するためのプリミティブ型です。ES2020で追加されました。
意識しなければならないケースが多くないので忘れがちですが、number型で扱える数値の範囲は±2^53 - 1
です。
正確には JavaScriptが正確に扱える整数の範囲が±2^53 - 1
ということです。これを超えると、計算結果が不正確になる可能性があります。
console.log(Number.MAX_SAFE_INTEGER) // 9007199254740991
console.log(Number.MIN_SAFE_INTEGER) // -9007199254740991
console.log(Number.MAX_SAFE_INTEGER + 1); // 9007199254740992
console.log(Number.MAX_SAFE_INTEGER + 2); // 9007199254740992 (正しくない!)
BigIntを使用することで、この制限を超えて任意の大きさの整数を正確に表現できるわけです。
BigInt型の作成方法
BigInt型の値は以下のどちらかの方法で作成できます。
- 数値の後に「n」をつける
- BigInt 関数を利用する
let bigInt1 = 1234567890123456789012345678901234567890n;
let bigInt2 = BigInt("1234567890123456789012345678901234567890");
console.log(bigInt1); // 1234567890123456789012345678901234567890n
console.log(bigInt2); // 1234567890123456789012345678901234567890n
console.log(typeof bigInt1) // "bigint"
console.log(typeof bigInt2) // "bigint"
上記コードで、BigInt関数のパラメータとして文字列を渡しているのは、number型の範囲を超える値だからです。
number型で±2^53 - 1を超える数値を扱った時点で正確さは失われるので、BigIntへの変換も正確におこなえないということですかね。
let bigIntIncorrect = BigInt(1234567890123456789012345678901234567890);
let bigIntCorrect = BigInt("1234567890123456789012345678901234567890");
console.log(bigIntIncorrect); // 1234567890123456778777190752404304697344n (正しくない)
console.log(bigIntCorrect); // 1234567890123456789012345678901234567890n (正しい)
BigInt型であれば、±2^53 - 1を超える数値であっても正確に扱うことが可能です。
const maxSafeInt = BigInt(Number.MAX_SAFE_INTEGER);
// 以下はすべて正しい値
console.log(maxSafeInt + 1n); // 9007199254740992n
console.log(maxSafeInt + 2n); // 9007199254740993n
console.log(maxSafeInt * 2n); // 18014398509481982n
BigIntの操作
算術演算
BigIntは基本的な算術操作が可能です。
let a = 10000000000000000000n;
let b = 99999999999999999999n;
console.log(a + b); // 109999999999999999999n
console.log(a - b); // -89999999999999999999n
console.log(a * b); // 999999999999999999990000000000000000000n
console.log(b / a); // 9n
console.log(b % a); // 9999999999999999999n
ただし、BigInt型とnumber型が混在した算術操作はできません。異なる型間で演算を行う場合は、型を揃える必要があります。
// BigInt型の変数
let bigIntValue = 100n;
// number型の変数
let numberValue = 100;
// BigIntとnumber型を混在させた算術演算
try {
let result = bigIntValue + numberValue;
} catch (error) {
console.log(error.message); // Cannot mix BigInt and other types, use explicit conversions
}
// number型をBigIntに変換して演算
let result1 = bigIntValue + BigInt(numberValue);
console.log(result1); // 200n
// BigInt型をnumber型に変換して演算(正確な値にならない可能性あり)
let result2 = Number(bigIntValue) + numberValue;
console.log(result2); // 200
比較演算
比較演算も可能ですが、異なる型間の比較には注意が必要ですね。
let bigInt = 500n;
let num = 500;
console.log(bigInt === BigInt(num)); // true
console.log(Number(bigInt) === num); // true
console.log(bigInt === num); // false
console.log(bigInt == num); // true
Symbolとは?
ES2015で導入されたプリミティブ型です。
こちらは名前からは少し想像しづらいので、MDNを確認すると以下のとおりでした。
Symbol データ型を持つ値は「シンボル値」として見ることができます。 JavaScript の実行時環境では、シンボル値は Symbol 関数を呼び出すことで生成され、動的に無名の一意の値を生み出します。シンボルはオブジェクトプロパティとして使用されることがあります。
Symbol (シンボル) - JavaScript | MDN
うーん、わかるようなわからないような・・・。
Symbolの作成方法
Symbol関数を利用して作成します。説明文を任意でつけることができます。
let sym1 = Symbol();
let sym2 = Symbol("説明");
console.log(typeof sym1); // "symbol"
console.log(sym1); // Symbol()
console.log(sym2); // Symbol(説明)
Symbolの特徴
同じ説明を持つSymbolを複数作成しても、それらは常に異なる値となります。
let sym1 = Symbol("hoge");
let sym2 = Symbol("hoge");
console.log(sym1 === sym2); // false
Symbolの用途
オブジェクトのプロパティキー
Symbolはオブジェクトのプロパティキーとして使用されることが多いです。これは他のコードと衝突しない一意のプロパティを作成するために便利です。
let mySymbol = Symbol("mySymbol");
let obj = {
[mySymbol]: "value"
};
console.log(obj[mySymbol]); // "value"
Symbolで作成されたプロパティキーは一意なので、例えば、他のライブラリやコードが同じオブジェクトを拡張しても、シンボルで作成したプロパティは影響を受けません。
ライブラリがオブジェクトにメタデータを追加したい際などに、そのプロパティ名がユーザーのコードと衝突しないようにするためにSymbolを使うようですね。
隠しプロパティの作成
Symbolを使うことで、他のコードからアクセスされにくいプロパティを作成できます。
もう少し具体的に言うと、Symbolで作成したプロパティは、Object.keys()
やObject.getOwnPropertyNames()
で列挙されません。
Object.getOwnPropertySymbols()
を使えば取得できます。
let secret = Symbol("secret");
let obj = {
normal: "normal value",
[secret]: "secret value"
};
console.log(obj["normal"]); // "normal value"
console.log(obj[secret]); // "secret value"
console.log(Object.keys(obj)); // ["normal"]
console.log(Object.getOwnPropertyNames(obj)); // ["normal"]
console.log(Object.getOwnPropertySymbols(obj)); // [Symbol("secret")]
あくまでも「アクセスしづらい」ですので、「アクセスできない」わけではないです。
おわりに
BigInt
、Symbol
どちらも調べればまだまだたくさんの情報がありますが、まずはそれぞれの基本を理解できたと思います。
今回新人くんとの会話から、とっても基本的な部分での知識の浅さを痛感することになりましたが、理解できていないことを知ることができた良い機会だったとポジティブに考えます。
JavaScriptは、「意外と理解できていないこと」や「わかったつもりになっている」 ことが多くなりがちな言語だと思います。
昔、ハイパーヨーヨープレイヤーの小暮宙太くんが言っていた 「基本が中途半端だとすべてが中途半端に終わるんだよね。」 という言葉を胸に、基本を大事に日々精進しようと思ったのでした。
※誤った知識や、理解不足な部分などがありましたらご指摘くださると助かります。
参考
BigInt - JavaScript | MDN
Symbol (シンボル) - JavaScript | MDN
JavaScriptのBigIntを勉強してみた
【JavaScript】プリミティブ型とオブジェクト型を理解したい
ECMAScript6にシンボルができた理由
JavaScriptのSymbolについて