JavaScript equality (2015 version)

JavaScript is required for the demo

Options

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

Explanation

Definition of == in the specification

First of all, as defined in the specification, there are seven different types: undefined, null, boolean, string, symbol (fairly new; these are values that can be used as attribute names but aren't strings), number (yes, NaN is a number), 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:

undefinednullbooleannumberstringsymbolobject
undefinedtruetruefalsefalse
nulltruetrue
booleanfalseCompare valueConvert to numberConvert to numbersfalseConvert object to primitive
numberfalseConvert to numberCompare valueConvert to numberfalseConvert object to primitive
stringConvert to numbersConvert to numberCompare value
symbolfalsefalseCompare value
objectConvert to primitiveConvert to primitiveCompare 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 returnsExamplesval is booleanval is numberval is stringval is symbol
null or undefined(none)false
booleanBooleanobj.valueOf() === val+obj.valueOf() === val+obj.valueOf() === +valfalse
numberNumberobj.valueOf() === +valobj.valueOf() === valobj.valueOf() === +valfalse
stringString+obj.valueOf() === +val+obj.valueOf() === valobj.valueOf() === valfalse
symbolSymbolfalsefalsefalseobj.valueOf() === val
objectObject, Array, Function, RegExp+obj.toString() === +val+obj.toString() === valobj.toString() === valfalse
Date objects (special case)falsefalseobj.toString() === valfalse

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; (and therefore returns an object).

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.

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:

Source: ECMAScript 2015 Language Specification

See also: My original, non-interactive demo; Perl equality table (not mine)

Transitive closure of ==

== isn't transitive (that is, x == y and y == z does not imply that x == z), but what if it were? I mean, one way to do it is just to not do type conversion (like ===), but what if it did all the type conversion it currently does and still was transitive, what would that look like?

Cases:

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 && obj.constructor != 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 constructor
	// was assigned explicitly, 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;
	// 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:
	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
	// +toPrimitive(x) == z
	if(typeof x_prim != 'symbol' && typeof y_prim != 'symbol' &&
	   +x_prim == +y_prim)
		return true;
	return false;
}