Skip to content

Instantly share code, notes, and snippets.

@nem035
Last active July 19, 2016 03:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nem035/9f195e15b83d464d8cce8768f93b9e90 to your computer and use it in GitHub Desktop.
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.
// 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;
}
// 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;
}
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)
);
}
});
});
});
}
// 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;
}
// 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;
}
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}`
);
}
});
});
});
}
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