Why Is {} > []?

Featured on Hashnode

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.

({}) > [] // true; ({}) < [] // false; ({}) == "[object Object]" // true

If you were to open a Node console rather than a browser console, you would see:

{} > [] // true; > {} < [] // false; {} == "[object Object]" // true

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:

¹ ToPrimitive specification

² OrdinaryToPrimitive specification

³ Object.prototype.valueOf() specification

Object.prototype.toString() specification

Array.prototype.toString() specification

Abstract Equality Comparison algorithm

Comments (2)

Martijn Imhoff's photo

When I open the console. I get this... image.png

Amy Shackles's photo

This is actually a good point to bring up.

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.

({}) > [] // true; ({}) < [] // false; ({}) == "[object Object]" // true

If you were to open a Node console rather than a browser console, you would see:

{} > [] // true; > {} < [] // false; {} == "[object Object]" // true

This is because Node made a change to evaluate input as expressions before evaluating them as statements. That change can be seen here.