Last active
July 19, 2016 03:33
-
-
Save nem035/9f195e15b83d464d8cce8768f93b9e90 to your computer and use it in GitHub Desktop.
(Loose) implementations of the double equals (==) and triple equals (===) algorithms in JavaScript (ES5 & ES6), showing the steps the JavaScript engine takes to evaluate both operations and how the operations are related. The only major ES6 addition for both operations is handling of Symbols.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// An implementation of the double equals (==) algorithm in JavaScript ES5 | |
// Spec: http://www.ecma-international.org/ecma-262/5.1/#sec-11.9.3 | |
function doubleEquals(x, y) { | |
// if x and y have the same type | |
if (areSameType(x, y)) { | |
// return the === comparison (based on the spec, this should run the code from step 2 of the === algorithm but this is a cleaner representation) | |
return tripleEquals(x, y); | |
} | |
// x and y have a different type | |
else { | |
// if x is null and y is undefined, | |
// return true | |
if (isNull(x) && isUndefined(y)) { | |
return true; | |
} | |
// if x is undefined and y is null, | |
// return true | |
if (isUndefined(x) && isNull(y)) { | |
return true; | |
} | |
// if x is a number and y is a string | |
// return the comparison x == toNumber(y) | |
if (isNumber(x) && isString(y)) { | |
return doubleEquals(x, toNumber(y)); | |
} | |
// if x is a string and y is a number | |
// return the comparison toNumber(x) == y | |
if (isString(x) && isNumber(y)) { | |
return doubleEquals(toNumber(x), y); | |
} | |
// if x is a boolean, | |
// return the result of the comparison toNumber(x) == y | |
if (isBoolean(x)) { | |
return doubleEquals(toNumber(x), y); | |
} | |
// if y is a boolean | |
// return the result of the comparison x == toNumber(y) | |
if (isBoolean(y)) { | |
return doubleEquals(x, toNumber(y)); | |
} | |
// if x is either a string or a number and y is an object | |
// return the result of comparison x == toPrimitive(y) | |
if (isStringOrNumber(x) && isObject(y)) { | |
return doubleEquals(x, toPrimitive(y)); | |
} | |
// if x is an object and y is either a string or a number | |
// return the result of the comparison toPrimitive(x) == y | |
if (isObject(x) && isStringOrNumber(y)) { | |
return doubleEquals(toPrimitive(x), y); | |
} | |
} | |
//return false | |
return false; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// An implementation of the triple equals (===) algorithm in JavaScript ES5 | |
// Spec: http://www.ecma-international.org/ecma-262/5.1/#sec-11.9.6 | |
function tripleEquals(x, y) { | |
if (!areSameType(x, y)) { | |
return false; | |
} | |
// if both are undefined, | |
// return true | |
if (isUndefined(x)) { | |
return true; | |
} | |
// if both are null, | |
// return true | |
if (isNull(x)) { | |
return true; | |
} | |
// if both are numbers | |
if (isNumber(x)) { | |
// if x is NaN, | |
// return false | |
if (isNaN(x)) { | |
return false; | |
} | |
// if y is NaN, | |
// return false | |
if (isNaN(y)) { | |
return false; | |
} | |
// if both have equal Number values, | |
// return true | |
if (areEqualNumberValues(x, y)) { | |
return true; | |
} | |
// if x is a positive zero and y is a negative zero, | |
// return true | |
if (isPositiveZero(x) && isNegativeZero(y)) { | |
return true; | |
} | |
// if x is a negative zero and y is a positive zero, | |
// return true | |
if (isNegativeZero(x) && isPositiveZero(y)) { | |
return true; | |
} | |
// otherwise return false | |
return false; | |
} | |
// if both are strings | |
if (isString(x)) { | |
// if both are the exactly same sequence of characters | |
// (same length and same characters in corresponding positions), | |
// return true | |
if (areAllCharsEqualInOrder(x, y)) { | |
return true; | |
} | |
// otherwise return false | |
return false; | |
} | |
// if both are booleans | |
if (isBoolean(x)) { | |
// if both are true or both are false, | |
// return true | |
if (areBothTrue(x, y) || areBothFalse(x, y)) { | |
return true; | |
} | |
// otherwise return false | |
return false; | |
} | |
// if both refer to the same object, | |
// return true | |
if (areEqualReferences(x, y)) { | |
return true; | |
} | |
// otherwise return false | |
return false; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var testCases = [ | |
null, | |
undefined, | |
0, | |
NaN, | |
'', | |
false, | |
true, | |
1, | |
-1, | |
0, | |
+0, | |
-0, | |
'0', | |
'1', | |
[], | |
[0], | |
[null], | |
[undefined], | |
[''], | |
[false], | |
[1, 2], | |
{}, | |
{ a: 1 }, | |
{ | |
toString: function() { | |
return ''; | |
} | |
}, | |
{ | |
toString: function() { | |
return '0'; | |
} | |
}, | |
{ | |
toString: function() { | |
return null; | |
} | |
}, | |
{ | |
toString: function() { | |
return ''; | |
}, | |
valueOf: function() { | |
return ''; | |
} | |
}, | |
{ | |
toString: function() { | |
return '0'; | |
}, | |
valueOf: function() { | |
return '0'; | |
} | |
}, | |
{ | |
toString: function() { | |
return {}; | |
}, | |
valueOf: function() { | |
return {}; | |
} | |
}, | |
function() {}, | |
function a() { return 1; }, | |
new Date(), | |
]; | |
function runTests() { | |
var EXCEPTION = 'hasException'; | |
var tests = [{ | |
algo: doubleEquals, | |
algoName: 'doubleEquals', | |
operation: function(x, y) { | |
return x == y; | |
}, | |
operator: '==' | |
}, { | |
algo: tripleEquals, | |
algoName: 'tripleEquals', | |
operation: function(x, y) { | |
return x === y; | |
}, | |
operator: '===' | |
}]; | |
tests.forEach(function(test) { | |
testCases.forEach(function(case1) { | |
testCases.forEach(function(case2) { | |
var result; | |
try { | |
result = test.algo(case1, case2); | |
} catch(e) { | |
result = EXCEPTION; | |
} | |
var expected; | |
try { | |
expected = test.operation(case1, case2); | |
} catch(e) { | |
expected = EXCEPTION; | |
} | |
if (result !== expected) { | |
console.error( | |
(case1) + ' ' + (test.operator) + ' ' + (case2) + ' returns ' + (expected) + | |
', ' + test.algoName + '(' + (case1) + ', ' + (case2) + ') returns ' + (result) | |
); | |
} | |
}); | |
}); | |
}); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// An implementation of the double equals (==) algorithm in JavaScript ES6 | |
// Spec: http://www.ecma-international.org/ecma-262/6.0/#sec-abstract-equality-comparison | |
function doubleEquals(x, y) { | |
// if x and y have the same type | |
if (areSameType(x, y)) { | |
// return the result of performing Strict Equality Comparison x === y. | |
return tripleEquals(x, y); | |
} | |
// x and y have a different type | |
else { | |
// if x is null and y is undefined, | |
// return true | |
if (isNull(x) && isUndefined(y)) { | |
return true; | |
} | |
// if x is undefined and y is null, | |
// return true | |
if (isUndefined(x) && isNull(y)) { | |
return true; | |
} | |
// if x is a number and y is a string, | |
// return the comparison x == toNumber(y) | |
if (isNumber(x) && isString(y)) { | |
return doubleEquals(x, toNumber(y)); | |
} | |
// if x is a string and y is a number, | |
// return the comparison toNumber(x) == y | |
if (isString(x) && isNumber(y)) { | |
return doubleEquals(toNumber(x), y); | |
} | |
// if x is a boolean, | |
// return the result of the comparison toNumber(x) == y | |
if (isBoolean(x)) { | |
return doubleEquals(toNumber(x), y); | |
} | |
// if y is a boolean, | |
// return the result of the comparison x == toNumber(y) | |
if (isBoolean(y)) { | |
return doubleEquals(x, toNumber(y)); | |
} | |
// if x is either a string, a number or a symbol and y is an object, | |
// return the result of comparison x == toPrimitive(y) | |
if (isStringOrNumberOrSymbol(x) && isObject(y)) { | |
return doubleEquals(x, toPrimitive(y)); | |
} | |
// if x is an object and y is either a string, a number or a symbol, | |
// return the result of the comparison toPrimitive(x) == y | |
if (isObject(x) && isStringOrNumberOrSymbol(y)) { | |
return doubleEquals(toPrimitive(x), y); | |
} | |
} | |
// otherwise return false | |
return false; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// An implementation of the triple equals (===) algorithm in JavaScript ES5 | |
// Spec: http://www.ecma-international.org/ecma-262/5.1/#sec-11.9.6 | |
function tripleEquals(x, y) { | |
if (!areSameType(x, y)) { | |
return false; | |
} | |
// if both are undefined, | |
// return true | |
if (isUndefined(x)) { | |
return true; | |
} | |
// if both are null, | |
// return true | |
if (isNull(x)) { | |
return true; | |
} | |
// if both are numbers | |
if (isNumber(x)) { | |
// if x is NaN, | |
// return false | |
if (isNaN(x)) { | |
return false; | |
} | |
// if y is NaN, | |
// return false | |
if (isNaN(y)) { | |
return false; | |
} | |
// if both have equal Number values, | |
// return true | |
if (areEqualNumberValues(x, y)) { | |
return true; | |
} | |
// if x is a positive zero and y is a negative zero, | |
// return true | |
if (isPositiveZero(x) && isNegativeZero(y)) { | |
return true; | |
} | |
// if x is a negative zero and y is a positive zero, | |
// return true | |
if (isNegativeZero(x) && isPositiveZero(y)) { | |
return true; | |
} | |
// otherwise return false | |
return false; | |
} | |
// if both are strings | |
if (isString(x)) { | |
// if both are the exactly same sequence of code units | |
// (same length and same code units in corresponding indices), | |
// return true | |
if (areAllCodeUnitsEqualInOrder(x, y)) { | |
return true; | |
} | |
// otherwise return false | |
return false; | |
} | |
// if both are booleans | |
if (isBoolean(x)) { | |
// if both are true or both are false, | |
// return true | |
if (areBothTrue(x, y) || areBothFalse(x, y)) { | |
return true; | |
} | |
// otherwise return false | |
return false; | |
} | |
// if both are the same Symbol value, | |
// return true | |
if (areSameSymbolValue(x, y)) { | |
return true; | |
} | |
// if both refer to the same object, | |
// return true | |
if (areEqualReferences(x, y)) { | |
return true; | |
} | |
// otherwise return false | |
return false; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const testCases = [ | |
null, | |
undefined, | |
0, | |
NaN, | |
'', | |
false, | |
true, | |
1, | |
-1, | |
0, | |
+0, | |
-0, | |
'0', | |
'1', | |
[], | |
[0], | |
[null], | |
[undefined], | |
[''], | |
[false], | |
[1, 2], | |
{}, | |
{ a: 1 }, | |
{ | |
toString() { | |
return ''; | |
} | |
}, | |
{ | |
toString() { | |
return '0'; | |
} | |
}, | |
{ | |
toString() { | |
return null; | |
} | |
}, | |
{ | |
toString() { | |
return ''; | |
}, | |
valueOf() { | |
return ''; | |
} | |
}, | |
{ | |
toString() { | |
return '0'; | |
}, | |
valueOf() { | |
return '0'; | |
} | |
}, | |
{ | |
toString() { | |
return {}; | |
}, | |
valueOf() { | |
return {}; | |
} | |
}, | |
function() {}, | |
function a() { return 1; }, | |
() => {}, | |
() => 1, | |
new Date(), | |
Symbol(), | |
Symbol(1), | |
Symbol('a') | |
]; | |
function runTests() { | |
const EXCEPTION = 'hasException'; | |
const tests = [{ | |
algo: doubleEquals, | |
algoName: 'doubleEquals', | |
operation: (x, y) => x == y, | |
operator: '==' | |
}, { | |
algo: tripleEquals, | |
algoName: 'tripleEquals', | |
operation: (x, y) => x === y, | |
operator: '===' | |
}]; | |
tests.forEach(({ algo, algoName, operation, operator }) => { | |
testCases.forEach((case1) => { | |
testCases.forEach((case2) => { | |
let result; | |
try { | |
result = algo(case1, case2); | |
} catch(e) { | |
result = EXCEPTION; | |
} | |
let expected; | |
try { | |
expected = operation(case1, case2); | |
} catch(e) { | |
expected = EXCEPTION; | |
} | |
if (result !== expected) { | |
console.error( | |
`${case1} ${operator} ${case2} returns ${expected}, ${algoName}(${case1}, ${case2}) returns ${result}` | |
); | |
} | |
}); | |
}); | |
}); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function toPrimitive(x) { | |
if (isObject(x)) { | |
return defaultValue(x); | |
} | |
return x; | |
} | |
function defaultValue(x) { | |
var methods = isDate(x) ? ['toString', 'valueOf'] : ['valueOf', 'toString']; | |
var result; | |
var success = methods.some(function(method) { | |
if (isFunction(x[method])) { | |
var temp = x[method](); | |
if (isPrimitive(temp)) { | |
result = temp; | |
return true; | |
} | |
} | |
}); | |
if (!success) { | |
throw new TypeError('Cannot convert object to primitive value'); | |
} | |
return result; | |
} | |
function areSameType(x, y) { | |
return areEqual(type(x), type(y)); | |
} | |
function type(x) { | |
if (isNull(x)) { | |
return 'null'; | |
} | |
if (Object.prototype.toString.call(x) === '[object Date]') { | |
return 'date'; | |
} | |
return typeof (x); | |
} | |
function areEqual(x, y) { | |
return x === y; | |
} | |
function areEqualNumberValues(x, y) { | |
return areEqual(x, y); | |
} | |
function areAllCharsEqualInOrder(x, y) { | |
if (!areEqual(x.length, y.length)) { | |
return false; | |
} | |
for (var i = 0; i < x.length; i++) { | |
if (!areEqual(x.charAt(i), y.charAt(i))) { | |
return false; | |
} | |
} | |
return true; | |
} | |
function areBothTrue(x, y) { | |
return areEqual(x, true) && areEqual(y, true); | |
} | |
function areBothFalse(x, y) { | |
return areEqual(x, false) && areEqual(y, false); | |
} | |
function areEqualNumbers(x, y) { | |
return areEqual(toNumber(x), toNumber(y)); | |
} | |
function toNumber(x) { | |
return Number(x); | |
} | |
function areEqualReferences(x, y) { | |
return areEqual(x, y); | |
} | |
function isNaN(x) { | |
return !areEqual(x, x); | |
} | |
function isNegativeZero(x) { | |
return areEqual(x, 0) && areEqual(1 / x, -Infinity); | |
} | |
function isPositiveZero(x) { | |
return areEqual(x, 0) && !isNegativeZero(x); | |
} | |
function isObject(x) { | |
return areEqual(type(x), 'object') || isFunction(x) || isDate(x); | |
} | |
function isNull(x) { | |
return areEqual(x, null); | |
} | |
function isUndefined(x) { | |
return areEqual(x, void 0); | |
} | |
function isNumber(x) { | |
return areEqual(type(x), 'number'); | |
} | |
function isString(x) { | |
return areEqual(type(x), 'string'); | |
} | |
function isStringOrNumber(x) { | |
return isString(x) || isNumber(x); | |
} | |
function isBoolean(x) { | |
return areEqual(type(x), 'boolean'); | |
} | |
function isFunction(x) { | |
return areEqual(type(x), 'function'); | |
} | |
function isDate(x) { | |
return areEqual(type(x), 'date'); | |
} | |
// ES6 engine will only ever have symbols | |
function isPrimitive(x) { | |
return ~['undefined', 'null', 'boolean', 'number', 'string', 'symbol'].indexOf(type(x)); | |
} | |
// ES6 engine exclusive method | |
function isStringOrNumberOrSymbol(x) { | |
return isString(x) || isNumber(x) || isSymbol(x); | |
} | |
// ES6 engine exclusive method | |
function isSymbol(x) { | |
return areEqual(type(x), 'symbol'); | |
} | |
// ES6 engine exclusive method | |
function areSameSymbolValue(x, y) { | |
return false; // symbol values are never the same ??? | |
} | |
// ES6 engine exclusive method | |
function areAllCodeUnitsEqualInOrder(x, y) { | |
if (!areEqual(x.length, y.length)) { | |
return false; | |
} | |
for (var i = 0; i < x.length; i++) { | |
if (!areEqual(x.charCodeAt(i), y.charCodeAt(i))) { | |
return false; | |
} | |
} | |
return true; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment