データ変換プリミティブ

【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

同様な関数の一部をリストアップします。

hint"number"が指定される関数
関数名対象引数

位置(1~)

内容
isFinite1引数が有限値かどうかチェックする
isNaN1引数が非数値かどうかチェックする
Number1数値への変換またはNumberオブジェクトの作成
Number.prototype.toExponential110進指数表記の文字列に変換
Number.prototype.toFixed110進固定小数点表記の文字列に変換
Number.prototype.toPrecision110進指数表記または10進固定小数点表記の文字列に変換
Number.prototype.toString1文字列に変換
BigInt1BigInt値への変換
BigInt.asIntN各引数指定したビット数に変換
BigInt.asUintN各引数指定したビット数に変換
BigInt.prototype.toString1文字列に変換
Math.abs1絶対値を取得
Math.acos1アークコサインの近似値を返す
Math.acosh1ハイパーボリックアークコサインの近似値を返す
Math.asin1アークサインの近似値を返す
Math.asinh1ハイパーボリックアークサインの近似値を返す
Math.atan1アークタンジェントの近似値を返す
Math.atan1ハイパーボリックアークタンジェントの近似値を返す
Math.atan2各引数2点間の角度を返す
Math.cbrt1立方根を返す
Math.ceil1切り上げ
Math.clz32132 ビットバイナリ表現での先頭からのゼロの数
Math.cos1コサインの近似値を返す
Math.cosh1ハイパーボリックコサインの近似値を返す
Math.exp1自然対数の底を累乗する
Math.expm11Math.expから1を引いた値
Math.floor1切り捨て
Math.fround132 ビット単精度に変換
Math.hypot各引数各引数の二乗の合計値の平方根
Math.imul各引数32 ビット整数での乗算
Math.log各引数自然対数計算
Math.log1p1(1 + 引数)の自然対数計算
Math.log10110 を底とした対数
Math.log212 を底とした対数
Math.log212 を底とした対数
Math.max各引数最大値を取得
Math.min各引数最小値を取得
Math.pow各引数累乗を求める
Math.round1四捨五入
Math.sign1符号(+1または-1)を取得
Math.sin1サインの近似値を返す
Math.sinh1ハイパーボリックサインの近似値を返す
Math.sqrt1平方根を返す
Math.tan1タンジェントの近似値を返す
Math.tanh1ハイパーボリックタンジェントの近似値を返す
Math.trunc1小数桁の削除
Date ※引数が二つ以上各引数時刻取得またはDateオブジェクトの作成
Date.UTC各引数UTC時刻取得
Date.prototype.setDate1時刻セット
Date.prototype.setFullYear各引数年月日セット
Date.prototype.setHours各引数時分秒ミリ秒セット
Date.prototype.setMilliseconds1ミリ秒セット
Date.prototype.setMinutes各引数分秒ミリ秒セット
Date.prototype.setMonth各引数月日セット
Date.prototype.setSeconds各引数秒ミリ秒セット
Date.prototype.setTime1時刻セット
Date.prototype.setUTCDate1UTC時刻セット
Date.prototype.setUTCFullYear各引数年月日セット(UTC)
Date.prototype.setUTCHours各引数時分秒ミリ秒セット(UTC)
Date.prototype.setUTCMilliseconds1ミリ秒セット(UTC)
Date.prototype.setUTCMinutes各引数分秒ミリ秒セット(UTC)
Date.prototype.setUTCMonth各引数月日セット(UTC)
Date.prototype.setUTCSeconds各引数秒ミリ秒セット(UTC)
Date.prototype.setUTCSeconds各引数秒ミリ秒セット(UTC)
String.fromCharCode各引数文字コードから文字列を作成
String.fromCodePoint各引数コードポイントから文字列を作成
String.prototype.charAt1指定位置の文字を返す
String.prototype.charCodeAt1指定位置のコードユニット値を返す
String.prototype.endsWith2(1は"string")指定した文字列で終わるか判定
String.prototype.includes2(1は"string")指定した文字列で含まれるか判定
String.prototype.indexOf2(1は"string")指定した文字列の位置を取得
String.prototype.lastIndexOf2(1は"string")指定した文字列の位置を後ろから取得
String.prototype.padEnd1(2は"string")後方の穴埋め
String.prototype.padStart1(2は"string")前方の穴埋め
String.prototype.repeat1文字列の繰り返し
String.prototype.slice各引数文字列の切り出し
String.prototype.split2(1は"string")文字列の分割
String.prototype.startsWith2(1は"string")指定した文字列で始まるか判定
String.prototype.substring各引数文字列の切り出し
Array.prototype.copyWithin各引数配列内で要素コピー
Array.prototype.fill2,3指定範囲を要素で埋める
Array.prototype.flat1配列をフラットにする
Array.prototype.includes2要素検索して要素を返す
Array.prototype.indexOf2要素検索してインデックスを返す
Array.prototype.lastIndexOf2後ろから要素検索してインデックスを返す
Array.prototype.slice各引数要素の切り出し
Array.prototype.splice1,2配列要素の操作

