【JavaScript】 プリミティブの奇妙な振る舞いとラッパーオブジェクト
更新日:2021/01/13
普段から作成しているJavaScriptコード。
しかしプリミティブの性質を考えると、少し奇妙な動きをしていることに気が付きます。
今回はプリミティブとオブジェクトの関係についてお伝えします。
プリミティブの奇妙な動作
JavaScriptのプリミティブは、メソッドやプロパティを持たないデータそのものを指します。
しかしプリミティブは、この認識だけでは説明できない奇妙な振る舞いをすることがあります。
例えば、次のようなコードです。
console.log( "今日は雨です".replace("雨","晴れ") ); // 今日は晴れです
"今日は雨です"は文字列プリミティブです。
文字列プリミティブはメソッドを持っていないので、上のコードを実行するとエラーが出るはずです。
しかし、何の問題もなく"今日は晴れです"という結果が返ってきます。
少しJavaScriptを学んだ人なら、直感的に「Stringオブジェクトのreplaceメソッドが呼ばれている」と判断することでしょう。
しかし、よく考えてみてください。
"今日は雨です"は文字列プリミティブであって、Stringオブジェクトではありません。
どう考えても、replaceメソッドが実行されるのは奇妙な振る舞いなのです。
ラッパーオブジェクト
結論から言うと、プリミティブはメソッド呼び出しの前段階として、対応するオブジェクトに変換されます。
例えば文字列プリミティブは、Stringオブジェクトに変換されます。
変換後のオブジェクトは、オブジェクトでプリミティブをラップ(包む)しているように見えることから、「ラッパーオブジェクト」と呼ぶこともあります。
前項のコード例では、文字列リテラルからラッパーオブジェクトに変換されていましたが、プリミティブを代入した変数でも同じように変換動作がおこなわれます。
const str = "今日は雨です";
console.log( str.replace("雨","晴れ") ); // 今日は晴れです
上のコードのような処理は、いつも普通に使用していますよね。
実はそれも、よく考えると奇妙な動作だったわけです。
※オブジェクトはプリミティブではありませんが、JavaScriptの内部での変換対象です。実際にはそのまま返しています。
ラッパーオブジェクトの種類
JavaScriptには様々なデータ型が存在します。
各々のデータ型からメソッドを呼び出そうとすると、次の表に従って対応するオブジェクトに変換されます。
データ型 | 値の例 | 対応するオブジェクト |
---|---|---|
Undefined | undefined | TypeError例外をスローする |
Null | null | TypeError例外をスローする |
Boolean | true/false | Booleanオブジェクト |
Number | 12345 | Numberオブジェクト |
String | "あいうえお" | Stringオブジェクト |
シンボル | Symbol() | Symbolオブジェクト |
BigInt | 1000n | BigIntオブジェクト |
Object | Objectをそのまま返す |
データにはundefinedなどの定数が存在します。
そのような値も変換ルールが決められています。
対応するオブジェクトの存在意義
データは頻繁に対応するオブジェクトに変換されます。
少し効率が悪く感じますね。
そこで次のように、あらかじめオブジェクトのインスタンスを作成しておく案が考えられます。
const value = new String( "あいうえお" );
これならラッパーオブジェクトの変換が必要ないので効率がよさそうです。
実は大きな罠だったりします。
StringやNumberなどのラッパーオブジェクトの多くは、基本的にプリミティブを返します。
StringやNumberオブジェクトを返すことはありません。
次のようなメソッドチェーンは、プリミティブが返されラッパーオブジェクトに変換され、再度プリミティブが返されと、毎回変換されているのです。
const text = " abcde ";
const result = text.trim().toUpperCase().replace("A","Z");
console.log( result ); // ZBCDE
変換を効率化しようとしても、あまり意味がないですね。
ちなみに、StringやNumberなどのインスタンスには、ある特徴があります。
それは、値を変更できないことです。
たとえば次のコードは、文字列"あいうえお"をデータとしてもつStringオブジェクトのインスタンスを作成して、変数valueにセットしています。
続くコードは変数valueの値を変更していますが、これはインスタンス値を変更しているのではなくて、新しい文字列プリミティブを変数valueにセットしているだけです。
値の変更はできません
const value = new String( "あいうえお" );
value = "ABCDE";
これらのオブジェクトはインスタンス化できますが、あまり意味がないのです。
では、その存在意義はというと、データをメソッドへと伝達するためのラッパーとして存在しているのです。
余談:実際は変換していないかもしれない
ラッパーオブジェクトへの変換は、かなり効率が悪いです。
例えば次のコードは、同じ文字列に対して10000回も変換操作をおこなっています。
for( let i = 0 ; i < 10000 ; i ++){
"今日は雨です".replace("雨","晴れ");
}
どれくらい遅いのか気になって、実際のコードで速度を計測する人もいるかもしれません。
しかし、JavaScriptの仕様書を元にブラウザなどへ実装する際、仕様書よりも効率的なアルゴリズムがあるならそちらを適用することが許されています。
つまり動作が正しく行われるなら、いちいち変換しなくてもいいのです。
もちろん今使っているブラウザは、仕様書に書かれている通りに毎回変換しているかもしれません。
効率を上げるために、オリジナルの処理を実装しているかもしれません。
そんな状況なので、変換しているという前提で速度計測するのは、あまり意味がないかもしれませんね。
更新日:2021/01/13
関連記事
スポンサーリンク
記事の内容について
こんにちはけーちゃんです。
説明するのって難しいですね。
「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。
裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。
掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。
ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php
このサイトは、リンクフリーです。大歓迎です。