├── .eslintrc.yml ├── .github └── FUNDING.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── benchmark ├── .eslintrc.yml ├── index.js └── package.json ├── build.js ├── es6 ├── index.d.ts └── react.d.ts ├── index.d.ts ├── package.json ├── react.d.ts ├── spec ├── .eslintrc.yml ├── es6tests.js ├── index.spec.js ├── react.spec.js └── tests.js └── src └── index.jst /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | node: true 3 | extends: 'eslint:recommended' 4 | rules: 5 | indent: [ 2, 2, { SwitchCase: 1 } ] 6 | no-trailing-spaces: 2 7 | quotes: [ 2, single, avoid-escape ] 8 | linebreak-style: [ 2, unix ] 9 | semi: [ 2, always ] 10 | valid-jsdoc: [ 2, { requireReturn: false } ] 11 | no-invalid-this: 2 12 | no-unused-vars: [ 2, { args: none } ] 13 | no-console: [ 2, { allow: [ warn, error ] } ] 14 | block-scoped-var: 2 15 | curly: [ 2, multi-or-nest, consistent ] 16 | dot-location: [ 2, property ] 17 | dot-notation: 2 18 | no-else-return: 2 19 | no-eq-null: 2 20 | no-fallthrough: 2 21 | no-return-assign: 2 22 | strict: [ 2, global ] 23 | no-use-before-define: [ 2, nofunc ] 24 | callback-return: 2 25 | no-path-concat: 2 26 | no-empty: 0 27 | globals: 28 | document: false 29 | Element: false 30 | beforeEach: false 31 | afterEach: false 32 | xit: false 33 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: epoberezkin 2 | tidelift: "npm/fast-deep-equal" 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # Lock files 58 | package-lock.json 59 | yarn.lock 60 | 61 | # dotenv environment variables file 62 | .env 63 | 64 | .DS_Store 65 | 66 | # generated files 67 | index.js 68 | react.js 69 | 70 | .idea 71 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "10" 4 | - "12" 5 | - "13" 6 | - "14" 7 | after_script: 8 | - coveralls < coverage/lcov.info 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Evgeny Poberezkin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fast-deep-equal 2 | The fastest deep equal with ES6 Map, Set and Typed arrays support. 3 | 4 | [![Build Status](https://travis-ci.org/epoberezkin/fast-deep-equal.svg?branch=master)](https://travis-ci.org/epoberezkin/fast-deep-equal) 5 | [![npm](https://img.shields.io/npm/v/fast-deep-equal.svg)](https://www.npmjs.com/package/fast-deep-equal) 6 | [![Coverage Status](https://coveralls.io/repos/github/epoberezkin/fast-deep-equal/badge.svg?branch=master)](https://coveralls.io/github/epoberezkin/fast-deep-equal?branch=master) 7 | 8 | 9 | ## Install 10 | 11 | ```bash 12 | npm install fast-deep-equal 13 | ``` 14 | 15 | 16 | ## Features 17 | 18 | - ES5 compatible 19 | - works in node.js (8+) and browsers (IE9+) 20 | - checks equality of Date and RegExp objects by value. 21 | 22 | ES6 equal (`require('fast-deep-equal/es6')`) also supports: 23 | - Maps 24 | - Sets 25 | - Typed arrays 26 | 27 | 28 | ## Usage 29 | 30 | ```javascript 31 | var equal = require('fast-deep-equal'); 32 | console.log(equal({foo: 'bar'}, {foo: 'bar'})); // true 33 | ``` 34 | 35 | To support ES6 Maps, Sets and Typed arrays equality use: 36 | 37 | ```javascript 38 | var equal = require('fast-deep-equal/es6'); 39 | console.log(equal(Int16Array([1, 2]), Int16Array([1, 2]))); // true 40 | ``` 41 | 42 | To use with React (avoiding the traversal of React elements' _owner 43 | property that contains circular references and is not needed when 44 | comparing the elements - borrowed from [react-fast-compare](https://github.com/FormidableLabs/react-fast-compare)): 45 | 46 | ```javascript 47 | var equal = require('fast-deep-equal/react'); 48 | var equal = require('fast-deep-equal/es6/react'); 49 | ``` 50 | 51 | 52 | ## Performance benchmark 53 | 54 | Node.js v12.6.0: 55 | 56 | ``` 57 | fast-deep-equal x 261,950 ops/sec ±0.52% (89 runs sampled) 58 | fast-deep-equal/es6 x 212,991 ops/sec ±0.34% (92 runs sampled) 59 | fast-equals x 230,957 ops/sec ±0.83% (85 runs sampled) 60 | nano-equal x 187,995 ops/sec ±0.53% (88 runs sampled) 61 | shallow-equal-fuzzy x 138,302 ops/sec ±0.49% (90 runs sampled) 62 | underscore.isEqual x 74,423 ops/sec ±0.38% (89 runs sampled) 63 | lodash.isEqual x 36,637 ops/sec ±0.72% (90 runs sampled) 64 | deep-equal x 2,310 ops/sec ±0.37% (90 runs sampled) 65 | deep-eql x 35,312 ops/sec ±0.67% (91 runs sampled) 66 | ramda.equals x 12,054 ops/sec ±0.40% (91 runs sampled) 67 | util.isDeepStrictEqual x 46,440 ops/sec ±0.43% (90 runs sampled) 68 | assert.deepStrictEqual x 456 ops/sec ±0.71% (88 runs sampled) 69 | 70 | The fastest is fast-deep-equal 71 | ``` 72 | 73 | To run benchmark (requires node.js 6+): 74 | 75 | ```bash 76 | npm run benchmark 77 | ``` 78 | 79 | __Please note__: this benchmark runs against the available test cases. To choose the most performant library for your application, it is recommended to benchmark against your data and to NOT expect this benchmark to reflect the performance difference in your application. 80 | 81 | 82 | ## Enterprise support 83 | 84 | fast-deep-equal package is a part of [Tidelift enterprise subscription](https://tidelift.com/subscription/pkg/npm-fast-deep-equal?utm_source=npm-fast-deep-equal&utm_medium=referral&utm_campaign=enterprise&utm_term=repo) - it provides a centralised commercial support to open-source software users, in addition to the support provided by software maintainers. 85 | 86 | 87 | ## Security contact 88 | 89 | To report a security vulnerability, please use the 90 | [Tidelift security contact](https://tidelift.com/security). 91 | Tidelift will coordinate the fix and disclosure. Please do NOT report security vulnerability via GitHub issues. 92 | 93 | 94 | ## License 95 | 96 | [MIT](https://github.com/epoberezkin/fast-deep-equal/blob/master/LICENSE) 97 | -------------------------------------------------------------------------------- /benchmark/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | parserOptions: 2 | ecmaVersion: 2016 3 | rules: 4 | no-invalid-this: 0 5 | no-console: 0 6 | -------------------------------------------------------------------------------- /benchmark/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assertDeepStrictEqual = require('assert').deepStrictEqual; 4 | const tests = require('../spec/tests'); 5 | const Benchmark = require('benchmark'); 6 | const suite = new Benchmark.Suite; 7 | 8 | 9 | const equalPackages = { 10 | 'fast-deep-equal': require('..'), 11 | 'fast-deep-equal/es6': require('../es6'), 12 | 'fast-equals': require('fast-equals').deepEqual, 13 | 'nano-equal': true, 14 | 'shallow-equal-fuzzy': true, 15 | 'underscore.isEqual': require('underscore').isEqual, 16 | 'lodash.isEqual': require('lodash').isEqual, 17 | 'deep-equal': true, 18 | 'deep-eql': true, 19 | 'ramda.equals': require('ramda').equals, 20 | 'util.isDeepStrictEqual': require('util').isDeepStrictEqual, 21 | 'assert.deepStrictEqual': (a, b) => { 22 | try { assertDeepStrictEqual(a, b); return true; } 23 | catch(e) { return false; } 24 | } 25 | }; 26 | 27 | 28 | for (const equalName in equalPackages) { 29 | let equalFunc = equalPackages[equalName]; 30 | if (equalFunc === true) equalFunc = require(equalName); 31 | 32 | for (const testSuite of tests) { 33 | for (const test of testSuite.tests) { 34 | try { 35 | if (equalFunc(test.value1, test.value2) !== test.equal) 36 | console.error('different result', equalName, testSuite.description, test.description); 37 | } catch(e) { 38 | console.error(equalName, testSuite.description, test.description, e); 39 | } 40 | } 41 | } 42 | 43 | suite.add(equalName, function() { 44 | for (const testSuite of tests) { 45 | for (const test of testSuite.tests) { 46 | if (test.description != 'pseudo array and equivalent array are not equal') 47 | equalFunc(test.value1, test.value2); 48 | } 49 | } 50 | }); 51 | } 52 | 53 | console.log(); 54 | 55 | suite 56 | .on('cycle', (event) => console.log(String(event.target))) 57 | .on('complete', function () { 58 | console.log('The fastest is ' + this.filter('fastest').map('name')); 59 | }) 60 | .run({async: true}); 61 | -------------------------------------------------------------------------------- /benchmark/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "devDependencies": { 4 | "benchmark": "^2.1.4", 5 | "deep-eql": "latest", 6 | "deep-equal": "latest", 7 | "fast-equals": "latest", 8 | "lodash": "latest", 9 | "nano-equal": "latest", 10 | "ramda": "latest", 11 | "shallow-equal-fuzzy": "latest", 12 | "underscore": "latest" 13 | } 14 | } -------------------------------------------------------------------------------- /build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var fs = require('fs'); 4 | var doT = require('dot'); 5 | doT.templateSettings.strip = false; 6 | 7 | var jst = doT.compile(fs.readFileSync('./src/index.jst', 'utf8')); 8 | fs.writeFileSync('./index.js', jst({es6: false})); 9 | fs.writeFileSync('./react.js', jst({es6: false, react: true})); 10 | try { fs.mkdirSync('./es6'); } catch(e) {} 11 | fs.writeFileSync('./es6/index.js', jst({es6: true})); 12 | fs.writeFileSync('./es6/react.js', jst({es6: true, react: true})); 13 | -------------------------------------------------------------------------------- /es6/index.d.ts: -------------------------------------------------------------------------------- 1 | declare function equal(actual: any, expected: T): actual is T 2 | export = equal 3 | -------------------------------------------------------------------------------- /es6/react.d.ts: -------------------------------------------------------------------------------- 1 | declare function equal(actual: any, expected: T): actual is T 2 | export = equal 3 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare function equal(actual: any, expected: T): actual is T 2 | export = equal 3 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fast-deep-equal", 3 | "version": "3.1.3", 4 | "description": "Fast deep equal", 5 | "main": "index.js", 6 | "scripts": { 7 | "eslint": "eslint *.js benchmark/*.js spec/*.js", 8 | "build": "node build", 9 | "benchmark": "npm i && npm run build && cd ./benchmark && npm i && node ./", 10 | "test-spec": "mocha spec/*.spec.js -R spec", 11 | "test-cov": "nyc npm run test-spec", 12 | "test-ts": "tsc --target ES5 --noImplicitAny index.d.ts", 13 | "test": "npm run build && npm run eslint && npm run test-ts && npm run test-cov", 14 | "prepublish": "npm run build" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/epoberezkin/fast-deep-equal.git" 19 | }, 20 | "keywords": [ 21 | "fast", 22 | "equal", 23 | "deep-equal" 24 | ], 25 | "author": "Evgeny Poberezkin", 26 | "license": "MIT", 27 | "bugs": { 28 | "url": "https://github.com/epoberezkin/fast-deep-equal/issues" 29 | }, 30 | "homepage": "https://github.com/epoberezkin/fast-deep-equal#readme", 31 | "devDependencies": { 32 | "coveralls": "^3.1.0", 33 | "dot": "^1.1.2", 34 | "eslint": "^7.2.0", 35 | "mocha": "^7.2.0", 36 | "nyc": "^15.1.0", 37 | "pre-commit": "^1.2.2", 38 | "react": "^16.12.0", 39 | "react-test-renderer": "^16.12.0", 40 | "sinon": "^9.0.2", 41 | "typescript": "^3.9.5" 42 | }, 43 | "nyc": { 44 | "exclude": [ 45 | "**/spec/**", 46 | "node_modules" 47 | ], 48 | "reporter": [ 49 | "lcov", 50 | "text-summary" 51 | ] 52 | }, 53 | "files": [ 54 | "index.js", 55 | "index.d.ts", 56 | "react.js", 57 | "react.d.ts", 58 | "es6/" 59 | ], 60 | "types": "index.d.ts" 61 | } 62 | -------------------------------------------------------------------------------- /react.d.ts: -------------------------------------------------------------------------------- 1 | declare function equal(actual: any, expected: T): actual is T 2 | export = equal 3 | -------------------------------------------------------------------------------- /spec/.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | es6: true 3 | rules: 4 | no-console: 0 5 | globals: 6 | describe: false 7 | it: false 8 | BigInt: false 9 | BigUint64Array: false 10 | -------------------------------------------------------------------------------- /spec/es6tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | class MyMap extends Map {} 4 | class MySet extends Set {} 5 | var emptyObj = {}; 6 | 7 | var skipBigInt = typeof BigInt == 'undefined'; 8 | var skipBigIntArray = typeof BigUint64Array == 'undefined'; 9 | 10 | module.exports = [ 11 | { 12 | description: 'bigint', 13 | tests: [ 14 | { 15 | description: 'equal bigints', 16 | value1: skipBigInt || BigInt(1), 17 | value2: skipBigInt || BigInt(1), 18 | equal: true, 19 | skip: skipBigInt 20 | }, 21 | { 22 | description: 'not equal bigints', 23 | value1: skipBigInt || BigInt(1), 24 | value2: skipBigInt || BigInt(2), 25 | equal: false, 26 | skip: skipBigInt 27 | } 28 | ] 29 | }, 30 | 31 | { 32 | description: 'Maps', 33 | tests: [ 34 | { 35 | description: 'empty maps are equal', 36 | value1: new Map, 37 | value2: new Map, 38 | equal: true 39 | }, 40 | { 41 | description: 'empty maps of different class are not equal', 42 | value1: new Map, 43 | value2: new MyMap, 44 | equal: false 45 | }, 46 | { 47 | description: 'equal maps (same key "order")', 48 | value1: map({a: 1, b: '2'}), 49 | value2: map({a: 1, b: '2'}), 50 | equal: true 51 | }, 52 | { 53 | description: 'not equal maps (same key "order" - instances of different classes)', 54 | value1: map({a: 1, b: '2'}), 55 | value2: myMap({a: 1, b: '2'}), 56 | equal: false 57 | }, 58 | { 59 | description: 'equal maps (different key "order")', 60 | value1: map({a: 1, b: '2'}), 61 | value2: map({b: '2', a: 1}), 62 | equal: true 63 | }, 64 | { 65 | description: 'equal maps (different key "order" - instances of the same subclass)', 66 | value1: myMap({a: 1, b: '2'}), 67 | value2: myMap({b: '2', a: 1}), 68 | equal: true 69 | }, 70 | { 71 | description: 'not equal maps (extra key)', 72 | value1: map({a: 1, b: '2'}), 73 | value2: map({a: 1, b: '2', c: []}), 74 | equal: false 75 | }, 76 | { 77 | description: 'not equal maps (different key value)', 78 | value1: map({a: 1, b: '2', c: 3}), 79 | value2: map({a: 1, b: '2', c: 4}), 80 | equal: false 81 | }, 82 | { 83 | description: 'not equal maps (different keys)', 84 | value1: map({a: 1, b: '2', c: 3}), 85 | value2: map({a: 1, b: '2', d: 3}), 86 | equal: false 87 | }, 88 | { 89 | description: 'equal maps (same sub-keys)', 90 | value1: map({ a: [ map({ b: 'c' }) ] }), 91 | value2: map({ a: [ map({ b: 'c' }) ] }), 92 | equal: true 93 | }, 94 | { 95 | description: 'not equal maps (different sub-key value)', 96 | value1: map({ a: [ map({ b: 'c' }) ] }), 97 | value2: map({ a: [ map({ b: 'd' }) ] }), 98 | equal: false 99 | }, 100 | { 101 | description: 'not equal maps (different sub-key)', 102 | value1: map({ a: [ map({ b: 'c' }) ] }), 103 | value2: map({ a: [ map({ c: 'c' }) ] }), 104 | equal: false 105 | }, 106 | { 107 | description: 'empty map and empty object are not equal', 108 | value1: {}, 109 | value2: new Map, 110 | equal: false 111 | }, 112 | { 113 | description: 'map with extra undefined key is not equal #1', 114 | value1: map({}), 115 | value2: map({foo: undefined}), 116 | equal: false 117 | }, 118 | { 119 | description: 'map with extra undefined key is not equal #2', 120 | value1: map({foo: undefined}), 121 | value2: map({}), 122 | equal: false 123 | }, 124 | { 125 | description: 'maps with extra undefined keys are not equal #3', 126 | value1: map({foo: undefined}), 127 | value2: map({bar: undefined}), 128 | equal: false 129 | }, 130 | { 131 | description: 'null and empty map are not equal', 132 | value1: null, 133 | value2: new Map, 134 | equal: false 135 | }, 136 | { 137 | description: 'undefined and empty map are not equal', 138 | value1: undefined, 139 | value2: new Map, 140 | equal: false 141 | }, 142 | { 143 | description: 'map and a pseudo map are not equal', 144 | value1: map({}), 145 | value2: { 146 | constructor: Map, 147 | size: 0, 148 | has: () => true, 149 | get: () => 1, 150 | }, 151 | equal: false 152 | }, 153 | ] 154 | }, 155 | 156 | { 157 | description: 'Sets', 158 | tests: [ 159 | { 160 | description: 'empty sets are equal', 161 | value1: new Set, 162 | value2: new Set, 163 | equal: true 164 | }, 165 | { 166 | description: 'empty sets of different class are not equal', 167 | value1: new Set, 168 | value2: new MySet, 169 | equal: false 170 | }, 171 | { 172 | description: 'equal sets (same value "order")', 173 | value1: set(['a', 'b']), 174 | value2: set(['a', 'b']), 175 | equal: true 176 | }, 177 | { 178 | description: 'not equal sets (same value "order" - instances of different classes)', 179 | value1: set(['a', 'b']), 180 | value2: mySet(['a', 'b']), 181 | equal: false 182 | }, 183 | { 184 | description: 'equal sets (different value "order")', 185 | value1: set(['a', 'b']), 186 | value2: set(['b', 'a']), 187 | equal: true 188 | }, 189 | { 190 | description: 'equal sets (different value "order" - instances of the same subclass)', 191 | value1: mySet(['a', 'b']), 192 | value2: mySet(['b', 'a']), 193 | equal: true 194 | }, 195 | { 196 | description: 'not equal sets (extra value)', 197 | value1: set(['a', 'b']), 198 | value2: set(['a', 'b', 'c']), 199 | equal: false 200 | }, 201 | { 202 | description: 'not equal sets (different values)', 203 | value1: set(['a', 'b', 'c']), 204 | value2: set(['a', 'b', 'd']), 205 | equal: false 206 | }, 207 | { 208 | description: 'not equal sets (different instances of objects)', 209 | value1: set([ 'a', {} ]), 210 | value2: set([ 'a', {} ]), 211 | equal: false 212 | }, 213 | { 214 | description: 'equal sets (same instances of objects)', 215 | value1: set([ 'a', emptyObj ]), 216 | value2: set([ 'a', emptyObj ]), 217 | equal: true 218 | }, 219 | { 220 | description: 'empty set and empty object are not equal', 221 | value1: {}, 222 | value2: new Set, 223 | equal: false 224 | }, 225 | { 226 | description: 'empty set and empty array are not equal', 227 | value1: [], 228 | value2: new Set, 229 | equal: false 230 | }, 231 | { 232 | description: 'set with extra undefined value is not equal #1', 233 | value1: set([]), 234 | value2: set([undefined]), 235 | equal: false 236 | }, 237 | { 238 | description: 'set with extra undefined value is not equal #2', 239 | value1: set([undefined]), 240 | value2: set([]), 241 | equal: false 242 | }, 243 | { 244 | description: 'set and pseudo set are not equal', 245 | value1: new Set, 246 | value2: { 247 | constructor: Set, 248 | size: 0, 249 | has: () => true, 250 | }, 251 | equal: false 252 | }, 253 | ] 254 | }, 255 | 256 | { 257 | description: 'Typed arrays', 258 | tests: [ 259 | { 260 | description: 'two empty arrays of the same class are equal', 261 | value1: new Int32Array([]), 262 | value2: new Int32Array([]), 263 | equal: true 264 | }, 265 | { 266 | description: 'two empty arrays of the different class are not equal', 267 | value1: new Int32Array([]), 268 | value2: new Int16Array([]), 269 | equal: false 270 | }, 271 | { 272 | description: 'equal arrays', 273 | value1: new Int32Array([1, 2, 3]), 274 | value2: new Int32Array([1, 2, 3]), 275 | equal: true 276 | }, 277 | { 278 | description: 'equal BigUint64Array arrays', 279 | value1: skipBigIntArray || new BigUint64Array(['1', '2', '3']), 280 | value2: skipBigIntArray || new BigUint64Array(['1', '2', '3']), 281 | equal: true, 282 | skip: skipBigIntArray 283 | }, 284 | { 285 | description: 'not equal BigUint64Array arrays', 286 | value1: skipBigIntArray || new BigUint64Array(['1', '2', '3']), 287 | value2: skipBigIntArray || new BigUint64Array(['1', '2', '4']), 288 | equal: false, 289 | skip: skipBigIntArray 290 | }, 291 | { 292 | description: 'not equal arrays (same items, different class)', 293 | value1: new Int32Array([1, 2, 3]), 294 | value2: new Int16Array([1, 2, 3]), 295 | equal: false 296 | }, 297 | { 298 | description: 'not equal arrays (different item)', 299 | value1: new Int32Array([1, 2, 3]), 300 | value2: new Int32Array([1, 2, 4]), 301 | equal: false 302 | }, 303 | { 304 | description: 'not equal arrays (different length)', 305 | value1: new Int32Array([1, 2, 3]), 306 | value2: new Int32Array([1, 2]), 307 | equal: false 308 | }, 309 | { 310 | description: 'pseudo array and equivalent typed array are not equal', 311 | value1: {'0': 1, '1': 2, length: 2, constructor: Int32Array}, 312 | value2: new Int32Array([1, 2]), 313 | equal: false 314 | } 315 | ] 316 | } 317 | ]; 318 | 319 | function map(obj, Class) { 320 | var a = new (Class || Map); 321 | for (var key in obj) 322 | a.set(key, obj[key]); 323 | return a; 324 | } 325 | 326 | function myMap(obj) { 327 | return map(obj, MyMap); 328 | } 329 | 330 | function set(arr, Class) { 331 | var a = new (Class || Set); 332 | for (var value of arr) 333 | a.add(value); 334 | return a; 335 | } 336 | 337 | function mySet(arr) { 338 | return set(arr, MySet); 339 | } 340 | -------------------------------------------------------------------------------- /spec/index.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var equal = require('..'); 4 | var equalReact = require('../react'); 5 | var es6equal = require('../es6'); 6 | var es6equalReact = require('../es6/react'); 7 | var assert = require('assert'); 8 | 9 | testCases(equal, 'equal - standard tests', require('./tests')); 10 | testCases(es6equal, 'es6 equal - standard tests', require('./tests')); 11 | testCases(es6equal, 'es6 equal - es6 tests', require('./es6tests')); 12 | 13 | testCases(equalReact, 'equal react - standard tests', require('./tests')); 14 | testCases(es6equalReact, 'es6 equal react - standard tests', require('./tests')); 15 | testCases(es6equalReact, 'es6 equal react - es6 tests', require('./es6tests')); 16 | 17 | function testCases(equalFunc, suiteName, suiteTests) { 18 | describe(suiteName, function() { 19 | suiteTests.forEach(function (suite) { 20 | describe(suite.description, function() { 21 | suite.tests.forEach(function (test) { 22 | (test.skip ? it.skip : it)(test.description, function() { 23 | assert.strictEqual(equalFunc(test.value1, test.value2), test.equal); 24 | }); 25 | (test.skip ? it.skip : it)(test.description + ' (reverse arguments)', function() { 26 | assert.strictEqual(equalFunc(test.value2, test.value1), test.equal); 27 | }); 28 | }); 29 | }); 30 | }); 31 | }); 32 | } -------------------------------------------------------------------------------- /spec/react.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | const sinon = require('sinon'); 5 | const React = require('react'); 6 | const ReactTestRenderer = require('react-test-renderer'); 7 | 8 | const equal1 = require('../es6/react'); 9 | const equal2 = require('../react'); 10 | 11 | const run = equal => { 12 | class ChildWithShouldComponentUpdate extends React.Component { 13 | shouldComponentUpdate(nextProps) { 14 | // this.props.children is a h1 with a circular reference to its owner, Container 15 | return !equal(this.props, nextProps); 16 | } 17 | render() { 18 | return null; 19 | } 20 | } 21 | 22 | class Container extends React.Component { 23 | render() { 24 | return React.createElement(ChildWithShouldComponentUpdate, { 25 | children: [ 26 | React.createElement('h1', this.props.title || ''), 27 | React.createElement('h2', this.props.subtitle || '') 28 | ] 29 | }); 30 | } 31 | } 32 | 33 | describe('advanced', () => { 34 | let sandbox; 35 | let warnStub; 36 | let childRenderSpy; 37 | 38 | beforeEach(() => { 39 | sandbox = sinon.createSandbox(); 40 | warnStub = sandbox.stub(console, 'warn'); 41 | childRenderSpy = sandbox.spy(ChildWithShouldComponentUpdate.prototype, 'render'); 42 | }); 43 | 44 | afterEach(() => { 45 | sandbox.restore(); 46 | }); 47 | 48 | describe('React', () => { 49 | describe('element (with circular references)', () => { 50 | it('compares without warning or errors', () => { 51 | const testRenderer = ReactTestRenderer.create(React.createElement(Container)); 52 | testRenderer.update(React.createElement(Container)); 53 | assert.strictEqual(warnStub.callCount, 0); 54 | }); 55 | it('elements of same type and props are equal', () => { 56 | const testRenderer = ReactTestRenderer.create(React.createElement(Container)); 57 | testRenderer.update(React.createElement(Container)); 58 | assert.strictEqual(childRenderSpy.callCount, 1); 59 | }); 60 | it('elements of same type with different props are not equal', () => { 61 | const testRenderer = ReactTestRenderer.create(React.createElement(Container)); 62 | testRenderer.update(React.createElement(Container, { title: 'New' })); 63 | assert.strictEqual(childRenderSpy.callCount, 2); 64 | }); 65 | }); 66 | }); 67 | }); 68 | }; 69 | 70 | run(equal1); 71 | run(equal2); 72 | -------------------------------------------------------------------------------- /spec/tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = [ 4 | { 5 | description: 'scalars', 6 | tests: [ 7 | { 8 | description: 'equal numbers', 9 | value1: 1, 10 | value2: 1, 11 | equal: true 12 | }, 13 | { 14 | description: 'not equal numbers', 15 | value1: 1, 16 | value2: 2, 17 | equal: false 18 | }, 19 | { 20 | description: 'number and array are not equal', 21 | value1: 1, 22 | value2: [], 23 | equal: false 24 | }, 25 | { 26 | description: '0 and null are not equal', 27 | value1: 0, 28 | value2: null, 29 | equal: false 30 | }, 31 | { 32 | description: 'equal strings', 33 | value1: 'a', 34 | value2: 'a', 35 | equal: true 36 | }, 37 | { 38 | description: 'not equal strings', 39 | value1: 'a', 40 | value2: 'b', 41 | equal: false 42 | }, 43 | { 44 | description: 'empty string and null are not equal', 45 | value1: '', 46 | value2: null, 47 | equal: false 48 | }, 49 | { 50 | description: 'null is equal to null', 51 | value1: null, 52 | value2: null, 53 | equal: true 54 | }, 55 | { 56 | description: 'equal booleans (true)', 57 | value1: true, 58 | value2: true, 59 | equal: true 60 | }, 61 | { 62 | description: 'equal booleans (false)', 63 | value1: false, 64 | value2: false, 65 | equal: true 66 | }, 67 | { 68 | description: 'not equal booleans', 69 | value1: true, 70 | value2: false, 71 | equal: false 72 | }, 73 | { 74 | description: '1 and true are not equal', 75 | value1: 1, 76 | value2: true, 77 | equal: false 78 | }, 79 | { 80 | description: '0 and false are not equal', 81 | value1: 0, 82 | value2: false, 83 | equal: false 84 | }, 85 | { 86 | description: 'NaN and NaN are equal', 87 | value1: NaN, 88 | value2: NaN, 89 | equal: true 90 | }, 91 | { 92 | description: '0 and -0 are equal', 93 | value1: 0, 94 | value2: -0, 95 | equal: true 96 | }, 97 | { 98 | description: 'Infinity and Infinity are equal', 99 | value1: Infinity, 100 | value2: Infinity, 101 | equal: true 102 | }, 103 | { 104 | description: 'Infinity and -Infinity are not equal', 105 | value1: Infinity, 106 | value2: -Infinity, 107 | equal: false 108 | } 109 | ] 110 | }, 111 | 112 | { 113 | description: 'objects', 114 | tests: [ 115 | { 116 | description: 'empty objects are equal', 117 | value1: {}, 118 | value2: {}, 119 | equal: true 120 | }, 121 | { 122 | description: 'equal objects (same properties "order")', 123 | value1: {a: 1, b: '2'}, 124 | value2: {a: 1, b: '2'}, 125 | equal: true 126 | }, 127 | { 128 | description: 'equal objects (different properties "order")', 129 | value1: {a: 1, b: '2'}, 130 | value2: {b: '2', a: 1}, 131 | equal: true 132 | }, 133 | { 134 | description: 'not equal objects (extra property)', 135 | value1: {a: 1, b: '2'}, 136 | value2: {a: 1, b: '2', c: []}, 137 | equal: false 138 | }, 139 | { 140 | description: 'not equal objects (different property values)', 141 | value1: {a: 1, b: '2', c: 3}, 142 | value2: {a: 1, b: '2', c: 4}, 143 | equal: false 144 | }, 145 | { 146 | description: 'not equal objects (different properties)', 147 | value1: {a: 1, b: '2', c: 3}, 148 | value2: {a: 1, b: '2', d: 3}, 149 | equal: false 150 | }, 151 | { 152 | description: 'equal objects (same sub-properties)', 153 | value1: { a: [ { b: 'c' } ] }, 154 | value2: { a: [ { b: 'c' } ] }, 155 | equal: true 156 | }, 157 | { 158 | description: 'not equal objects (different sub-property value)', 159 | value1: { a: [ { b: 'c' } ] }, 160 | value2: { a: [ { b: 'd' } ] }, 161 | equal: false 162 | }, 163 | { 164 | description: 'not equal objects (different sub-property)', 165 | value1: { a: [ { b: 'c' } ] }, 166 | value2: { a: [ { c: 'c' } ] }, 167 | equal: false 168 | }, 169 | { 170 | description: 'empty array and empty object are not equal', 171 | value1: {}, 172 | value2: [], 173 | equal: false 174 | }, 175 | { 176 | description: 'object with extra undefined properties are not equal #1', 177 | value1: {}, 178 | value2: {foo: undefined}, 179 | equal: false 180 | }, 181 | { 182 | description: 'object with extra undefined properties are not equal #2', 183 | value1: {foo: undefined}, 184 | value2: {}, 185 | equal: false 186 | }, 187 | { 188 | description: 'object with extra undefined properties are not equal #3', 189 | value1: {foo: undefined}, 190 | value2: {bar: undefined}, 191 | equal: false 192 | }, 193 | { 194 | description: 'nulls are equal', 195 | value1: null, 196 | value2: null, 197 | equal: true 198 | }, 199 | { 200 | description: 'null and undefined are not equal', 201 | value1: null, 202 | value2: undefined, 203 | equal: false 204 | }, 205 | { 206 | description: 'null and empty object are not equal', 207 | value1: null, 208 | value2: {}, 209 | equal: false 210 | }, 211 | { 212 | description: 'undefined and empty object are not equal', 213 | value1: undefined, 214 | value2: {}, 215 | equal: false 216 | }, 217 | { 218 | description: 'objects with different `toString` functions returning same values are equal', 219 | value1: {toString: ()=>'Hello world!'}, 220 | value2: {toString: ()=>'Hello world!'}, 221 | equal: true 222 | }, 223 | { 224 | description: 'objects with `toString` functions returning different values are not equal', 225 | value1: {toString: ()=>'Hello world!'}, 226 | value2: {toString: ()=>'Hi!'}, 227 | equal: false 228 | } 229 | ] 230 | }, 231 | 232 | { 233 | description: 'arrays', 234 | tests: [ 235 | { 236 | description: 'two empty arrays are equal', 237 | value1: [], 238 | value2: [], 239 | equal: true 240 | }, 241 | { 242 | description: 'equal arrays', 243 | value1: [1, 2, 3], 244 | value2: [1, 2, 3], 245 | equal: true 246 | }, 247 | { 248 | description: 'not equal arrays (different item)', 249 | value1: [1, 2, 3], 250 | value2: [1, 2, 4], 251 | equal: false 252 | }, 253 | { 254 | description: 'not equal arrays (different length)', 255 | value1: [1, 2, 3], 256 | value2: [1, 2], 257 | equal: false 258 | }, 259 | { 260 | description: 'equal arrays of objects', 261 | value1: [{a: 'a'}, {b: 'b'}], 262 | value2: [{a: 'a'}, {b: 'b'}], 263 | equal: true 264 | }, 265 | { 266 | description: 'not equal arrays of objects', 267 | value1: [{a: 'a'}, {b: 'b'}], 268 | value2: [{a: 'a'}, {b: 'c'}], 269 | equal: false 270 | }, 271 | { 272 | description: 'pseudo array and equivalent array are not equal', 273 | value1: {'0': 0, '1': 1, length: 2}, 274 | value2: [0, 1], 275 | equal: false 276 | } 277 | ] 278 | }, 279 | { 280 | description: 'Date objects', 281 | tests: [ 282 | { 283 | description: 'equal date objects', 284 | value1: new Date('2017-06-16T21:36:48.362Z'), 285 | value2: new Date('2017-06-16T21:36:48.362Z'), 286 | equal: true 287 | }, 288 | { 289 | description: 'not equal date objects', 290 | value1: new Date('2017-06-16T21:36:48.362Z'), 291 | value2: new Date('2017-01-01T00:00:00.000Z'), 292 | equal: false 293 | }, 294 | { 295 | description: 'date and string are not equal', 296 | value1: new Date('2017-06-16T21:36:48.362Z'), 297 | value2: '2017-06-16T21:36:48.362Z', 298 | equal: false 299 | }, 300 | { 301 | description: 'date and object are not equal', 302 | value1: new Date('2017-06-16T21:36:48.362Z'), 303 | value2: {}, 304 | equal: false 305 | } 306 | ] 307 | }, 308 | { 309 | description: 'RegExp objects', 310 | tests: [ 311 | { 312 | description: 'equal RegExp objects', 313 | value1: /foo/, 314 | value2: /foo/, 315 | equal: true 316 | }, 317 | { 318 | description: 'not equal RegExp objects (different pattern)', 319 | value1: /foo/, 320 | value2: /bar/, 321 | equal: false 322 | }, 323 | { 324 | description: 'not equal RegExp objects (different flags)', 325 | value1: /foo/, 326 | value2: /foo/i, 327 | equal: false 328 | }, 329 | { 330 | description: 'RegExp and string are not equal', 331 | value1: /foo/, 332 | value2: 'foo', 333 | equal: false 334 | }, 335 | { 336 | description: 'RegExp and object are not equal', 337 | value1: /foo/, 338 | value2: {}, 339 | equal: false 340 | } 341 | ] 342 | }, 343 | { 344 | description: 'functions', 345 | tests: [ 346 | { 347 | description: 'same function is equal', 348 | value1: func1, 349 | value2: func1, 350 | equal: true 351 | }, 352 | { 353 | description: 'different functions are not equal', 354 | value1: func1, 355 | value2: func2, 356 | equal: false 357 | } 358 | ] 359 | }, 360 | { 361 | description: 'sample objects', 362 | tests: [ 363 | { 364 | description: 'big object', 365 | value1: { 366 | prop1: 'value1', 367 | prop2: 'value2', 368 | prop3: 'value3', 369 | prop4: { 370 | subProp1: 'sub value1', 371 | subProp2: { 372 | subSubProp1: 'sub sub value1', 373 | subSubProp2: [1, 2, {prop2: 1, prop: 2}, 4, 5] 374 | } 375 | }, 376 | prop5: 1000, 377 | prop6: new Date(2016, 2, 10) 378 | }, 379 | value2: { 380 | prop5: 1000, 381 | prop3: 'value3', 382 | prop1: 'value1', 383 | prop2: 'value2', 384 | prop6: new Date('2016/03/10'), 385 | prop4: { 386 | subProp2: { 387 | subSubProp1: 'sub sub value1', 388 | subSubProp2: [1, 2, {prop2: 1, prop: 2}, 4, 5] 389 | }, 390 | subProp1: 'sub value1' 391 | } 392 | }, 393 | equal: true 394 | } 395 | ] 396 | } 397 | ]; 398 | 399 | function func1() {} 400 | function func2() {} 401 | -------------------------------------------------------------------------------- /src/index.jst: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // do not edit .js files directly - edit src/index.jst 4 | 5 | {{? it.es6 }} 6 | var envHasBigInt64Array = typeof BigInt64Array !== 'undefined'; 7 | {{?}} 8 | 9 | module.exports = function equal(a, b) { 10 | if (a === b) return true; 11 | 12 | if (a && b && typeof a == 'object' && typeof b == 'object') { 13 | if (a.constructor !== b.constructor) return false; 14 | 15 | var length, i, keys; 16 | if (Array.isArray(a)) { 17 | length = a.length; 18 | if (length != b.length) return false; 19 | for (i = length; i-- !== 0;) 20 | if (!equal(a[i], b[i])) return false; 21 | return true; 22 | } 23 | 24 | {{? it.es6 }} 25 | if ((a instanceof Map) && (b instanceof Map)) { 26 | if (a.size !== b.size) return false; 27 | for (i of a.entries()) 28 | if (!b.has(i[0])) return false; 29 | for (i of a.entries()) 30 | if (!equal(i[1], b.get(i[0]))) return false; 31 | return true; 32 | } 33 | 34 | if ((a instanceof Set) && (b instanceof Set)) { 35 | if (a.size !== b.size) return false; 36 | for (i of a.entries()) 37 | if (!b.has(i[0])) return false; 38 | return true; 39 | } 40 | 41 | if (ArrayBuffer.isView(a) && ArrayBuffer.isView(b)) { 42 | length = a.length; 43 | if (length != b.length) return false; 44 | for (i = length; i-- !== 0;) 45 | if (a[i] !== b[i]) return false; 46 | return true; 47 | } 48 | {{?}} 49 | 50 | if (a.constructor === RegExp) return a.source === b.source && a.flags === b.flags; 51 | if (a.valueOf !== Object.prototype.valueOf) return a.valueOf() === b.valueOf(); 52 | if (a.toString !== Object.prototype.toString) return a.toString() === b.toString(); 53 | 54 | keys = Object.keys(a); 55 | length = keys.length; 56 | if (length !== Object.keys(b).length) return false; 57 | 58 | for (i = length; i-- !== 0;) 59 | if (!Object.prototype.hasOwnProperty.call(b, keys[i])) return false; 60 | 61 | for (i = length; i-- !== 0;) { 62 | var key = keys[i]; 63 | {{? it.react }} 64 | if (key === '_owner' && a.$$typeof) { 65 | // React-specific: avoid traversing React elements' _owner. 66 | // _owner contains circular references 67 | // and is not needed when comparing the actual elements (and not their owners) 68 | continue; 69 | } 70 | {{?}} 71 | if (!equal(a[key], b[key])) return false; 72 | } 73 | 74 | return true; 75 | } 76 | 77 | // true if both NaN, false otherwise 78 | return a!==a && b!==b; 79 | }; 80 | --------------------------------------------------------------------------------