UNPKG

deep-equal-x

Version:

node's deepEqual and deepStrictEqual algorithm.

335 lines (319 loc) 12.2 kB
/** * @file * <a href="https://travis-ci.org/Xotic750/deep-equal-x" * title="Travis status"> * <img src="https://travis-ci.org/Xotic750/deep-equal-x.svg?branch=master" * alt="Travis status" height="18"> * </a> * <a href="https://david-dm.org/Xotic750/deep-equal-x" * title="Dependency status"> * <img src="https://david-dm.org/Xotic750/deep-equal-x.svg" * alt="Dependency status" height="18"/> * </a> * <a href="https://david-dm.org/Xotic750/deep-equal-x#info=devDependencies" * title="devDependency status"> * <img src="https://david-dm.org/Xotic750/deep-equal-x/dev-status.svg" * alt="devDependency status" height="18"/> * </a> * <a href="https://badge.fury.io/js/deep-equal-x" title="npm version"> * <img src="https://badge.fury.io/js/deep-equal-x.svg" * alt="npm version" height="18"> * </a> * * node's deepEqual and deepStrictEqual algorithm. * * <h2>ECMAScript compatibility shims for legacy JavaScript engines</h2> * `es5-shim.js` monkey-patches a JavaScript context to contain all EcmaScript 5 * methods that can be faithfully emulated with a legacy JavaScript engine. * * `es5-sham.js` monkey-patches other ES5 methods as closely as possible. * For these methods, as closely as possible to ES5 is not very close. * Many of these shams are intended only to allow code to be written to ES5 * without causing run-time errors in older engines. In many cases, * this means that these shams cause many ES5 methods to silently fail. * Decide carefully whether this is what you want. Note: es5-sham.js requires * es5-shim.js to be able to work properly. * * `json3.js` monkey-patches the EcmaScript 5 JSON implimentation faithfully. * * `es6.shim.js` provides compatibility shims so that legacy JavaScript engines * behave as closely as possible to ECMAScript 6 (Harmony). * * @version 1.2.6 * @author Xotic750 <Xotic750@gmail.com> * @copyright Xotic750 * @license {@link <https://opensource.org/licenses/MIT> MIT} * @module deep-equal-x */ /*jslint maxlen:80, es6:false, this:false, white:true */ /*jshint bitwise:true, camelcase:true, curly:true, eqeqeq:true, forin:true, freeze:true, futurehostile:true, latedef:true, newcap:true, nocomma:true, nonbsp:true, singleGroups:true, strict:true, undef:true, unused:true, es3:true, esnext:false, plusplus:true, maxparams:4, maxdepth:2, maxstatements:45, maxcomplexity:26 */ /*global require, module */ ;(function () { 'use strict'; var isDate = require('is-date-object'); var isArguments = require('is-arguments'); var isPrimitive = require('is-primitive'); var isObject = require('is-object'); var isBuffer = require('is-buffer'); var isString = require('is-string'); var isError = require('is-error-x'); var isMap = require('is-map-x'); var isSet = require('is-set-x'); var isNil = require('is-nil-x'); var isRegExp = require('is-regex'); var pIndexOf = Array.prototype.indexOf; var pPush = Array.prototype.push; var pPop = Array.prototype.pop; var pSlice = Array.prototype.slice; var pSome = Array.prototype.some; var pFilter = Array.prototype.filter; var pSort = Array.prototype.sort; var pTest = RegExp.prototype.test; var rToString = RegExp.prototype.toString; var pCharAt = String.prototype.charAt; var pGetTime = Date.prototype.getTime; var $Number = Number; var $keys = Object.keys; var $getPrototypeOf = Object.getPrototypeOf; // Check failure of by-index access of string characters (IE < 9) // and failure of `0 in boxedString` (Rhino) var boxedString = Object('a'); var hasBoxedStringBug = boxedString[0] !== 'a' || !(0 in boxedString); // Used to detect unsigned integer values. var reIsUint = /^(?:0|[1-9]\d*)$/; var hasMapEnumerables = typeof Map === 'function' ? $keys(new Map()) : []; var hasSetEnumerables = typeof Set === 'function' ? $keys(new Set()) : []; var hasErrorEnumerables; try { throw new Error('a'); } catch (e) { hasErrorEnumerables = $keys(e); } /** * Checks if `value` is a valid string index. Specifically for boxed string * bug fix and not general purpose. * * @private * @param {*} value The value to check. * @return {boolean} Returns `true` if `value` is valid index, else `false`. */ function isIndex(value) { var num = -1; if (pTest.call(reIsUint, value)) { num = $Number(value); } return num > -1 && num % 1 === 0 && num < 4294967295; } /** * Get an object's key avoiding boxed string bug. Specifically for boxed * string bug fix and not general purpose. * * @private * @param {Object} object The object to get the `value` from. * @param {string} key The `key` reference to the `value`. * @param {boolean} isStr Is the object a string. * @param {boolean} isIdx Is the `key` a character index. * @return {*} Returns the `value` referenced by the `key`. */ function getItem(object, key, isStr, isIdx) { return isStr && isIdx ? pCharAt.call(object, key) : object[key]; } /** * Filter `keys` of unwanted Error enumerables. Specifically for Error has * unwanted enumerables fix and not general purpose. * * @private * @param {Array} keys The Error object's keys. * @param {Array} unwanted The unwanted keys. * @returns {Array} Returns the filtered keys. */ function filterUnwanted(keys, unwanted) { return unwanted.length ? pFilter.call(keys, function (key) { return pIndexOf.call(unwanted, key) < 0; }) : keys; } /** * Tests for deep equality. Primitive values are compared with the equal * comparison operator ( == ). This only considers enumerable properties. * It does not test object prototypes, attached symbols, or non-enumerable * properties. This can lead to some potentially surprising results. If * `strict` is `true` then Primitive values are compared with the strict * equal comparison operator ( === ). * * @private * @param {*} actual First comparison object. * @param {*} expected Second comparison object. * @param {boolean} [strict] Comparison mode. If set to `true` use `===`. * @param {Object} previousStack The circular stack. * @return {boolean} `true` if `actual` and `expected` are deemed equal, * otherwise `false`. */ function baseDeepEqual(actual, expected, strict, previousStack) { // 7.1. All identical values are equivalent, as determined by ===. if (actual === expected) { return true; } if (isBuffer(actual) && isBuffer(expected)) { return actual.length === expected.length && !pSome.call(actual, function (item, index) { return item !== expected[index]; }); } // 7.2. If the expected value is a Date object, the actual value is // equivalent if it is also a Date object that refers to the same time. if (isDate(actual) && isDate(expected)) { return pGetTime.call(actual) === pGetTime.call(expected); } // 7.3 If the expected value is a RegExp object, the actual value is // equivalent if it is also a RegExp object with the same `source` and // properties (`global`, `multiline`, `lastIndex`, `ignoreCase` & `sticky`). if (isRegExp(actual) && isRegExp(expected)) { return rToString.call(actual) === rToString.call(expected) && actual.lastIndex === expected.lastIndex; } // 7.4. Other pairs that do not both pass typeof value == 'object', // equivalence is determined by == or strict ===. if (!isObject(actual) && !isObject(expected)) { /*jshint eqeqeq:false */ return strict ? actual === expected : actual == expected; } // 7.5 For all other Object pairs, including Array objects, equivalence is // determined by having the same number of owned properties (as verified // with Object.prototype.hasOwnProperty.call), the same set of keys // (although not necessarily the same order), equivalent values for every // corresponding key, and an identical 'prototype' property. Note: this // accounts for both named and indexed properties on Arrays. if (isNil(actual) || isNil(expected)) { return false; } /*jshint eqnull:false */ // This only considers enumerable properties. It does not test object // prototypes, attached symbols, or non-enumerable properties. This can // lead to some potentially surprising results. if (strict && $getPrototypeOf(actual) !== $getPrototypeOf(expected)) { return false; } // if one is actual primitive, the other must be same if (isPrimitive(actual) || isPrimitive(expected)) { return actual === expected; } var ka = isArguments(actual); var kb = isArguments(expected); if (ka && !kb || !ka && kb) { return false; } if (ka) { if (ka.length !== kb.length) { return false; } return baseDeepEqual( pSlice.call(actual), pSlice.call(expected), strict, null ); } ka = $keys(actual); kb = $keys(expected); // having the same number of owned properties (keys incorporates // hasOwnProperty) if (ka.length !== kb.length) { return false; } if (isObject(actual)) { if (isError(actual)) { ka = filterUnwanted(ka, hasErrorEnumerables); } else if (isMap(actual)) { ka = filterUnwanted(ka, hasMapEnumerables); } else if (isSet(actual)) { ka = filterUnwanted(ka, hasSetEnumerables); } } if (isObject(expected)) { if (isError(expected)) { kb = filterUnwanted(kb, hasErrorEnumerables); } else if (isMap(expected)) { kb = filterUnwanted(kb, hasMapEnumerables); } else if (isSet(expected)) { kb = filterUnwanted(kb, hasSetEnumerables); } } //the same set of keys (although not necessarily the same order), pSort.call(ka); pSort.call(kb); var aIsString, bIsString; if (hasBoxedStringBug) { aIsString = isString(actual); bIsString = isString(expected); } //~~~cheap key test //equivalent values for every corresponding key, and //~~~possibly expensive deep test return !pSome.call(ka, function (key, index) { if (key !== kb[index]) { return true; } var isIdx = (aIsString || bIsString) && isIndex(key); var stack = previousStack ? previousStack : [actual]; var item = getItem(actual, key, aIsString, isIdx); var isPrim = isPrimitive(item); if (!isPrim) { if (pIndexOf.call(stack, item) > -1) { throw new RangeError('Circular object'); } pPush.call(stack, item); } var result = !baseDeepEqual( item, getItem(expected, key, bIsString, isIdx), strict, stack ); if (!isPrim) { pPop.call(stack); } return result; }); } /** * Tests for deep equality. Primitive values are compared with the equal * comparison operator ( == ). This only considers enumerable properties. * It does not test object prototypes, attached symbols, or non-enumerable * properties. This can lead to some potentially surprising results. If * `strict` is `true` then Primitive values are compared with the strict * equal comparison operator ( === ). * * @param {*} actual First comparison object. * @param {*} expected Second comparison object. * @param {boolean} [strict] Comparison mode. If set to `true` use `===`. * @return {boolean} `true` if `actual` and `expected` are deemed equal, * otherwise `false`. * @see https://nodejs.org/api/assert.html * @example * var deepEqual = require('deep-equal-x'); * * deepEqual(Error('a'), Error('b')); * // => true * // This does not return `false` because the properties on the Error object * // are non-enumerable: * * deepEqual(4, '4'); * // => true * * deepEqual({ a: 4, b: '1' }, { b: '1', a: 4 }); * // => true * * deepEqual(new Date(), new Date(2000, 3, 14)); * // => false * * deepEqual(4, '4', true); * // => false */ module.exports = function deepEqual(actual, expected, strict) { return baseDeepEqual(actual, expected, strict); }; }());