TLDR version
Relational Comparisons
In JavaScript, the result of a relational comparison is determined by the Abstract Relational Comparison algorithm. The algorithm converts both sides of a comparison to primitive values and then returns the result of the comparison between those two primitive values.
ToPrimitive¹
The Abstract Relational Comparison algorithm calls ToPrimitive
twice, once for each operand, passing 'number' as the second argument. This tells the ToPrimitive
function that if there are multiple primitive types the operand could convert to, and number is one of them, it should convert the value to a number instead of a different type.
OrdinaryToPrimitive²
If the value being passed to ToPrimitive
is an object, it then calls OrdinaryToPrimitive
with the same two arguments, value and type hint. OrdinaryToPrimitive
generates a list of methods to call to convert the value to a primitive.
If "string" is passed in as the type hint, the method order becomes toString
followed by valueOf
. In this case, since "number" was passed, the method order is valueOf
followed by toString
. It's important to note that while all values that get to this point are objects, not every value will use the valueOf
and toString
methods on the Object prototype.
If the first method results in a value of type "object", the result of calling the second method returned. If the first method does not return a value of type "object", the result of the first method is returned.
OrdinaryToPrimitive( {} )
In the case of {}, the only prototype being looked at is Object, so it first tries calling valueOf
on the object using Object.prototype.value()
³, but that returns {}. Since typeof {} === "object", it moves to the next method. It then calls Object.prototype.toString()
⁴
; If Object.prototype.toString()
is called on a value that is an object, the builtinTag is set to "Object". The return value of Object.prototype.toString()
is the concatenation of "[object ", tag, "]". The return value for passing in an empty object, then, is "[object Object]"
OrdinaryToPrimitive( [] )
In the case of [], there are two prototypes to take into consideration -- Array and Object. If a method exists on the Array prototype, that is the method called. If, however, it does not exist on the Array prototype, it looks for the method on the Object prototype. The Array prototype does not contain a method for valueOf
, so it first tries calling Object.prototype.valueOf()
. That returns [], and since typeof [] === "object", it moves on to the next method.
The Array prototype does have a toString()
method, so It then calls Array.prototype.toString()
⁵.
Array.prototype.toString()
returns the value of the join
method on the array. As there are no elements in the array, the return value of Array.prototype.toString()
on an empty array is an empty string.
Comparison
Now that both sides are converted to their primitive values, it's time to compare them in relation to each other.
"[object Object]" > ""
A string of any length is going to be greater in value than the value of an empty string.
Follow-up
The way that JavaScript evaluates abstract equality when one operand is of type String/Number/Symbol/BigInt and the other operand is an object is to call the same ToPrimitive
on the object and then check equality⁶.
Therefore, we can also sanity check that {} is actually converted to "[object Object]"
and [] is converted to an empty string by performing abstract equality checks.
console.log({} == "[object Object]") // true
console.log([] == "") // true
Why does {} > [] error in browser?
Shout out to Martijn Imhoff for asking this question. I answered in the comments, but feel it's important enough to note here.
The way that the specification for JavaScript is written, block statements are evaluated before expressions, so when the interpreter sees curly braces when not in an expression context, it interprets them as a block rather than an object literal. That's why you get an error when you attempt to run those expressions in the browser. The way to force the interpreter to see {} as an object literal instead of as a block is to wrap it in parentheses.
If you were to open a Node console rather than a browser console, you would see:
This is because Node made a change to evaluate input as expressions before evaluating them as statements. That change can be seen here.
TLDR Version
{}
is converted to "[object Object]"
[]
is converted to ""
"[object Object]" > ""
References:
² OrdinaryToPrimitive specification
³ Object.prototype.valueOf() specification
⁴ Object.prototype.toString() specification