🔢

JavaScriptでオブジェクトと数値を比較するとどうなるのか

2023/03/21に公開

JavaScriptでobject < 100というようにオブジェクトと数値を比較した場合に、どういう挙動になるのか調べてみました。

ChatGPTに聞いてみる

まずは、最近流行りのChatGPTに聞いてみましょう。

自分:
ECMAScriptではobject < 100のようにオブジェクトと数値を比較した場合の挙動はどう定義されているの?

ChatGPT:
ECMAScriptでは、オブジェクトと数値を比較した場合の挙動は、オブジェクトがToPrimitive抽象操作によってプリミティブ値に変換された後に比較されます。

ToPrimitive抽象操作では、オブジェクトをプリミティブ値に変換するために、まずvalueOfメソッドが呼び出されます。もしvalueOfメソッドがプリミティブ値を返す場合、その値が結果として返されます。そうでなければ、toStringメソッドが呼び出され、その結果が返されます。

例えば、以下のようなコードを考えてみましょう。

const obj = {
 valueOf: function() {
   return 50;
 }
};
console.log(obj < 100); // true

この場合、オブジェクトobjはvalueOfメソッドを実装しており、その結果として50を返します。そのため、obj < 100は50 < 100と同じくtrueを返します。

一方で、以下のようなコードでは、valueOfもtoStringも実装されていないため、TypeErrorがスローされます。

const obj = {};
console.log(obj < 100); // TypeError: Cannot convert object to primitive value

したがって、オブジェクトと数値を比較する場合は、オブジェクトがどのようにプリミティブ値に変換されるかに注意する必要があります。

なるほど。でも本当か分からないので、仕様を確認したいですね。

自分:
それは仕様のどこに書いてある?

ChatGPT:
この動作は、ECMAScriptの仕様である「ECMAScript Language Specification」のToPrimitive抽象操作のセクションで定義されています。具体的には、第9.1節で述べられています。

また、オブジェクトの比較については、第7.2.12節で述べられています。この節では、比較演算子が使用された場合、演算子のオペランドのいずれかがオブジェクトである場合に、オペランドがToPrimitive抽象操作によってプリミティブ値に変換されることが説明されています。

具体的には、以下のように記載されています。

If Type(lprim) is either String or Number and Type(rprim) is Object, then
Return the result of comparing lprim and ToPrimitive(rprim).

したがって、オブジェクトと数値を比較した場合の挙動は、ECMAScriptの仕様によって正式に定義されています。

というわけで、仕様を見てみましょう。

ECMAScriptの仕様を読んでみる

ECMAScriptの仕様は https://tc39.es/ecma262/ で見られます。
ChatGPTが教えてくれたセクション番号は少し違っていましたが、教えてくれたキーワードで検索すると辿り着けます。
また、あるセクションを参照している箇所を探すこともできるので、それを使って目的の箇所を探すこともできます。

注意: 以下の説明は、自分なりに要約した雑なものになっています。正確な仕様はご自身で確認してください。

まず、13.10.1 Runtime Semantics: Evaluationが比較演算の動作を定義しているっぽいことが分かりました。
a < bという比較をした場合を見てみると、

  1. aを評価してlvalとする
  2. bを評価してrvalとする
  3. IsLessThan(a, b, true)を呼んだ結果をrとする
  4. rがundefinedならfalse、それ以外はrを返す

ほとんどIsLessThanという抽象操作(abstract operation)が本体ですね。

7.2.13 IsLessThan ( x, y, LeftFirst ) をみてみると、

  1. 引数x,yをToPrimitiveでプリミティブに変換してpx,pyとする
    • ToPrimitiveはざっくり言うとvalueOftoStringを順番に呼んでみて結果を返します
  2. pxとpyの両方が文字列なら、文字列の比較結果を返す
  3. pxとpyが文字列とBigIntの組み合わせなら、文字列の方をBigIntに変換して比較結果を返す
  4. px,pyをToNumericで数値に変換してnx,nyとします
    • ToNumericは、BigInt以外の値はnumberに変換()し、BigIntはそのまま返します
  5. nx,nyが両方numberなら、数値の比較結果(Number::lessThan)を返す
  6. nx,nyが両方BigIntなら、BigIntの比較結果(BigInt::lessThan)を返す
  7. 残るはnx,nyの片方がnumberで片方がBigIntの場合
    1. どちらかがNaNならundefinedを返す
    2. nxが-∞またはnyが+∞ならtrueを返す
    3. nxが+∞またはnyが-∞ならfalseを返す
    4. それ以外はnxとnyをmathematical valueに変換して比較した結果を返す

という感じです。

最後にmathematical valueというのが出てきますが、これは仕様の中で出てくる「実数型」みたいな概念のようです。
BigIntが任意の大きさの整数が扱えるのに対して、number型は64ビットの浮動小数点数が扱えますが、それらの2つを統一して扱えるように定義してあるのかなと思います。

ちなみに、Number関数の仕様 21.1.1.1 Number ( value ) を見てみると、こちらでもToNumeric抽象操作が呼ばれています。
BigInt以外の値に関しては、Number関数とToNumeric抽象操作は同じと言って良いでしょう。

結論

  • object < 100のようにオブジェクトと数値を比較をした場合、オブジェクトの方が数値に変換されて比較されることが分かりました。
    • かなりざっくり言うと、Number(object.valueOf())のようにして数値に変換しています。
  • ChatGPTを鵜呑みにせず、自分で仕様を読みに行きましょう。

Discussion