├── .editorconfig ├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── .gitignore ├── bench ├── fixtures │ ├── basic.js │ └── complex.js ├── index.js └── package.json ├── index.d.ts ├── license ├── package.json ├── readme.md ├── src ├── alts.js ├── index.js └── lite.js └── test ├── index.js └── lite.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_size = 2 6 | indent_style = tab 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.{json,yml,md}] 13 | indent_style = space 14 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: lukeed 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - 'bench/**' 7 | - '*.md' 8 | branches: 9 | - '**' 10 | tags-ignore: 11 | - '**' 12 | pull_request: 13 | paths-ignore: 14 | - 'bench/**' 15 | - '*.md' 16 | branches: 17 | - master 18 | 19 | jobs: 20 | test: 21 | name: Node.js v${{ matrix.nodejs }} 22 | runs-on: ubuntu-latest 23 | strategy: 24 | matrix: 25 | nodejs: [8, 10, 12, 18] 26 | steps: 27 | - uses: actions/checkout@v3 28 | - uses: actions/setup-node@v3 29 | with: 30 | node-version: ${{ matrix.nodejs }} 31 | 32 | - name: Install 33 | run: npm install 34 | 35 | - name: Test 36 | if: matrix.nodejs < 18 37 | run: npm test 38 | 39 | - name: Test w/ Coverage 40 | if: matrix.nodejs >= 18 41 | run: | 42 | npm install -g c8 43 | c8 --include=src npm test 44 | 45 | - name: Report 46 | if: matrix.nodejs >= 18 47 | run: | 48 | c8 report --reporter=text-lcov > coverage.lcov 49 | bash <(curl -s https://codecov.io/bash) 50 | env: 51 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | *-lock.* 4 | *.lock 5 | *.log 6 | 7 | /dist 8 | /lite 9 | -------------------------------------------------------------------------------- /bench/fixtures/basic.js: -------------------------------------------------------------------------------- 1 | exports.foo = { 2 | prop1: 'value1', 3 | prop2: 'value2', 4 | prop3: 'value3', 5 | prop4: { 6 | subProp1: 'sub value1', 7 | subProp2: { 8 | subSubProp1: 'sub sub value1', 9 | subSubProp2: [1, 2, { prop2:1, prop:2 }, 4, 5] 10 | } 11 | }, 12 | prop5: 1000, 13 | prop6: new Date(2016, 2, 10) 14 | } 15 | 16 | exports.bar = { 17 | prop5: 1000, 18 | prop3: 'value3', 19 | prop1: 'value1', 20 | prop2: 'value2', 21 | prop6: new Date('2016/03/10'), 22 | prop4: { 23 | subProp2: { 24 | subSubProp1: 'sub sub value1', 25 | subSubProp2: [1, 2, {prop2: 1, prop: 2}, 4, 5] 26 | }, 27 | subProp1: 'sub value1' 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /bench/fixtures/complex.js: -------------------------------------------------------------------------------- 1 | exports.foo = { 2 | foo: 'value1', 3 | bar: new Set([1, 2, 3]), 4 | baz: /foo/i, 5 | bat: { 6 | hello: new Map([ ['hello', 'world'] ]), 7 | world: { 8 | aaa: new Map([ 9 | [{ foo: /bar/ }, 'sub sub value1'], 10 | ]), 11 | bbb: [1, 2, { prop2:1, prop:2 }, 4, 5] 12 | } 13 | }, 14 | quz: new Set([{ a:1 , b:2 }]), 15 | qut: new Date(2016, 2, 10), 16 | qar: new Uint8Array([1, 2, 3, 4, 5]), 17 | } 18 | 19 | exports.bar = { 20 | quz: new Set([{ a:1 , b:2 }]), 21 | baz: /foo/i, 22 | foo: 'value1', 23 | bar: new Set([1, 2, 3]), 24 | qar: new Uint8Array([1, 2, 3, 4, 5]), 25 | qut: new Date('2016/03/10'), 26 | bat: { 27 | world: { 28 | aaa: new Map([ 29 | [{ foo: /bar/ }, 'sub sub value1'], 30 | ]), 31 | bbb: [1, 2, { prop2:1, prop:2 }, 4, 5] 32 | }, 33 | hello: new Map([ ['hello', 'world'] ]) 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /bench/index.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | const { Suite } = require('benchmark'); 3 | const klona = require('klona'); 4 | 5 | console.log('Load times:'); 6 | 7 | console.time('assert'); 8 | const { deepStrictEqual } = require('assert'); 9 | console.timeEnd('assert'); 10 | 11 | console.time('util'); 12 | const { isDeepStrictEqual } = require('util'); 13 | console.timeEnd('util'); 14 | 15 | console.time('deep-equal'); 16 | const deepEqual = require('deep-equal'); 17 | console.timeEnd('deep-equal'); 18 | 19 | console.time('fast-deep-equal'); 20 | const fastdeep = require('fast-deep-equal'); 21 | console.timeEnd('fast-deep-equal'); 22 | 23 | console.time('lodash/isEqual'); 24 | const lodash = require('lodash/isEqual'); 25 | console.timeEnd('lodash/isEqual'); 26 | 27 | console.time('nano-equal'); 28 | const nanoequal = require('nano-equal'); 29 | console.timeEnd('nano-equal'); 30 | 31 | console.time('dequal'); 32 | const { dequal } = require('dequal'); 33 | console.timeEnd('dequal'); 34 | 35 | console.time('dequal/lite'); 36 | const lite = require('dequal/lite'); 37 | console.timeEnd('dequal/lite'); 38 | 39 | function naiive(a, b) { 40 | try { 41 | deepStrictEqual(a, b); 42 | return true; 43 | } catch (err) { 44 | return false; 45 | } 46 | } 47 | 48 | // @ts-ignore 49 | const assert = (foo, bar, msg='') => deepStrictEqual(foo, bar, msg); 50 | 51 | function runner(name, contenders) { 52 | const file = join(__dirname, 'fixtures', name + '.js'); 53 | const fixture = require(file); 54 | 55 | console.log('\n(%s) Validation: ', name); 56 | Object.keys(contenders).forEach(name => { 57 | const func = contenders[name]; 58 | const { foo, bar } = klona(fixture); 59 | 60 | try { 61 | assert(func(1, 1), true, 'equal numbers'); 62 | assert(func(1, 2), false, 'not equal numbers'); 63 | assert(func(1, [1]), false, 'number vs array'); 64 | assert(func(0, null), false, 'number vs null'); 65 | assert(func(0, undefined), false, 'number vs undefined'); 66 | 67 | assert(func(foo, bar), true, 'kitchen sink'); 68 | console.log(' ✔', name); 69 | } catch (err) { 70 | console.log(' ✘', name, `(FAILED @ "${err.message}")`); 71 | } 72 | }); 73 | 74 | console.log('\n(%s) Benchmark: ', name); 75 | const bench = new Suite().on('cycle', e => { 76 | console.log(' ' + e.target); 77 | }); 78 | 79 | Object.keys(contenders).forEach(name => { 80 | const { foo, bar } = klona(fixture); 81 | bench.add(name + ' '.repeat(22 - name.length), () => { 82 | // contenders[name]({ a: 1, b: 2, c: 3 }, { a: 1, b: 4, c: 3 }); 83 | contenders[name](foo, bar); 84 | }) 85 | }); 86 | 87 | bench.run(); 88 | } 89 | 90 | runner('basic', { 91 | 'assert.deepStrictEqual': naiive, 92 | 'util.isDeepStrictEqual': isDeepStrictEqual, 93 | 'deep-equal': deepEqual, 94 | 'fast-deep-equal': fastdeep, 95 | 'lodash.isEqual': lodash, 96 | 'nano-equal': nanoequal, 97 | 'dequal/lite': lite.dequal, 98 | 'dequal': dequal, 99 | }); 100 | 101 | // Only keep those that pass 102 | runner('complex', { 103 | 'assert.deepStrictEqual': naiive, 104 | 'util.isDeepStrictEqual': isDeepStrictEqual, 105 | 'deep-equal': deepEqual, 106 | 'lodash.isEqual': lodash, 107 | 'dequal': dequal, 108 | }); 109 | -------------------------------------------------------------------------------- /bench/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "devDependencies": { 4 | "benchmark": "2.1.4", 5 | "deep-equal": "2.2.3", 6 | "dequal": "file:../", 7 | "fast-deep-equal": "3.1.3", 8 | "klona": "1.1.2", 9 | "lodash": "4.17.19", 10 | "nano-equal": "2.0.2" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export function dequal(foo: any, bar: any): boolean; -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Luke Edwards (lukeed.com) 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dequal", 3 | "version": "2.0.3", 4 | "repository": "lukeed/dequal", 5 | "description": "A tiny (304B to 489B) utility for check for deep equality", 6 | "unpkg": "dist/index.min.js", 7 | "module": "dist/index.mjs", 8 | "main": "dist/index.js", 9 | "types": "index.d.ts", 10 | "license": "MIT", 11 | "author": { 12 | "name": "Luke Edwards", 13 | "email": "luke.edwards05@gmail.com", 14 | "url": "https://lukeed.com" 15 | }, 16 | "engines": { 17 | "node": ">=6" 18 | }, 19 | "scripts": { 20 | "build": "bundt", 21 | "pretest": "npm run build", 22 | "postbuild": "echo \"lite\" | xargs -n1 cp -v index.d.ts", 23 | "test": "uvu -r esm test" 24 | }, 25 | "files": [ 26 | "*.d.ts", 27 | "dist", 28 | "lite" 29 | ], 30 | "exports": { 31 | ".": { 32 | "types": "./index.d.ts", 33 | "import": "./dist/index.mjs", 34 | "require": "./dist/index.js" 35 | }, 36 | "./lite": { 37 | "types": "./index.d.ts", 38 | "import": "./lite/index.mjs", 39 | "require": "./lite/index.js" 40 | }, 41 | "./package.json": "./package.json" 42 | }, 43 | "modes": { 44 | "lite": "src/lite.js", 45 | "default": "src/index.js" 46 | }, 47 | "keywords": [ 48 | "deep", 49 | "deep-equal", 50 | "equality" 51 | ], 52 | "devDependencies": { 53 | "bundt": "1.0.2", 54 | "esm": "3.2.25", 55 | "uvu": "0.3.2" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # dequal [![CI](https://github.com/lukeed/dequal/workflows/CI/badge.svg)](https://github.com/lukeed/dequal/actions) [![licenses](https://licenses.dev/b/npm/dequal)](https://licenses.dev/npm/dequal) 2 | 3 | > A tiny (304B to 489B) utility to check for deep equality 4 | 5 | This module supports comparison of all types, including `Function`, `RegExp`, `Date`, `Set`, `Map`, `TypedArray`s, `DataView`, `null`, `undefined`, and `NaN` values. Complex values (eg, Objects, Arrays, Sets, Maps, etc) are traversed recursively. 6 | 7 | > **Important:** 8 | > * key order **within Objects** does not matter 9 | > * value order **within Arrays** _does_ matter 10 | > * values **within Sets and Maps** use value equality 11 | > * keys **within Maps** use value equality 12 | 13 | 14 | ## Install 15 | 16 | ``` 17 | $ npm install --save dequal 18 | ``` 19 | 20 | ## Modes 21 | 22 | There are two "versions" of `dequal` available: 23 | 24 | #### `dequal` 25 | > **Size (gzip):** 489 bytes
26 | > **Availability:** [CommonJS](https://unpkg.com/dequal/dist/index.js), [ES Module](https://unpkg.com/dequal/dist/index.mjs), [UMD](https://unpkg.com/dequal/dist/index.min.js) 27 | 28 | #### `dequal/lite` 29 | > **Size (gzip):** 304 bytes
30 | > **Availability:** [CommonJS](https://unpkg.com/dequal/lite/index.js), [ES Module](https://unpkg.com/dequal/lite/index.mjs) 31 | 32 | | | IE9+ | Number | String | Date | RegExp | Object | Array | Class | Set | Map | ArrayBuffer | [TypedArray](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray#TypedArray_objects) | [DataView](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView) | 33 | |-|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:| 34 | | `dequal` | :x: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | 35 | | `dequal/lite` | :+1: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :x: | :x: | :x: | :x: | :x: | 36 | 37 | > **Note:** Table scrolls horizontally! 38 | 39 | ## Usage 40 | 41 | ```js 42 | import { dequal } from 'dequal'; 43 | 44 | dequal(1, 1); //=> true 45 | dequal({}, {}); //=> true 46 | dequal('foo', 'foo'); //=> true 47 | dequal([1, 2, 3], [1, 2, 3]); //=> true 48 | dequal(dequal, dequal); //=> true 49 | dequal(/foo/, /foo/); //=> true 50 | dequal(null, null); //=> true 51 | dequal(NaN, NaN); //=> true 52 | dequal([], []); //=> true 53 | dequal( 54 | [{ a:1 }, [{ b:{ c:[1] } }]], 55 | [{ a:1 }, [{ b:{ c:[1] } }]] 56 | ); //=> true 57 | 58 | dequal(1, '1'); //=> false 59 | dequal(null, undefined); //=> false 60 | dequal({ a:1, b:[2,3] }, { a:1, b:[2,5] }); //=> false 61 | dequal(/foo/i, /bar/g); //=> false 62 | ``` 63 | 64 | ## API 65 | 66 | ### dequal(foo, bar) 67 | Returns: `Boolean` 68 | 69 | Both `foo` and `bar` can be of any type.
70 | A `Boolean` is returned indicating if the two were deeply equal. 71 | 72 | 73 | ## Benchmarks 74 | 75 | > Running Node v10.13.0 76 | 77 | The benchmarks can be found in the [`/bench`](/bench) directory. They are separated into two categories: 78 | 79 | * `basic` – compares an object comprised of `String`, `Number`, `Date`, `Array`, and `Object` values. 80 | * `complex` – like `basic`, but adds `RegExp`, `Map`, `Set`, and `Uint8Array` values. 81 | 82 | > **Note:** Only candidates that pass validation step(s) are listed.
For example, `fast-deep-equal/es6` handles `Set` and `Map` values, but uses _referential equality_ while those listed use _value equality_. 83 | 84 | ``` 85 | Load times: 86 | assert 0.109ms 87 | util 0.006ms 88 | fast-deep-equal 0.479ms 89 | lodash/isequal 22.826ms 90 | nano-equal 0.417ms 91 | dequal 0.396ms 92 | dequal/lite 0.264ms 93 | 94 | Benchmark :: basic 95 | assert.deepStrictEqual x 325,262 ops/sec ±0.57% (94 runs sampled) 96 | util.isDeepStrictEqual x 318,812 ops/sec ±0.87% (94 runs sampled) 97 | fast-deep-equal x 1,332,393 ops/sec ±0.36% (93 runs sampled) 98 | lodash.isEqual x 269,129 ops/sec ±0.59% (95 runs sampled) 99 | nano-equal x 1,122,053 ops/sec ±0.36% (96 runs sampled) 100 | dequal/lite x 1,700,972 ops/sec ±0.31% (94 runs sampled) 101 | dequal x 1,698,972 ops/sec ±0.63% (97 runs sampled) 102 | 103 | Benchmark :: complex 104 | assert.deepStrictEqual x 124,518 ops/sec ±0.64% (96 runs sampled) 105 | util.isDeepStrictEqual x 125,113 ops/sec ±0.24% (96 runs sampled) 106 | lodash.isEqual x 58,677 ops/sec ±0.49% (96 runs sampled) 107 | dequal x 345,386 ops/sec ±0.27% (96 runs sampled) 108 | ``` 109 | 110 | ## License 111 | 112 | MIT © [Luke Edwards](https://lukeed.com) 113 | -------------------------------------------------------------------------------- /src/alts.js: -------------------------------------------------------------------------------- 1 | // 227B – 128k op/s 2 | export function v227(foo, bar) { 3 | var keys, ctor; 4 | return foo === bar || ( 5 | foo && bar && (ctor=foo.constructor) === bar.constructor ? 6 | ctor === RegExp ? foo.toString() == bar.toString() 7 | : ctor === Date ? foo.getTime() == bar.getTime() 8 | : ctor === Array ? 9 | foo.length === bar.length && foo.every(function (val, idx) { 10 | return v227(val, bar[idx]); 11 | }) 12 | : ctor === Object 13 | && (keys=Object.keys(foo)).length === Object.keys(bar).length 14 | && keys.every(function (k) { 15 | return k in bar && v227(foo[k], bar[k]); 16 | }) 17 | : (foo !== foo && bar !== bar) 18 | ); 19 | } 20 | 21 | // 255B – 155k op/s 22 | export function v255(foo, bar) { 23 | var ctor, len, k; 24 | if (foo === bar) return true; 25 | if (foo && bar && (ctor=foo.constructor) === bar.constructor) { 26 | if (ctor === Date) return foo.getTime() === bar.getTime(); 27 | if (ctor === RegExp) return foo.toString() === bar.toString(); 28 | if (ctor === Array && (len=foo.length) === bar.length) { 29 | while (len-- > 0 && v255(foo[len], bar[len])); 30 | return len === -1; 31 | } 32 | if (ctor === Object) { 33 | if (Object.keys(foo).length !== Object.keys(bar).length) return false; 34 | for (k in foo) { 35 | if (!(k in bar) || !v255(foo[k], bar[k])) return false; 36 | } 37 | return true; 38 | } 39 | } 40 | return foo !== foo && bar !== bar; 41 | } 42 | 43 | // 246B – 157k op/s 44 | export function v246(foo, bar) { 45 | var ctor, i; 46 | if (foo === bar) return true; 47 | if (foo && bar && (ctor=foo.constructor) === bar.constructor) { 48 | if (ctor === Date) return foo.getTime() === bar.getTime(); 49 | if (ctor === RegExp) return foo.toString() === bar.toString(); 50 | if (ctor === Array) { 51 | if (foo.length !== bar.length) return false; 52 | for (i=0; i < foo.length; i++) if (!v246(foo[i], bar[i])) return false; 53 | return true 54 | } 55 | if (ctor === Object) { 56 | if (Object.keys(foo).length !== Object.keys(bar).length) return false; 57 | for (i in foo) if (!(i in bar) || !v246(foo[i], bar[i])) return false; 58 | return true; 59 | } 60 | } 61 | return foo !== foo && bar !== bar; 62 | } 63 | 64 | // 225B - 97k op/s 65 | export function v225(foo, bar) { 66 | var ctor, keys, len; 67 | if (foo === bar) return true; 68 | if (foo && bar && (ctor=foo.constructor) === bar.constructor) { 69 | if (ctor === Date) return foo.getTime() === bar.getTime(); 70 | if (ctor === RegExp) return foo.toString() === bar.toString(); 71 | if (typeof foo === 'object') { 72 | if (Object.keys(foo).length !== Object.keys(bar).length) return false; 73 | for (len in foo) if (!(len in bar) || !v225(foo[len], bar[len])) return false; 74 | return true; 75 | } 76 | } 77 | return foo !== foo && bar !== bar; 78 | } 79 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | var has = Object.prototype.hasOwnProperty; 2 | 3 | function find(iter, tar, key) { 4 | for (key of iter.keys()) { 5 | if (dequal(key, tar)) return key; 6 | } 7 | } 8 | 9 | export function dequal(foo, bar) { 10 | var ctor, len, tmp; 11 | if (foo === bar) return true; 12 | 13 | if (foo && bar && (ctor=foo.constructor) === bar.constructor) { 14 | if (ctor === Date) return foo.getTime() === bar.getTime(); 15 | if (ctor === RegExp) return foo.toString() === bar.toString(); 16 | 17 | if (ctor === Array) { 18 | if ((len=foo.length) === bar.length) { 19 | while (len-- && dequal(foo[len], bar[len])); 20 | } 21 | return len === -1; 22 | } 23 | 24 | if (ctor === Set) { 25 | if (foo.size !== bar.size) { 26 | return false; 27 | } 28 | for (len of foo) { 29 | tmp = len; 30 | if (tmp && typeof tmp === 'object') { 31 | tmp = find(bar, tmp); 32 | if (!tmp) return false; 33 | } 34 | if (!bar.has(tmp)) return false; 35 | } 36 | return true; 37 | } 38 | 39 | if (ctor === Map) { 40 | if (foo.size !== bar.size) { 41 | return false; 42 | } 43 | for (len of foo) { 44 | tmp = len[0]; 45 | if (tmp && typeof tmp === 'object') { 46 | tmp = find(bar, tmp); 47 | if (!tmp) return false; 48 | } 49 | if (!dequal(len[1], bar.get(tmp))) { 50 | return false; 51 | } 52 | } 53 | return true; 54 | } 55 | 56 | if (ctor === ArrayBuffer) { 57 | foo = new Uint8Array(foo); 58 | bar = new Uint8Array(bar); 59 | } else if (ctor === DataView) { 60 | if ((len=foo.byteLength) === bar.byteLength) { 61 | while (len-- && foo.getInt8(len) === bar.getInt8(len)); 62 | } 63 | return len === -1; 64 | } 65 | 66 | if (ArrayBuffer.isView(foo)) { 67 | if ((len=foo.byteLength) === bar.byteLength) { 68 | while (len-- && foo[len] === bar[len]); 69 | } 70 | return len === -1; 71 | } 72 | 73 | if (!ctor || typeof foo === 'object') { 74 | len = 0; 75 | for (ctor in foo) { 76 | if (has.call(foo, ctor) && ++len && !has.call(bar, ctor)) return false; 77 | if (!(ctor in bar) || !dequal(foo[ctor], bar[ctor])) return false; 78 | } 79 | return Object.keys(bar).length === len; 80 | } 81 | } 82 | 83 | return foo !== foo && bar !== bar; 84 | } 85 | -------------------------------------------------------------------------------- /src/lite.js: -------------------------------------------------------------------------------- 1 | var has = Object.prototype.hasOwnProperty; 2 | 3 | export function dequal(foo, bar) { 4 | var ctor, len; 5 | if (foo === bar) return true; 6 | 7 | if (foo && bar && (ctor=foo.constructor) === bar.constructor) { 8 | if (ctor === Date) return foo.getTime() === bar.getTime(); 9 | if (ctor === RegExp) return foo.toString() === bar.toString(); 10 | 11 | if (ctor === Array) { 12 | if ((len=foo.length) === bar.length) { 13 | while (len-- && dequal(foo[len], bar[len])); 14 | } 15 | return len === -1; 16 | } 17 | 18 | if (!ctor || typeof foo === 'object') { 19 | len = 0; 20 | for (ctor in foo) { 21 | if (has.call(foo, ctor) && ++len && !has.call(bar, ctor)) return false; 22 | if (!(ctor in bar) || !dequal(foo[ctor], bar[ctor])) return false; 23 | } 24 | return Object.keys(bar).length === len; 25 | } 26 | } 27 | 28 | return foo !== foo && bar !== bar; 29 | } 30 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import { suite } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | import { dequal } from '../src'; 4 | 5 | function same(a, b) { 6 | assert.is(dequal(a, b), true); 7 | } 8 | 9 | function different(a, b) { 10 | assert.is(dequal(a, b), false); 11 | } 12 | 13 | const API = suite('exports'); 14 | 15 | API('exports', () => { 16 | assert.type(dequal, 'function'); 17 | }); 18 | 19 | API.run(); 20 | 21 | // --- 22 | 23 | const scalars = suite('scalars'); 24 | 25 | scalars('scalars', () => { 26 | same(1, 1); 27 | different(1, 2); 28 | different(1, []); 29 | different(1, '1'); 30 | same(Infinity, Infinity); 31 | different(Infinity, -Infinity); 32 | different(NaN, undefined); 33 | different(NaN, null); 34 | same(NaN, NaN); 35 | different(1, -1); 36 | same(0, -0); 37 | 38 | same(null, null); 39 | same(void 0, undefined); 40 | same(undefined, undefined); 41 | different(null, undefined); 42 | different('', null); 43 | different(0, null); 44 | 45 | same(true, true); 46 | same(false, false); 47 | different(true, false); 48 | different(0, false); 49 | different(1, true); 50 | 51 | same('a', 'a'); 52 | different('a', 'b'); 53 | }); 54 | 55 | scalars.run(); 56 | 57 | // --- 58 | 59 | const Objects = suite('Object'); 60 | 61 | Objects('Objects', () => { 62 | same({}, {}); 63 | same({ a:1, b:2 }, { a:1, b:2 }); 64 | same({ b:2, a:1 }, { a:1, b:2 }); 65 | 66 | different({ a:1, b:2, c:[] }, { a:1, b:2 }); 67 | different({ a:1, b:2 }, { a:1, b:2, c:[] }); 68 | different({ a:1, c:3 }, { a:1, b:2 }); 69 | 70 | same({ a:[{ b:1 }] }, { a:[{ b:1 }] }); 71 | different({ a:[{ b:2 }] }, { a:[{ b:1 }] }); 72 | different({ a:[{ c:1 }] }, { a:[{ b:1 }] }); 73 | 74 | different([], {}); 75 | different({}, []); 76 | different({}, null); 77 | different({}, undefined); 78 | 79 | different({ a:void 0 }, {}); 80 | different({}, { a:undefined }); 81 | different({ a:undefined }, { b:undefined }); 82 | }); 83 | 84 | Objects('dictionary', () => { 85 | const foo = Object.create(null); 86 | const bar = Object.create(null); 87 | same(foo, bar); 88 | 89 | foo.hello = 'world'; 90 | different(foo, bar); 91 | }); 92 | 93 | Objects.run(); 94 | 95 | // --- 96 | 97 | const Arrays = suite('Array'); 98 | 99 | Arrays('Arrays', () => { 100 | same([], []); 101 | same([1,2,3], [1,2,3]); 102 | different([1,2,4], [1,2,3]); 103 | different([1,2], [1,2,3]); 104 | 105 | same([{ a:1 }, { b:2 }], [{ a:1 }, { b:2 }]); 106 | different([{ a:2 }, { b:2 }], [{ a:1 }, { b:2 }]); 107 | 108 | different({ '0':0, '1':1, length:2 }, [0, 1]); 109 | }); 110 | 111 | Arrays.run(); 112 | 113 | // --- 114 | 115 | const Dates = suite('Date'); 116 | 117 | Dates('Dates', () => { 118 | same( 119 | new Date('2015-05-01T22:16:18.234Z'), 120 | new Date('2015-05-01T22:16:18.234Z') 121 | ); 122 | 123 | different( 124 | new Date('2015-05-01T22:16:18.234Z'), 125 | new Date('2017-01-01T00:00:00.000Z') 126 | ); 127 | 128 | different( 129 | new Date('2015-05-01T22:16:18.234Z'), 130 | '2015-05-01T22:16:18.234Z' 131 | ); 132 | 133 | different( 134 | new Date('2015-05-01T22:16:18.234Z'), 135 | 1430518578234 136 | ); 137 | 138 | different( 139 | new Date('2015-05-01T22:16:18.234Z'), 140 | {} 141 | ); 142 | }); 143 | 144 | Dates.run(); 145 | 146 | // --- 147 | 148 | const RegExps = suite('RegExp'); 149 | 150 | RegExps('RegExps', () => { 151 | same(/foo/, /foo/); 152 | same(/foo/i, /foo/i); 153 | 154 | different(/foo/, /bar/); 155 | different(/foo/, /foo/i); 156 | 157 | different(/foo/, 'foo'); 158 | different(/foo/, {}); 159 | }); 160 | 161 | RegExps.run(); 162 | 163 | // --- 164 | 165 | const Functions = suite('Function'); 166 | 167 | Functions('Functions', () => { 168 | let foo = () => {}; 169 | let bar = () => {}; 170 | 171 | same(foo, foo); 172 | different(foo, bar); 173 | different(foo, () => {}); 174 | }); 175 | 176 | Functions.run(); 177 | 178 | // --- 179 | 180 | const Classes = suite('class'); 181 | 182 | Classes('class', () => { 183 | class Foobar {} 184 | same(new Foobar, new Foobar); 185 | }); 186 | 187 | // @see https://github.com/lukeed/klona/issues/14 188 | Classes('prototype', () => { 189 | function Test () {} 190 | Test.prototype.val = 42; 191 | 192 | same(new Test, new Test); 193 | }); 194 | 195 | Classes('constructor properties', () => { 196 | function Test (num) { 197 | this.value = num; 198 | } 199 | 200 | Test.prototype.val = 42; 201 | 202 | same(new Test(123), new Test(123)); 203 | different(new Test(0), new Test(123)); 204 | }); 205 | 206 | Classes('constructor properties :: class', () => { 207 | class Test { 208 | constructor(num) { 209 | this.value = num; 210 | } 211 | } 212 | 213 | same(new Test, new Test); 214 | same(new Test(123), new Test(123)); 215 | different(new Test, new Test(123)); 216 | }); 217 | 218 | Classes('constructor properties :: defaults', () => { 219 | class Test { 220 | constructor(num = 123) { 221 | this.value = num; 222 | } 223 | } 224 | 225 | same(new Test(456), new Test(456)); 226 | same(new Test(123), new Test); 227 | }); 228 | 229 | Classes('accessors', () => { 230 | class Test { 231 | get val() { 232 | return 42; 233 | } 234 | } 235 | 236 | same(new Test, new Test); 237 | }); 238 | 239 | Classes('values but not prototype', () => { 240 | class Item { 241 | constructor() { 242 | this.foo = 1; 243 | this.bar = 2; 244 | } 245 | } 246 | 247 | const hello = new Item; 248 | const world = { 249 | foo: 1, 250 | bar: 2, 251 | }; 252 | 253 | assert.is( 254 | JSON.stringify(hello), 255 | JSON.stringify(world) 256 | ); 257 | 258 | different(hello, world); 259 | 260 | hello.foo = world.foo; 261 | hello.bar = world.bar; 262 | 263 | different(hello, world); 264 | }); 265 | 266 | Classes.run(); 267 | 268 | // --- 269 | 270 | const Maps = suite('Map'); 271 | 272 | Maps('flat', () => { 273 | const hello = new Map(); 274 | const world = new Map(); 275 | 276 | same(hello, world); 277 | 278 | world.set('hello', 'world'); 279 | different(hello, world); 280 | 281 | hello.set('foo', 'bar'); 282 | different(hello, world); 283 | 284 | world.set('foo', 'bar'); 285 | hello.set('hello', 'world'); 286 | same(hello, world); 287 | }); 288 | 289 | Maps('nested', () => { 290 | const hello = new Map([ 291 | ['foo', { a: 1 }], 292 | ['bar', [1, 2, 3]], 293 | ]); 294 | 295 | const world = new Map([ 296 | ['foo', 'bar'] 297 | ]); 298 | 299 | different(hello, world); 300 | 301 | // @ts-ignore 302 | world.set('foo', { a: 1 }); 303 | different(hello, world); 304 | 305 | // @ts-ignore 306 | world.set('bar', [1, 2, 3]); 307 | same(hello, world); 308 | 309 | // @ts-ignore 310 | hello.set('baz', new Map([['hello', 'world']])); 311 | different(hello, world); 312 | 313 | // @ts-ignore 314 | world.set('baz', new Map([['hello', 'world']])); 315 | same(hello, world); 316 | }); 317 | 318 | Maps('keys :: complex', () => { 319 | const hello = new Map([ 320 | [{ foo:1 }, { a:1 }] 321 | ]); 322 | 323 | const world = new Map([ 324 | [{ foo:1 }, { a:1 }] 325 | ]); 326 | 327 | same(hello, world); 328 | 329 | // @ts-ignore 330 | [...world.keys()][0].bar = 2; 331 | 332 | assert.equal([...hello.keys()][0], { foo:1 }); 333 | assert.equal([...world.keys()][0], { foo:1, bar:2 }); 334 | 335 | different(hello, world); 336 | }); 337 | 338 | Maps('keys :: value-based', () => { 339 | different( 340 | new Map([ 341 | [{ a: 1 }, undefined] 342 | ]), 343 | new Map([ 344 | [{ a: 1 }, {}] 345 | ]) 346 | ); 347 | 348 | same( 349 | new Map([ 350 | [{ a: 1 }, 1] 351 | ]), 352 | new Map([ 353 | [{ a: 1 }, 1] 354 | ]) 355 | ); 356 | }); 357 | 358 | Maps.run(); 359 | 360 | // --- 361 | 362 | const Sets = suite('Set'); 363 | 364 | Sets('flat', () => { 365 | const hello = new Set(); 366 | const world = new Set(); 367 | 368 | same(hello, world); 369 | 370 | world.add('hello'); 371 | different(hello, world); 372 | 373 | hello.add('foo'); 374 | different(hello, world); 375 | 376 | world.add('foo'); 377 | hello.add('hello'); 378 | same(hello, world); 379 | }); 380 | 381 | Sets('flat :: order', () => { 382 | const hello = new Set(['foo', 'bar']); 383 | const world = new Set(['bar', 'foo']); 384 | same(hello, world); 385 | }); 386 | 387 | Sets('complex', () => { 388 | const hello = new Set([ 389 | 'foo', 'bar', { a: 1 }, [1, 2, 3] 390 | ]); 391 | 392 | const world = new Set([ 393 | 'foo', { a: 1 }, 'bar' 394 | ]); 395 | 396 | different(hello, world); 397 | 398 | // @ts-ignore 399 | world.add([1, 2, 3]); 400 | same(hello, world); 401 | 402 | world.delete('foo'); 403 | different(hello, world); 404 | 405 | world.add('foo'); 406 | same(hello, world); 407 | }); 408 | 409 | Sets.run(); 410 | 411 | // --- 412 | 413 | const TypedArrays = suite('TypedArray'); 414 | 415 | TypedArrays('Buffer', () => { 416 | same( 417 | Buffer.from('hello'), 418 | new Buffer('hello'), 419 | ); 420 | 421 | different( 422 | Buffer.from('hello'), 423 | Buffer.from('world'), 424 | ); 425 | 426 | different( 427 | Buffer.from('hello', 'base64'), 428 | Buffer.from('hello', 'utf8'), 429 | ); 430 | }); 431 | 432 | TypedArrays('Int16Array', () => { 433 | same( 434 | new Int16Array([42]), 435 | new Int16Array([42]), 436 | ); 437 | 438 | different( 439 | new Int16Array([1, 2, 3]), 440 | new Int16Array([1, 2]), 441 | ); 442 | 443 | different( 444 | new Int16Array([1, 2, 3]), 445 | new Int16Array([4, 5, 6]), 446 | ); 447 | 448 | different( 449 | new Int16Array([1, 2, 3]), 450 | new Uint16Array([1, 2, 3]), 451 | ); 452 | 453 | different( 454 | new Int16Array([1, 2, 3]), 455 | new Int8Array([1, 2, 3]), 456 | ); 457 | }); 458 | 459 | TypedArrays('Int32Array', () => { 460 | same( 461 | new Int32Array(new ArrayBuffer(4)), 462 | new Int32Array(new ArrayBuffer(4)), 463 | ); 464 | 465 | different( 466 | new Int32Array(8), 467 | new Uint32Array(8), 468 | ); 469 | 470 | different( 471 | new Int32Array(new ArrayBuffer(8)), 472 | new Int32Array(Array.from({ length: 8 })), 473 | ); 474 | }); 475 | 476 | TypedArrays('ArrayBuffer', () => { 477 | same( 478 | new ArrayBuffer(2), 479 | new ArrayBuffer(2), 480 | ); 481 | 482 | different( 483 | new ArrayBuffer(1), 484 | new ArrayBuffer(2), 485 | ); 486 | }); 487 | 488 | TypedArrays('DataView', () => { 489 | same( 490 | new DataView(new ArrayBuffer(4)), 491 | new DataView(new ArrayBuffer(4)), 492 | ); 493 | 494 | const hello = new Int8Array([1, 2, 3, 4, 5]); 495 | const world = new Int8Array([1, 2, 3, 4, 5]); 496 | 497 | same(hello, world); 498 | same(hello.buffer, world.buffer); 499 | 500 | same( 501 | new DataView(hello.buffer), 502 | new DataView(world.buffer) 503 | ); 504 | 505 | hello.fill(0); 506 | 507 | different(hello, world); 508 | different(hello.buffer, world.buffer); 509 | 510 | different( 511 | new DataView(hello.buffer), 512 | new DataView(world.buffer) 513 | ); 514 | }); 515 | 516 | TypedArrays.run(); 517 | 518 | // --- 519 | 520 | const kitchen = suite('kitchen'); 521 | 522 | kitchen('kitchen sink', () => { 523 | same({ 524 | prop1: 'value1', 525 | prop2: 'value2', 526 | prop3: 'value3', 527 | prop4: { 528 | subProp1: 'sub value1', 529 | subProp2: { 530 | subSubProp1: 'sub sub value1', 531 | subSubProp2: [1, 2, {prop2: 1, prop: 2}, 4, 5] 532 | } 533 | }, 534 | prop5: 1000, 535 | prop6: new Date(2016, 2, 10) 536 | }, { 537 | prop5: 1000, 538 | prop3: 'value3', 539 | prop1: 'value1', 540 | prop2: 'value2', 541 | prop6: new Date('2016/03/10'), 542 | prop4: { 543 | subProp2: { 544 | subSubProp1: 'sub sub value1', 545 | subSubProp2: [1, 2, {prop2: 1, prop: 2}, 4, 5] 546 | }, 547 | subProp1: 'sub value1' 548 | } 549 | }); 550 | }); 551 | 552 | kitchen.run(); 553 | -------------------------------------------------------------------------------- /test/lite.js: -------------------------------------------------------------------------------- 1 | import { suite } from 'uvu'; 2 | import * as assert from 'uvu/assert'; 3 | import { dequal } from '../src/lite'; 4 | 5 | function same(a, b) { 6 | assert.is(dequal(a, b), true); 7 | } 8 | 9 | function different(a, b) { 10 | assert.is(dequal(a, b), false); 11 | } 12 | 13 | const API = suite('exports'); 14 | 15 | API('exports', () => { 16 | assert.type(dequal, 'function'); 17 | }); 18 | 19 | API.run(); 20 | 21 | // --- 22 | 23 | const scalars = suite('scalars'); 24 | 25 | scalars('scalars', () => { 26 | same(1, 1); 27 | different(1, 2); 28 | different(1, []); 29 | different(1, '1'); 30 | same(Infinity, Infinity); 31 | different(Infinity, -Infinity); 32 | different(NaN, undefined); 33 | different(NaN, null); 34 | same(NaN, NaN); 35 | different(1, -1); 36 | same(0, -0); 37 | 38 | same(null, null); 39 | same(void 0, undefined); 40 | same(undefined, undefined); 41 | different(null, undefined); 42 | different('', null); 43 | different(0, null); 44 | 45 | same(true, true); 46 | same(false, false); 47 | different(true, false); 48 | different(0, false); 49 | different(1, true); 50 | 51 | same('a', 'a'); 52 | different('a', 'b'); 53 | }); 54 | 55 | scalars.run(); 56 | 57 | // --- 58 | 59 | const Objects = suite('Object'); 60 | 61 | Objects('Objects', () => { 62 | same({}, {}); 63 | same({ a:1, b:2 }, { a:1, b:2 }); 64 | same({ b:2, a:1 }, { a:1, b:2 }); 65 | 66 | different({ a:1, b:2, c:[] }, { a:1, b:2 }); 67 | different({ a:1, b:2 }, { a:1, b:2, c:[] }); 68 | different({ a:1, c:3 }, { a:1, b:2 }); 69 | 70 | same({ a:[{ b:1 }] }, { a:[{ b:1 }] }); 71 | different({ a:[{ b:2 }] }, { a:[{ b:1 }] }); 72 | different({ a:[{ c:1 }] }, { a:[{ b:1 }] }); 73 | 74 | different([], {}); 75 | different({}, []); 76 | different({}, null); 77 | different({}, undefined); 78 | 79 | different({ a:void 0 }, {}); 80 | different({}, { a:undefined }); 81 | different({ a:undefined }, { b:undefined }); 82 | }); 83 | 84 | Objects('dictionary', () => { 85 | const foo = Object.create(null); 86 | const bar = Object.create(null); 87 | same(foo, bar); 88 | 89 | foo.hello = 'world'; 90 | different(foo, bar); 91 | }); 92 | 93 | Objects.run(); 94 | 95 | // --- 96 | 97 | const Arrays = suite('Array'); 98 | 99 | Arrays('Arrays', () => { 100 | same([], []); 101 | same([1,2,3], [1,2,3]); 102 | different([1,2,4], [1,2,3]); 103 | different([1,2], [1,2,3]); 104 | 105 | same([{ a:1 }, { b:2 }], [{ a:1 }, { b:2 }]); 106 | different([{ a:2 }, { b:2 }], [{ a:1 }, { b:2 }]); 107 | 108 | different({ '0':0, '1':1, length:2 }, [0, 1]); 109 | }); 110 | 111 | Arrays.run(); 112 | 113 | // --- 114 | 115 | const Dates = suite('Date'); 116 | 117 | Dates('Dates', () => { 118 | same( 119 | new Date('2015-05-01T22:16:18.234Z'), 120 | new Date('2015-05-01T22:16:18.234Z') 121 | ); 122 | 123 | different( 124 | new Date('2015-05-01T22:16:18.234Z'), 125 | new Date('2017-01-01T00:00:00.000Z') 126 | ); 127 | 128 | different( 129 | new Date('2015-05-01T22:16:18.234Z'), 130 | '2015-05-01T22:16:18.234Z' 131 | ); 132 | 133 | different( 134 | new Date('2015-05-01T22:16:18.234Z'), 135 | 1430518578234 136 | ); 137 | 138 | different( 139 | new Date('2015-05-01T22:16:18.234Z'), 140 | {} 141 | ); 142 | }); 143 | 144 | Dates.run(); 145 | 146 | // --- 147 | 148 | const RegExps = suite('RegExp'); 149 | 150 | RegExps('RegExps', () => { 151 | same(/foo/, /foo/); 152 | same(/foo/i, /foo/i); 153 | 154 | different(/foo/, /bar/); 155 | different(/foo/, /foo/i); 156 | 157 | different(/foo/, 'foo'); 158 | different(/foo/, {}); 159 | }); 160 | 161 | RegExps.run(); 162 | 163 | // --- 164 | 165 | const Functions = suite('Function'); 166 | 167 | Functions('Functions', () => { 168 | let foo = () => {}; 169 | let bar = () => {}; 170 | 171 | same(foo, foo); 172 | different(foo, bar); 173 | different(foo, () => {}); 174 | }); 175 | 176 | Functions.run(); 177 | 178 | // --- 179 | 180 | const Classes = suite('class'); 181 | 182 | Classes('class', () => { 183 | class Foobar {} 184 | same(new Foobar, new Foobar); 185 | }); 186 | 187 | // @see https://github.com/lukeed/klona/issues/14 188 | Classes('prototype', () => { 189 | function Test () {} 190 | Test.prototype.val = 42; 191 | 192 | same(new Test, new Test); 193 | }); 194 | 195 | Classes('constructor properties', () => { 196 | function Test (num) { 197 | this.value = num; 198 | } 199 | 200 | Test.prototype.val = 42; 201 | 202 | same(new Test(123), new Test(123)); 203 | different(new Test(0), new Test(123)); 204 | }); 205 | 206 | Classes('constructor properties :: class', () => { 207 | class Test { 208 | constructor(num) { 209 | this.value = num; 210 | } 211 | } 212 | 213 | same(new Test, new Test); 214 | same(new Test(123), new Test(123)); 215 | different(new Test, new Test(123)); 216 | }); 217 | 218 | Classes('constructor properties :: defaults', () => { 219 | class Test { 220 | constructor(num = 123) { 221 | this.value = num; 222 | } 223 | } 224 | 225 | same(new Test(456), new Test(456)); 226 | same(new Test(123), new Test); 227 | }); 228 | 229 | Classes('accessors', () => { 230 | class Test { 231 | get val() { 232 | return 42; 233 | } 234 | } 235 | 236 | same(new Test, new Test); 237 | }); 238 | 239 | Classes('values but not prototype', () => { 240 | class Item { 241 | constructor() { 242 | this.foo = 1; 243 | this.bar = 2; 244 | } 245 | } 246 | 247 | const hello = new Item; 248 | const world = { 249 | foo: 1, 250 | bar: 2, 251 | }; 252 | 253 | assert.is( 254 | JSON.stringify(hello), 255 | JSON.stringify(world) 256 | ); 257 | 258 | different(hello, world); 259 | 260 | hello.foo = world.foo; 261 | hello.bar = world.bar; 262 | 263 | different(hello, world); 264 | }); 265 | 266 | Classes.run(); 267 | 268 | // --- 269 | 270 | const kitchen = suite('kitchen'); 271 | 272 | kitchen('kitchen sink', () => { 273 | same({ 274 | prop1: 'value1', 275 | prop2: 'value2', 276 | prop3: 'value3', 277 | prop4: { 278 | subProp1: 'sub value1', 279 | subProp2: { 280 | subSubProp1: 'sub sub value1', 281 | subSubProp2: [1, 2, {prop2: 1, prop: 2}, 4, 5] 282 | } 283 | }, 284 | prop5: 1000, 285 | prop6: new Date(2016, 2, 10) 286 | }, { 287 | prop5: 1000, 288 | prop3: 'value3', 289 | prop1: 'value1', 290 | prop2: 'value2', 291 | prop6: new Date('2016/03/10'), 292 | prop4: { 293 | subProp2: { 294 | subSubProp1: 'sub sub value1', 295 | subSubProp2: [1, 2, {prop2: 1, prop: 2}, 4, 5] 296 | }, 297 | subProp1: 'sub value1' 298 | } 299 | }); 300 | }); 301 | 302 | kitchen.run(); 303 | --------------------------------------------------------------------------------