JavaScript equality

Contents

The table

==nullundefined"null""undefined"false-000n"-0""0"[0]""[]"false"true11.01n"1"[1]"1.0"NaN
nulltruetruefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalse
undefinedtruetruefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalse
"null"falsefalsetruefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalse
"undefined"falsefalsefalsetruefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalse
falsefalsefalsefalsefalsetruetruetruetruetruetruetruetruetruefalsefalsefalsefalsefalsefalsefalsefalsefalse
-0falsefalsefalsefalsetruetruetruetruetruetruetruetruetruefalsefalsefalsefalsefalsefalsefalsefalsefalse
0falsefalsefalsefalsetruetruetruetruetruetruetruetruetruefalsefalsefalsefalsefalsefalsefalsefalsefalse
0nfalsefalsefalsefalsetruetruetruetruetruetruetruetruetruefalsefalsefalsefalsefalsefalsefalsefalsefalse
"-0"falsefalsefalsefalsetruetruetruetruetruefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalse
"0"falsefalsefalsefalsetruetruetruetruefalsetruetruefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalse
[0]falsefalsefalsefalsetruetruetruetruefalsetruetruefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalse
""falsefalsefalsefalsetruetruetruetruefalsefalsefalsetruetruefalsefalsefalsefalsefalsefalsefalsefalsefalse
[]falsefalsefalsefalsetruetruetruetruefalsefalsefalsetruetruefalsefalsefalsefalsefalsefalsefalsefalsefalse
"false"falsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsetruefalsefalsefalsefalsefalsefalsefalsefalse
truefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsetruetruetruetruetruetruetruefalse
1falsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsetruetruetruetruetruetruetruefalse
1.0falsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsetruetruetruetruetruetruetruefalse
1nfalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsetruetruetruetruetruetruefalsefalse
"1"falsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsetruetruetruetruetruetruefalsefalse
[1]falsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsetruetruetruetruetruetruefalsefalse
"1.0"falsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsetruetruetruefalsefalsefalsetruefalse
NaNfalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalsefalse
typeof x"object""undefined""string""string""boolean""number""number""bigint""string""string""object""string""object""string""boolean""number""number""bigint""string""object""string""number"
Boolean(x)falsefalsetruetruefalsefalsefalsefalsetruetruetruefalsetruetruetruetruetruetruetruetruetruefalse
Number(x)0NaNNaNNaN000000000NaN1111111NaN
String(x)"null""undefined""null""undefined""false""0""0""0""-0""0""0""""""false""true""1""1""1""1""1""1.0""NaN"
Object(x)[object Object][object Object]nullundefinedfalse000-00[0][]falsetrue1111[1]1.0NaN
x.constructor.nameTypeError: x is nullTypeError: x is undefined"String""String""Boolean""Number""Number""BigInt""String""String""Array""String""Array""String""Boolean""Number""Number""BigInt""String""Array""String""Number"
x.valueOf()TypeError: x is nullTypeError: x is undefined"null""undefined"false000n"-0""0"[0]""[]"false"true111n"1"[1]"1.0"NaN
x.toString()TypeError: x is nullTypeError: x is undefined"null""undefined""false""0""0""0""-0""0""0""""""false""true""1""1""1""1""1""1.0""NaN"

Options

JavaScript is required to use this interactively.

Keep in mind that anything entered into these fields will be evaluated as JavaScript code. Do not enter suspicious code from outside sources.

false true boolean number bigint string symbol object function other throws exception

Explanation of ==

Definition of == in the specification

First of all, as defined in the specification, there are eight different types: undefined, null, boolean, string, symbol (values that can be used as attribute names but aren't strings), number (yes, NaN is a number), bigint, and object (which includes arrays, functions, dates, regular expressions, etc.). This definition differs from what the typeof operator uses, in that in this definition null is its own type (rather than being of type "object"), and that in this definition functions are a type of object (rather than being their own type). I'm going to use this definition of type, because the rules for equality are simpler that way.

Second, there are two different equality operators, == and ===. (!= and !== are the negation of == and ===, respectively.) If the two values being compared are the same type, these operators give the same result. If the two values being compared are different types, === always returns false, whereas == will generally perform some kind of type conversion. == has been in the language since the beginning, whereas === was added later. Some behaviors shared by both operators:

Type conversions for == are summarized in the following table:

undefined null boolean number string bigint symbol object
undefined true true false false
null true true
boolean false Compare value Convert to number Convert to numbers Convert to bigint false Convert to primitive
number false Convert to number Compare value Convert to number Convert to ℝ false Convert to primitive
string Convert to numbers Convert to number Compare value Convert to bigint
bigint Convert to bigint Convert to ℝ Convert to bigint Compare value
symbol false false Compare value
object Convert to primitive Convert to primitive Compare reference

That is:

For object == primitive and primitive == object comparisons, the value depends on what valueOf returns. Unless you're doing something weird, the following table describes what happens (where obj is the object and val is the primitive value; +val converts a boolean or string into a number, as described above):

valueOf returnsExamplesResultNotes
null or undefined(no standard objects do this)false
boolean, number, string, bigint, or symbolBoolean, Number, String, BigInt and Symbol objectsobj.valueOf() == valSee the table above
objectObject, Array, Function, RegExpobj.toString() == valMight still compare equal to a number if toString returns a string that can be parsed as a number (including "")
Date objects (special case)obj.toString() == val

That is, conversion to primitive uses valueOf unless valueOf returns an object, in which case it uses toString. The default implementation of valueOf is basically return this;, so objects that don't define valueOf use toString.

Of the standard object types that don't implement valueOf, there's one that's likely to return a string that can convert into number, and that is Array. toString on an array converts each element to a string and puts commas between them, but doesn't put any sort of brackets around the array. For instance, ["hi","there"].toString() is "hi,there", and [2,3,4].toString() is "2,3,4". If you have an array of one element, which is either a number (like [5]) or a string containing a number (like ["5"]), then converting the array to a string will result in a string that can successfully be converted into a number. Because of this, [5] == 5 and ["5"] == 5 both return true. An empty array ([]) or an array containing only an empty string ([""]) converts to "", which is considered equal to 0.

The actual algorithm it uses: When the browser converts an object into a primitive type, it first tries the valueOf function. If valueOf returns undefined or null, then the values are not equal. If valueOf returns a primitive value, then it's compared to the other value using == comparison (i.e., it may be further converted to a different type). If valueOf is not a function or returns an object, then it tries toString instead, using the same rules that it uses for valueOf. If toString is not a function or returns an object, then it throws TypeError. However, there are two special cases:

Additionally, there's one other weird exception for browsers: document.all == undefined and null (but not ===), and typeof document.all is "undefined". This is to keep compatibility with old webpages that use document.all, but also to keep compatibility with old webpages that try to detect certain older browsers by checking if document.all exists.

Other operators

General conversions

In general, if JavaScript wants to implicitly convert a value from one type to another, these are the rules it uses:

to boolean to number to bigint to numeric to string to object
from undefined false NaN throw exception NaN "undefined" throw exception*
from null false 0 throw exception 0 "null" throw exception*
from boolean (no conversion) 0 or 1 0n or 1n 0 or 1 "false" or "true" Boolean object
from number false if ±0 or NaN (no conversion) throw exception* (no conversion) string representation Number object
from bigint false if 0 throw exception* (no conversion) (no conversion) string representation (no "n") BigInt object
from string false if empty parse number (NaN if fail) parse bigint (exception if fail) parse number (NaN if fail) (no conversion) String object
from symbol true throw exception throw exception throw exception throw exception* Symbol object
from object true (except document.all) prefer valueOf prefer valueOf prefer valueOf prefer toString (no conversion)

For conversions from objects, it tries valueOf and toString, using the same algorithm as for ==, except there isn't an exception for Dates, and conversion to string tries toString first (except for Symbol objects). After it converts the value to a primitive, it then tries to convert whatever primitive value it gets to the right type.

* Usually if you call Boolean, Number, etc. as a function, it behaves the same as an implicit type conversion. However, there are a few cases where it behaves differently:

Arithmetic and comparison operators

Summary:

Operator Date string and string string and bigint string and other bigint and number/boolean/null
==, != string representation string compare convert to bigint convert to number (except null) compare
<, >, <=, >= milliseconds since 1970 string compare convert to bigint convert to number compare
x + y string representation string concat string concat string concat exception
Other arithmetic milliseconds since 1970 convert to number exception convert to number exception
Operator Date string/other bigint
+x milliseconds since 1970 convert to number exception
Number(x) milliseconds since 1970 convert to number convert to number
-x, ~x milliseconds since 1970 convert to number perform bigint operation

For everything shown in the tables above, objects other than Date first try valueOf and then try toString if that doesn't work. For everything except == in the tables above, using a symbol as an operand throws an exception.

Transitive closure of ==

One of the main complains about == is that it isn't transitive; that is, x == y and y == z does not imply that x == z. The way this was actually solved with === was to not do type conversions, meaning that in some cases where == returns true, === would return false (and this is probably the right way to do things), but it could be somewhat interesting to think, what if they went the other way? What if they instead defined an operator that still did the type conversions, was true everywhere == is true, but was also true in other situations in order to be transitive?

Cases where x == y and y == z but x != z:

  1. y is null or undefined: then x and z are both null, undefined, or document.all, and therefore already equal to each other
  2. y is a boolean: then x and z, converted to primitive then converted to number, both equal 0 or 1, so this is taken care of by case 3
  3. y is a number: three cases:
    1. neither x nor z is a bigint: then x and z, converted to primitive and then converted to number, equal the same number, and that number isn't NaN.
    2. x is bigint, z is not bigint (or vice versa): then z, when converted to primitive and then converted to number, is exactly equal to x
    3. both x and z are bigints: then both x and z equal the value of the number, and therefore are already equal
  4. y is a string: cases:
    1. the string is converted to a number in both comparisons, which is the same as case 3a
    2. the string is converted to a number in x == y and a bigint in y == z (or vice versa), in which case the bigint converted to a string and then a number is equal to the non-bigint converted to a number
    3. the string is converted to a bigint in both comparisons, which case 4b should already take care of
    4. there's no string conversion in x == y (or y == z), in which case String(x) === y so String(x) == z
  5. y is a bigint: I'm pretty sure all the possibilities are taken care of by case 3
  6. y is a symbol: then x and z, converted to a primitive, both convert to the same symbol
  7. y is an object:
    1. the object converts to a primitive type in both cases (in which case it's the same as if a primitive type is used)
    2. the object is exactly the same object as either x or z (or both)
    3. the object is document.all, x is null or undefined, and z is the string representation of document.all (or vice versa)

Note that this also means that bigints that are approximated by the same number have to be equivalent, since e.g. 9007199254740993n == "9007199254740993" == 9007199254740992 == 9007199254740992n.

There isn't really a good way to take into consideration objects where valueOf or toString is not referentially transparent (e.g., {valueOf:function(){return Math.random();}}).

This is what I came up with:

function toPrimitive(obj) {
	if(obj == null || (typeof obj != 'object' && typeof obj != 'function'))
		return obj;
	if(obj.valueOf != null && Object.prototype.toString.apply(obj) != '[object Date]') {
		var v = obj.valueOf();
		if(v == null || (typeof v != 'object' && typeof v != 'function'))
			return v;
	}
	if(obj.toString != null) {
		var v = obj.toString();
		if(v == null || (typeof v != 'object' && typeof v != 'function'))
			return v;
	}
	// my implementation doesn't handle case where a Date object doesn't
	// have a toString method that returns a string, nor where a Symbol
	// object has a modified valueOf method
	throw new TypeError();
}

// transitive closure of ==
function transitiveEquals(x, y) {
	if(x == y) return true;
	// case 7c
	if((x == String(document.all) && y == null)) ||
	   (x == null && y == String(document.all))
		return true;
	// otherwise, null and undefined are only equal to each other
	if(x == null || y == null)
		return false;
	// is there a value z such that x == z and z == y?
	// x and y convert to the same primitive value (cases 3c, 4d, and 6):
	var x_prim = toPrimitive(x), y_prim = toPrimitive(y);
	if(x_prim == null || y_prim == null)
		return false;
	if(x_prim == y_prim)
		return true;
	// z is number: x == z iff toPrimitive(z) is not a symbol and
	// Number(toPrimitive(x)) == z
	// cases 3a, 3b, 4a, 4b, 4c
	if(typeof x_prim != 'symbol' && typeof y_prim != 'symbol' &&
	   (typeof x_prim == 'bigint' ? +String(x_prim) : +x_prim) ==
	   (typeof y_prim == 'bigint' ? +String(y_prim) : +y_prim))
		return true;
	return false;
}

Sources

See also