【JavaScript】 オブジェクトで直接計算させるSymbol.toPrimitiveでプリミティブ変換
更新日:2021/09/27
JavaScriptの全てのオブジェクトは[Symbol.toPrimitive]という特殊なプロパティを指定することができます。
このプロパティを使用すると、オブジェクトそのものを値として算術演算などおこなうことができます。
オブジェクトをプリミティブに変換する
JavaScriptでは計算をするときなど、状況によってはオブジェクトをプリミティブ値に変換します。
その際、[Symbol.toPrimitive]を参照します。
[Symbol.toPrimitive]でプリミティブに変換
オブジェクトに、Symbol.toPrimitiveプロパティを定義すると、オブジェクトをプリミティブに変換できます。
例えば次のように、Symbol.toPrimitiveプロパティを定義したオブジェクトがあるとします。
Symbol.toPrimitiveプロパティを定義したオブジェクト
const obj = {
num : 1000,
[Symbol.toPrimitive]:function(){
return this.num;
}
};
このオブジェクトは、次のようにオブジェクト名で演算することができます。
console.log( 123 + obj ); // 1123
つまり、Symbol.toPrimitiveプロパティで、obj を 1000という数値プリミティブに変換したのです。
[Symbol.toPrimitive]が定義されていないケース
通常のオブジェクトには[Symbol.toPrimitive]が定義されていません。
このようなケースでは、toStringメソッドとvalueOfメソッドが順番に呼び出されます。
呼び出される順番は状況により変化しますが、概ね、文字列が必要なときはtoStringが、数値が必要な時はvalueOfが先に呼び出されます。
なお、メソッド呼び出しの結果がオブジェクトでないとき、値が有効になります。
通常オブジェクトのvalueOf実行結果は、自分自身なのでオブジェクトです。
そのため、toStringの結果がプリミティブ値として採用されます。
toStringとvalueOfの結果
console.log( { }.toString() ); // "[object Object]" ← 文字列
console.log( { }.valueOf() ); // { } ← オブジェクト
次のようにオブジェクトを演算対象にすると、奇妙な文字列が表示されるのは、このような処理をおこなっているからです。
console.log( "" + { } ); // "[object Object]"
Symbol.toPrimitiveプロパティは、この規定値を上書きしているわけです。
Symbol.toPrimitiveプロパティの仕様
Symbol.toPrimitiveプロパティは、引数を一つ受け付ける関数をセットします。
Symbol.toPrimitiveプロパティ定義例
{
[Symbol.toPrimitive]:function( hint ){ }
}
※関数内でthis値を使用しないときは、アロー関数でも問題ありません。
引数hintは、次の文字列のうちどれかです。
"default"
"number"
"string"
これらの値を使用した場合、次のようなコードになります。
引数を使用した例
const obj = {
num : 1000,
text : "abcde",
default : Symbol("default"),
[Symbol.toPrimitive]:function(hint){
switch ( hint ){
case "number": return this.num;
case "string": return this.text;
default: return this.default;
}
}
};
このオブジェクトは、hintが"number"のときは1000を、"string"のときは"abcde"を、"default"のときはシンボル値を返しています。
このオブジェクトを使って計算してみます。
console.log( 500 - obj ); // -500
console.log( obj / 2 ); // 500
console.log( obj % 3 ); // 1
数値演算のためhintの値が"number"でした。
そのため500がリターンされ、その値で計算が続行されました。
この動作は容易に想像できます。
しかし + 演算子は注意が必要です。
console.log( 123 + obj );
// TypeError: can't convert symbol to number
「シンボルが数値に変換できない」というエラーが表示されてしまいました。
つまり、hintの値が"number"でなく、"default"だったため、シンボル値をリターンしたのです。
+ 演算子は加算だけでなく、文字列の連結にも使用されます。
優先度が特に決まっていないため、hintは"default"が使用されるのです。
そして、+ 演算子は、その左右のどちらかが文字列なら、連結されます。
両方文字列でないときは、値を数値に変換してから計算されます。
しかしJavaScriptの仕様上、シンボルは数値に変換できません。
そのため上述のエラーが表示されるのです。
hintとして"number"と"string"を受け取れることから、次のような結果を期待すると思います。
■数値計算のときは、数値プリミティブを有効にしたい
123 + obj ⇒ 1123
■文字列連結のときは、文字列プリミティブを有効にしたい
"ABC" + obj ⇒ "ABCabcde"
残念ながら、このようなことはできません。
個人的には、とても残念です。
オブジェクトを内部でプリミティブに変換する関数
JavaScriptの関数の中には、引数として受け取ったオブジェクトを内部でプリミティブに変換するものがあります。
概要
例えば、文字列引数を数値に変換するparseIntという関数は、その一つです。
parseIntは、数値として判定できない文字列のとき、NaNを返します。
parseIntの結果
console.log( parseInt( "12345") ); // 12345
console.log( parseInt( "abced") ); // NaN
console.log( parseInt( true ) ); // NaN
console.log( parseInt( { } ) ); // NaN
実際には数値変換の前に、引数を文字列に変換しています。
そのため、引数に数値を渡すことができます。
parseIntの結果
console.log( parseInt( 12345 ) ); // 12345 ← 12345 ⇒ "12345" ⇒ 12345
同様に、オブジェクトも文字列に変換されます。
しかし通常のオブジェトを変換すると"[object Object]"という文字列になり、数値と判定できないため、parseIntの結果がNaNになるのです。
Symbol.toPrimitiveプロパティで数値や数値文字列を返すと、parseIntで変換することが可能になります。
Symbol.toPrimitiveプロパティとparseInt
const obj = {
[Symbol.toPrimitive]:function(){
return 12345;
}
}
console.log( parseInt(obj) ); // 12345
JavaScriptはparseIntの他にも、内部でプリミティブ変換しているものが多数あります。
その際、hintに"default"以外のものが指定されるものを、いくつか挙げてみます。
なお実際に使用するときは、console出力やデバッガでどんなhintが与えられるのかを確認してください。
"number"が指定される関数
一部の関数は、次の例のように[Symbol.toPrimitive]関数が呼び出され、その関数の引数として"number"が渡されます。
const obj2 = {
[Symbol.toPrimitive]:function(hint){
switch ( hint ){
case "number": return 1;
case "string": return "abcde";
default: return "abcde";
}
}
}
console.log( isFinite(1) ); // true
console.log( isFinite("abcde") ); // false
console.log( isFinite(obj2) ); // false
同様な関数の一部をリストアップします。
関数名 | 対象引数 位置(1~) | 内容 |
---|---|---|
isFinite | 1 | 引数が有限値かどうかチェックする |
isNaN | 1 | 引数が非数値かどうかチェックする |
Number | 1 | 数値への変換またはNumberオブジェクトの作成 |
Number.prototype.toExponential | 1 | 10進指数表記の文字列に変換 |
Number.prototype.toFixed | 1 | 10進固定小数点表記の文字列に変換 |
Number.prototype.toPrecision | 1 | 10進指数表記または10進固定小数点表記の文字列に変換 |
Number.prototype.toString | 1 | 文字列に変換 |
BigInt | 1 | BigInt値への変換 |
BigInt.asIntN | 各引数 | 指定したビット数に変換 |
BigInt.asUintN | 各引数 | 指定したビット数に変換 |
BigInt.prototype.toString | 1 | 文字列に変換 |
Math.abs | 1 | 絶対値を取得 |
Math.acos | 1 | アークコサインの近似値を返す |
Math.acosh | 1 | ハイパーボリックアークコサインの近似値を返す |
Math.asin | 1 | アークサインの近似値を返す |
Math.asinh | 1 | ハイパーボリックアークサインの近似値を返す |
Math.atan | 1 | アークタンジェントの近似値を返す |
Math.atan | 1 | ハイパーボリックアークタンジェントの近似値を返す |
Math.atan2 | 各引数 | 2点間の角度を返す |
Math.cbrt | 1 | 立方根を返す |
Math.ceil | 1 | 切り上げ |
Math.clz32 | 1 | 32 ビットバイナリ表現での先頭からのゼロの数 |
Math.cos | 1 | コサインの近似値を返す |
Math.cosh | 1 | ハイパーボリックコサインの近似値を返す |
Math.exp | 1 | 自然対数の底を累乗する |
Math.expm1 | 1 | Math.expから1を引いた値 |
Math.floor | 1 | 切り捨て |
Math.fround | 1 | 32 ビット単精度に変換 |
Math.hypot | 各引数 | 各引数の二乗の合計値の平方根 |
Math.imul | 各引数 | 32 ビット整数での乗算 |
Math.log | 各引数 | 自然対数計算 |
Math.log1p | 1 | (1 + 引数)の自然対数計算 |
Math.log10 | 1 | 10 を底とした対数 |
Math.log2 | 1 | 2 を底とした対数 |
Math.log2 | 1 | 2 を底とした対数 |
Math.max | 各引数 | 最大値を取得 |
Math.min | 各引数 | 最小値を取得 |
Math.pow | 各引数 | 累乗を求める |
Math.round | 1 | 四捨五入 |
Math.sign | 1 | 符号(+1または-1)を取得 |
Math.sin | 1 | サインの近似値を返す |
Math.sinh | 1 | ハイパーボリックサインの近似値を返す |
Math.sqrt | 1 | 平方根を返す |
Math.tan | 1 | タンジェントの近似値を返す |
Math.tanh | 1 | ハイパーボリックタンジェントの近似値を返す |
Math.trunc | 1 | 小数桁の削除 |
Date ※引数が二つ以上 | 各引数 | 時刻取得またはDateオブジェクトの作成 |
Date.UTC | 各引数 | UTC時刻取得 |
Date.prototype.setDate | 1 | 時刻セット |
Date.prototype.setFullYear | 各引数 | 年月日セット |
Date.prototype.setHours | 各引数 | 時分秒ミリ秒セット |
Date.prototype.setMilliseconds | 1 | ミリ秒セット |
Date.prototype.setMinutes | 各引数 | 分秒ミリ秒セット |
Date.prototype.setMonth | 各引数 | 月日セット |
Date.prototype.setSeconds | 各引数 | 秒ミリ秒セット |
Date.prototype.setTime | 1 | 時刻セット |
Date.prototype.setUTCDate | 1 | UTC時刻セット |
Date.prototype.setUTCFullYear | 各引数 | 年月日セット(UTC) |
Date.prototype.setUTCHours | 各引数 | 時分秒ミリ秒セット(UTC) |
Date.prototype.setUTCMilliseconds | 1 | ミリ秒セット(UTC) |
Date.prototype.setUTCMinutes | 各引数 | 分秒ミリ秒セット(UTC) |
Date.prototype.setUTCMonth | 各引数 | 月日セット(UTC) |
Date.prototype.setUTCSeconds | 各引数 | 秒ミリ秒セット(UTC) |
Date.prototype.setUTCSeconds | 各引数 | 秒ミリ秒セット(UTC) |
String.fromCharCode | 各引数 | 文字コードから文字列を作成 |
String.fromCodePoint | 各引数 | コードポイントから文字列を作成 |
String.prototype.charAt | 1 | 指定位置の文字を返す |
String.prototype.charCodeAt | 1 | 指定位置のコードユニット値を返す |
String.prototype.endsWith | 2(1は"string") | 指定した文字列で終わるか判定 |
String.prototype.includes | 2(1は"string") | 指定した文字列で含まれるか判定 |
String.prototype.indexOf | 2(1は"string") | 指定した文字列の位置を取得 |
String.prototype.lastIndexOf | 2(1は"string") | 指定した文字列の位置を後ろから取得 |
String.prototype.padEnd | 1(2は"string") | 後方の穴埋め |
String.prototype.padStart | 1(2は"string") | 前方の穴埋め |
String.prototype.repeat | 1 | 文字列の繰り返し |
String.prototype.slice | 各引数 | 文字列の切り出し |
String.prototype.split | 2(1は"string") | 文字列の分割 |
String.prototype.startsWith | 2(1は"string") | 指定した文字列で始まるか判定 |
String.prototype.substring | 各引数 | 文字列の切り出し |
Array.prototype.copyWithin | 各引数 | 配列内で要素コピー |
Array.prototype.fill | 2,3 | 指定範囲を要素で埋める |
Array.prototype.flat | 1 | 配列をフラットにする |
Array.prototype.includes | 2 | 要素検索して要素を返す |
Array.prototype.indexOf | 2 | 要素検索してインデックスを返す |
Array.prototype.lastIndexOf | 2 | 後ろから要素検索してインデックスを返す |
Array.prototype.slice | 各引数 | 要素の切り出し |
Array.prototype.splice | 1,2 | 配列要素の操作 |
"string"が指定される関数
一部の関数は[Symbol.toPrimitive]関数が呼び出され、その関数の引数として"string"が渡されます。
その一部をリストアップします。
関数名 | 対象引数 位置(1~) | 内容 |
---|---|---|
parseFloat | 1 | 引数を数値に変換する |
Number.parseFloat | 1 | 引数を数値に変換する |
parseInt | 各引数 | 引数を整数値に変換する |
Number.parseInt | 各引数 | 引数を整数値に変換する |
decodeURI | 1 | エンコードされたURIをデコードする |
decodeURIComponent | 1 | エンコードされたURIコンポーネントをデコードする |
encodeURI | 1 | URIをエンコードする |
encodeURIComponent | 1 | URIコンポーネントをエンコードする |
String | 1 | 文字列への変換、またはStringオブジェクト作成 |
String.prototype.concat | 各引数 | 文字列の連結 |
String.prototype.endsWith | 1(2は"number") | 指定した文字列で終わるか判定 |
String.prototype.includes | 1(2は"number") | 指定した文字列で含まれるか判定 |
String.prototype.indexOf | 1(2は"number") | 指定した文字列の位置を取得 |
String.prototype.lastIndexOf | 1(2は"number") | 指定した文字列の位置を後ろから取得 |
String.prototype.localeCompare | 1 | 現在のロケールで文字列を比較 |
String.prototype.normalize | 1 | 文字列を正規化する |
String.prototype.padEnd | 2(1は"number") | 後方の穴埋め |
String.prototype.padStart | 2(1は"number") | 前方の穴埋め |
String.prototype.replace | 各引数 | 文字列の置き換え |
String.prototype.split | 1(2は"number") | 文字列の分割 |
String.prototype.startsWith | 1(2は"number") | 指定した文字列で始まるか判定 |
RegExp | 各引数 | RegExpオブジェクトを作成 |
RegExp.prototype.exec | 1 | 正規表現マッチの実行 |
RegExp.prototype.test | 1 | 正規表現マッチのテスト実行 |
Array.prototype.join | 1 | 要素連結 |
Symbol | 1 | Symbolを作成する |
Symbol.for | 1 | 作成済みシンボルの検索 |
Error | 1 | Errorオブジェクトの作成 |
Function | 各引数 | Functionオブジェクトの作成 |
Object.defineProperty | 2(プロパティ名) | プロパティを定義する |
Object.getOwnPropertyDescriptor | 2(プロパティ名) | プロパティディスクリプタ―を取得する |
Object.prototype.hasOwnProperty | 1 | オブジェクトのプロパティ所持確認 |
Object.prototype.propertyIsEnumerable | 1 | プロパティが列挙可能かどうか |
Object.prototype.propertyIsEnumerable | 1 | プロパティが列挙可能かどうか |
比較演算でのSymbol.toPrimitive
比較演算で[Symbol.toPrimitive]プリミティブを使用する場合、注意が必要です。
厳密な比較の場合
[Symbol.toPrimitive]プロパティは、厳密な比較( === , !== ) では呼び出されません。
厳密な比較は型のチェックをおこなうので、プリミティブとの比較は問答無用でfalseです。
プリミティブとの比較
const obj = {
[Symbol.toPrimitive]:()=>5
};
console.log( obj == 5 ); // false ← Object と Number型との比較
オブジェクトの比較は、同じオブジェクトかどうかがチェックされます。
そのため、プリミティブに変換されません。
オブジェクトとの比較
const obj1 = { [Symbol.toPrimitive]:()=>5 };
const obj2 = { [Symbol.toPrimitive]:()=>5 };
console.log( obj1 === obj2 ); // false ← 内容ではなく同じオブジェクトかどうか
厳密でない比較の場合
厳密でない比較( == , != ) は、[Symbol.toPrimitive]プロパティが呼び出されるケースと、呼び出されないケースがあります。
呼び出されないケース
オブジェクト同士を比較した場合、同じオブジェクトかどうかがチェックされます。
そのため、プリミティブに変換されません。
オブジェクトとの比較
const obj1 = { [Symbol.toPrimitive]:()=>5 };
const obj2 = { [Symbol.toPrimitive]:()=>5 };
console.log( obj1 == obj2 ); // false ← 内容ではなく同じオブジェクトかどうか
呼び出されるケース
比較対象の一方がオブジェクトで、もう一方が文字列、数値、長整数、シンボルのいずれかの場合、オブジェクトがプリミティブに変換されます。
オブジェクトとプリミティブとの比較
const obj1 = { [Symbol.toPrimitive]:()=>5 };
console.log( obj1 == 5 ); // true
このとき、hintは"default"が渡されます。
関係演算子の場合
関係演算子( < 、>、 <=、 >=)は、一方、または両方ともオブジェクトのとき、オブジェクトがプリミティブに変換されます。
オブジェクトの関係比較
const obj1 = { [Symbol.toPrimitive]:()=>5 };
const obj2 = { [Symbol.toPrimitive]:()=>6 };
console.log( obj1 > obj2 ); // false
このとき、hintは"number"が渡されます。
更新日:2021/09/27
関連記事
スポンサーリンク
記事の内容について
こんにちはけーちゃんです。
説明するのって難しいですね。
「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。
裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。
掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。
ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php
このサイトは、リンクフリーです。大歓迎です。