"string"が指定される関数

一部の関数は[Symbol.toPrimitive]関数が呼び出され、その関数の引数として"string"が渡されます。
その一部をリストアップします。

hint"string"が指定される関数
関数名対象引数

位置(1~)

内容
parseFloat1引数を数値に変換する
Number.parseFloat1引数を数値に変換する
parseInt各引数引数を整数値に変換する
Number.parseInt各引数引数を整数値に変換する
decodeURI1エンコードされたURIをデコードする
decodeURIComponent1エンコードされたURIコンポーネントをデコードする
encodeURI1URIをエンコードする
encodeURIComponent1URIコンポーネントをエンコードする
String1文字列への変換、またはStringオブジェクト作成
String.prototype.concat各引数文字列の連結
String.prototype.endsWith1(2は"number")指定した文字列で終わるか判定
String.prototype.includes1(2は"number")指定した文字列で含まれるか判定
String.prototype.indexOf1(2は"number")指定した文字列の位置を取得
String.prototype.lastIndexOf1(2は"number")指定した文字列の位置を後ろから取得
String.prototype.localeCompare1現在のロケールで文字列を比較
String.prototype.normalize1文字列を正規化する
String.prototype.padEnd2(1は"number")後方の穴埋め
String.prototype.padStart2(1は"number")前方の穴埋め
String.prototype.replace各引数文字列の置き換え
String.prototype.split1(2は"number")文字列の分割
String.prototype.startsWith1(2は"number")指定した文字列で始まるか判定
RegExp各引数RegExpオブジェクトを作成
RegExp.prototype.exec1正規表現マッチの実行
RegExp.prototype.test1正規表現マッチのテスト実行
Array.prototype.join1要素連結
Symbol1Symbolを作成する
Symbol.for1作成済みシンボルの検索
Error1Errorオブジェクトの作成
Function各引数Functionオブジェクトの作成
Object.defineProperty2(プロパティ名)プロパティを定義する
Object.getOwnPropertyDescriptor2(プロパティ名)プロパティディスクリプタ―を取得する
Object.prototype.hasOwnProperty1オブジェクトのプロパティ所持確認
Object.prototype.propertyIsEnumerable1プロパティが列挙可能かどうか
Object.prototype.propertyIsEnumerable1プロパティが列挙可能かどうか

 

比較演算での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

書いた人(管理人):けーちゃん

スポンサーリンク

記事の内容について

null

こんにちはけーちゃんです。
説明するのって難しいですね。

「なんか言ってることおかしくない?」
たぶん、こんなご意見あると思います。

裏付けを取りながら記事を作成していますが、僕の勘違いだったり、そもそも情報源の内容が間違えていたりで、正確でないことが多いと思います。
そんなときは、ご意見もらえたら嬉しいです。

掲載コードについては事前に動作確認をしていますが、貼り付け後に体裁を整えるなどをした結果動作しないものになっていることがあります。
生暖かい視線でスルーするか、ご指摘ください。

ご意見、ご指摘はこちら。
https://note.affi-sapo-sv.com/info.php

 

このサイトは、リンクフリーです。大歓迎です。