今日の疑問
Q&A
JavaScript・TypeScriptのプリミティブ型はオブジェクトではないのに、なぜメソッドが使えるのか?
はじめに
JavaScriptやTypeScriptを学習している中で、以下のような疑問を持ちました。
プリミティブ型はオブジェクトではないのに、なぜメソッドを使用できるのだろう?
例えば、JavaScriptでは文字列に対して以下のようなメソッドを使用できます。
const text = "hello";
console.log(text.toUpperCase());
実行結果
HELLO
しかし、文字列はプリミティブ型であり、オブジェクトではありません。
const text = "hello";
console.log(typeof text);
実行結果
string
typeof の結果は object ではなく string です。
それでは、なぜオブジェクトではないプリミティブ型に対して toUpperCase() のようなメソッドを使用できるのでしょうか。
今回はこの仕組みについて調べてみました。
⸻
プリミティブ型とは
JavaScriptには、プリミティブ型と呼ばれる基本的なデータ型があります。
主なプリミティブ型は以下の通りです。
- string(文字列)
- number(数値)
- boolean(真偽値)
- undefined
- null
- symbol
- bigint
例えば、
const text = "hello"; // string
const number = 100; // number
const flag = true; // boolean
これらはオブジェクトではなく、プリミティブ値として扱われます。
⸻
オブジェクトではないのにメソッドが使える理由
結論から言うと、
JavaScriptが必要なタイミングで、プリミティブ値を一時的に対応するオブジェクトとして扱ってくれるためです。
例えば、以下のコードを見てみます。
const text = "hello";
console.log(text.toUpperCase());
text は string 型のプリミティブ値です。
しかし、toUpperCase() を実行する際、JavaScriptは内部的に一時的な String オブジェクトを生成します。
イメージとしては以下のような流れです。
"hello"
↓
一時的にStringオブジェクトとして扱う
↓
toUpperCase()を実行
↓
処理が終わると一時的なオブジェクトは不要になる
コードで表すと、以下のようなイメージになります。
new String("hello").toUpperCase();
ただし、実際に自分で new String() を書く必要はありません。
JavaScriptが内部的に自動で行ってくれます。
⸻
ラッパーオブジェクトとは
プリミティブ値をオブジェクトとして扱うためのオブジェクトを、ラッパーオブジェクトと呼びます。
代表的な対応関係は以下の通りです。
プリミティブ型 ラッパーオブジェクト
string String
number Number
boolean Boolean
例えば、
const text = "hello";
の "hello" はプリミティブ型です。
一方で、
const text = new String("hello");
は String オブジェクトです。
この2つは似ていますが、厳密には異なります。
⸻
実際に比較してみる
プリミティブ型の文字列と、String オブジェクトを比較してみます。
const primitive = "hello";
const object = new String("hello");
console.log(typeof primitive);
console.log(typeof object);
実行結果
string
object
このように、
"hello"
は string 型ですが、
new String("hello")
は object 型になります。
通常は new String() を使用する必要はなく、プリミティブ型の string を使用します。
const text = "hello";
メソッドを使用する際は、JavaScriptが必要に応じて一時的にラッパーオブジェクトとして扱ってくれるためです。
⸻
NumberやBooleanでも同じ
この仕組みは文字列だけではありません。
Numberの場合
const number = 123.456;
console.log(number.toFixed(2));
実行結果
123.46
number はプリミティブ型ですが、toFixed() というメソッドを使用できます。
これはJavaScriptが必要に応じて一時的に Number オブジェクトとして扱うためです。
Booleanの場合
const flag = true;
console.log(flag.toString());
実行結果
true
Booleanについても同様に、メソッドを使用できます。
⸻
プリミティブ型にプロパティを追加しても保持されない
ここで少し気になることがあります。
オブジェクトであれば、プロパティを追加できます。
const user = {};
user.name = "Taro";
console.log(user.name);
実行結果
Taro
しかし、プリミティブ型ではどうでしょうか。
let text = "hello";
text.test = "test";
console.log(text.test);
実行結果
undefined
これは、プリミティブ値にアクセスするたびに一時的なラッパーオブジェクトが作られ、処理後に破棄されるためです。
イメージとしては以下のようになります。
text.test = "test"
↓
一時的なStringオブジェクトを作成
↓
testプロパティを追加
↓
処理終了後、一時的なオブジェクトが破棄される
次に、
text.test
へアクセスすると、先ほどとは別の一時的なオブジェクトとして扱われます。
そのため、先ほど追加した test プロパティは存在せず、undefined になります。
⸻
TypeScriptでも同じ?
結論として、TypeScriptでも基本的な仕組みは同じです。
例えば、
const text: string = "hello";
console.log(text.toUpperCase());
TypeScriptでも string はプリミティブ型ですが、toUpperCase() を使用できます。
理由は、TypeScriptは最終的にJavaScriptに変換されて実行されるためです。
TypeScriptのコード
const text: string = "hello";
console.log(text.toUpperCase());
JavaScriptとして実行されるコード
const text = "hello";
console.log(text.toUpperCase());
実際にブラウザやNode.jsで実行されるのはJavaScriptです。
そのため、プリミティブ型からメソッドを使用できる仕組みはJavaScriptと同じです。
⸻
TypeScriptでは小文字と大文字に注意
TypeScriptでは以下のような型があります。
string
number
boolean
これらはプリミティブ型です。
一方で、
String
Number
Boolean
も存在します。
こちらはJavaScriptのラッパーオブジェクトに対応する型です。
通常は以下のように、小文字のプリミティブ型を使用します。
let name: string;
let age: number;
let isLogin: boolean;
大文字の String、Number、Boolean は基本的には使用しないことが推奨されています。
⸻
まとめ
JavaScriptのプリミティブ型は、オブジェクトではありません。
例えば、
const text = "hello";
console.log(typeof text); // string
しかし、以下のようにメソッドを使用できます。
text.toUpperCase();
これはJavaScriptがメソッドやプロパティへアクセスする際に、一時的に対応するラッパーオブジェクトとして扱ってくれるためです。
主な対応関係は以下の通りです。
プリミティブ型 ラッパーオブジェクト
string String
number Number
boolean Boolean
つまり、
プリミティブ型そのものはオブジェクトではないが、メソッドを使用する際にJavaScriptが一時的にラッパーオブジェクトとして扱ってくれるため、メソッドを使用できる。
という仕組みになっています。
最初は「オブジェクトではないのに、なぜメソッドがあるの?」と疑問に思いましたが、ラッパーオブジェクトの仕組みを知ることで理解することができました。
JavaScriptには、このように一見すると不思議に思える仕様がまだ多くありそうなので、今後も疑問に思ったことを調べて記事にしていきたいと思います。