NHN Cloud Meetup 編集部
ECMAScriptスペック探索:Primitive
2018.03.12
468
はじめに
今回は、プリミティブについて紹介したいと思います。プリミティブが何か、プリミティブラッパーがどのように関与するかについて、スペック文書といくつかの実験をもとに取り扱います。すでによく知られている内容もありますが、プリミティブについてもう少し詳しく知りたいフロントエンド向けに作成しました。
プリミティブ型とは?
Javaスクリプトには6つ(undefined、null、boolean、string、number、symbol)のプリミティブ型があります。
スペック文書の概要では、プリミティブ値を説明するときに、次のように記述されています。
“A primitive value is a member of one of the following built-in types: Undefined, Null, Boolean, Number, String, and Symbol …”
プリミティブ値は、内蔵タイプのいずれかのクラスに属します。内蔵タイプのコンストラクタでは別の概念です。
var numberPrimitive = 10; var booleanPrimitive = false; var stringPrimitivie = 'string boy';
typeof
typeofキーワードを使うとターゲットのタイプが分かります。プリミティブ値に直接使うと値のタイプが分かります。関数は実行可能なオブジェクトでobject型に分類されます。そのためオブジェクトとしての特性もすべて持っています。typeofを使えばECMAScript言語タイプを区別できます。特異点は、ECMAScript言語タイプにfunctionというタイプがないにもかかわらず、関数をtypeof
を通して見ると”function”に戻ることです。typeofの動作原理を簡単に説明すると、オブジェクトでありながら、内部プロパティ[[Call]]
が実装されている場合、”function”を返却します。関数とオブジェクトを区別する必要がある場合があるため、関数に対する特別待遇であるようです。
プリミティブの特徴
スペックにはプリミティブに直接関わる単語が3つあります。
- プリミティブ値(Primitive Value)
- プリミティブ型(Primitive Type)
- プリミティブオブジェクト(Primitive Object)
プリミティブ値は、Javaスクリプト言語の実装の中で最も低レベルのデータムを意味します。ナンバー値の場合、64-bit倍程度のバイナリフォーマットIEEE 754-2008値です。そのため小数点を演算する際にIEEE754標準のよく知られた問題が発生することもあります。(0.1 +0.2)プリミティブ値は、特定のプリミティブ型に属します。プリミティブ型は、プリミティブ値で作れるすべてのものを意味します。プリミティブ型はオブジェクトではないためプロパティを持つことができません。プリミティブオブジェクトは、プリミティブ型に対応する内蔵されたコンストラクタを通じて作成されたインスタンスです。このインスタンスは、特定タイプのプリミティブ値を扱うプロパティが含まれており、内部スロット([[PrimitiveValue]])にプリミティブ値を持っているオブジェクト型に属します。つまり、プロパティを持つことができます。プリミティブオブジェクトが持っているプリミティブ値はvalueOf()メソッドを利用して取り出すことができます。
例えば、数字の10はナンバー値(Number Value)で、numberタイプに属します。そしてNaNとInfinityもnumberタイプに属します。(NaNとInfinityはIEEE754で定義されたナンバー値)Numberコンストラクタを使用してオブジェクトにすると、タイプはオブジェクトタイプですが、valueOf()
を通じてプリミティブ値を求めることができます。
演算子を用いて演算するときは、オブジェクトのvalueOf()メソッドを実行した結果として評価するため、オブジェクトもプリミティブ値で相互演算が可能です。
var num10 = 10; //ナンバー値 typeof num10 // ナンバー値はnumberタイプである typeof NaN // 'number' typeof Infinity // 'number' var num10Obj = new Number(10); // ナンバーオブジェクト typeof num10Obj // 'object' console.log(num10Obj) // Chromeではコンソール内部でスロット[[PrimitiveValue]]の値が確認できる num10Obj.valueOf() // 10 num10 === num10Obj // 実際には、num10 === num10Obj.valueOf(); で評価
valueOfメソッドはオーバーライドができます。内部スロットのプリミティブ値を返却するのではなく、オブジェクトの意図に合わせて、他の動作に変更、拡張が可能です。演算子のオーバーライドができないJavaスクリプトでは、利点を使ってオブジェクトが演算子に適切に反応するように開発できます。これが可能な理由は、内部的にオブジェクトのvalueOfを先に実行し、その結果値で演算を行うからです。
演算子オブジェクトのvalueOfが実行される点を利用すると、演算子のオーバーライドができないJavaスクリプトでもオブジェクトが演算子に意図した通り反応するように開発できます。つまり内部スロットのプリミティブ値を返却するのではなく、オブジェクトの意図に合わせて、他の動作に変更、拡張ができます。
num10Obj.valueOf = function() { return 50; }; // valueOfメソッドはオーバーライドできる num10 + num10Obj // 60 Number.prototype.valueOf.call(num10Obj); // 10, 依然として内部スロットには10という値が入っている
プリミティブ値はイミュータブルであり、プリミティブ型はCall by valueでオブジェクトはCall by referenceです。しかし、オブジェクトタイプであってもvalueOf()メソッドを通してプリミティブ値を用いて演算するので、基本的に値のイミュータブル性は変わりません。プリミティブオブジェクトの演算結果は、プリミティブ値に変更されることに注意する必要があります。
var n10a = 10; var n10b = n10a; // 値をコピー n10b += 1; n10a === n10b // false, var no10a = new Number(10); var no10b = no10a; // 参照が伝わる。aとb両方同じ参照 var no10c = new Number(10); no10a === no10b // true, 参照比較をする no10a === no10c // false, 参照比較をする no10b += 1; no10a === no10b // false, num10Objbにはナンバーオブジェクトではなく、"11"というナンバー値が入る
プリミティブオブジェクトへの型変換
プリミティブ値はプロパティが持てません。そもそもオブジェクトではなく、ただメモリ上に定められたスペースを占めている低レベルのデータの一部にすぎません。
var num = 10; num.newProp = 5; console.log(num.newProp); // undefined
コード自体は正常に実行されエラーは発生しないが、プロパティは生成されません。このように動作する理由は、スペックの中でSet抽象オペレーションが動作するためだが、Setは引数としてオブジェクトやプロパティ名、値などを受け取り、オブジェクトのプロパティとして特定の値を割り当てるときに使用します。Set抽象オペレーションは次の通りです。
1. Assert: Type(O) is Object. 2. Assert: IsPropertyKey(P) is true. 3. Assert: Type(Throw) is Boolean. 4. Let success be ? O.[[Set]](P, V, O). 5. If success is false and Throw is true, throw a TypeError exception. 6. Return success.
オペレーションの最初の行を見ると、対象のタイプがObjectであることが必要になります。つまり対象がオブジェクトのときだけプロパティを生成するという意味です。オブジェクトがない場合は、別途エラーはなく作業は無視されます。しかしStrict Modeでは以下のようにTypeError例外が発生します。
“Uncaught TypeError: Cannot create property ‘newProp’ on number ’10′”
このようにStrictモードでエラーが発生する内容は、スペックのThe Strict Mode Of ECMAScriptから検索できます。
“… nor to a non-existent property of an object whose [[Extensible]] internal slot has the value false. In these cases a TypeError exception is thrown. “
特定の識別子の存在しないプロパティにアクセスするときに、ターゲットの[[Extensible]]内部スロットがfalse値を持つ場合は、TypeError例外が発生します。言い換えれば、対象がExtensibleでなければプロパティを拡張することができないため、例外が生じます。幸いなことにターゲットの[[Extensible]]
内部スロットの値を確認するAPIがJavaスクリプト上に露出されており、コードで確認できます。まさにObject.isExtensible()メソッドです。
var obj = {}; console.log(Object.isExtensible(obj)) // true var num = 10; console.log(Object.isExtensible(num)) // false console.log(Object.isExtensible(undefined)); // false console.log(Object.isExtensible(null)); // false console.log(Object.isExtensible(NaN)); // false
JavaScriptのプリミティブの最も代表的な特徴は、おそらく暗黙的なプリミティブオブジェクトへの型変換です。そのためNumberコンストラクタを使用して作成されたオブジェクトではなく、プリミティブnumberの値もNumberコンストラクタのプロパティを使用できます。コーディングの利便性を考慮したときにも必要な機能ではありますが、Javaのオートボクシングに影響を受けたものと思われます。
var num = 0.1234; num.toFixed(2); // 0.12
簡単な関数をNumberコンストラクタのプロトタイプに追加して型変換が実際に起こっているか確認できます。
Number.prototype.whoAmI = function() { console.log(typeof this);} var num = 10; typeof num; // "number" num.whoAmI(); // "object" typeof num // "number"
プリミティブオブジェクトへの型変換は、抽象オペレーション[[ToObject]]によって実質的に型変換が行われます。引数として渡されたプリミティブ値のタイプに応じて、スペックの変換テーブルを基に定められたコンストラクタを利用して、引数で渡された値を内部スロットに所有している新しいオブジェクトに返却します。引数のプリミティブ値と同じように評価されるプリミティブオブジェクトを返却します。型変換が起こる原因はさまざまだが、実際の型変換は[[ToObject]]抽象オペレーションを介してのみ変換されます。
実質的にプリミティブ値をプリミティブオブジェクトに変更するのは[[ToObject]]であり、私たちが使うコードでプリミティブ値のプロパティにアクセスして実行される抽象オペレーションは[[GetV]]
です。[[GetV]]
はプリミティブ値のプロパティにアクセスするときに実行されるオペレーションで、対象がオブジェクトでなければ同じタイプのプリミティブオブジェクトを生成してプロパティを借りて使わせます。ここでのオペレーションでももちろん[[ToObject]]
を使用します。
1. Assert: IsPropertyKey(P) is true. 2. Let O be ? ToObject(V). 3. Return ? O.[[Get]](P, V).
Pはアクセスするプロパティで、Vはプリミティブ値Oで新規作成されたプリミティブオブジェクトです。アクセスするプロパティのキーが、正常なキーであることを確認し[[ToObject]]を利用して変換した後、生成されたOに[[Get]]
という抽象オペレーションを実行するために、このオペレーションはオブジェクトのプロパティにアクセスするときに実行されます。プリミティブ値を通してオブジェクトを作成したので、正常にオブジェクトのプロパティにアクセスします。思ったより簡単な作業ではありません。「プリミティブオブジェクトへの型変換」と言いましたが、それは表面的な意味であり、実際にはプリミティブオブジェクトを生成して使用した後、削除する作業です。上段のJavaScriptコードでwhoAmIとメソッドを実行するときに生成されたオブジェクトは、whoAmIメソッドの実行が終了すると消えます。すなわち既存のプリミティブ値は、そのまま変化がありません。