├── .flowconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── babel.config.js ├── index.js ├── index.js.flow ├── index.original.js ├── package.json ├── renovate.json ├── test └── shallowequal.test.js ├── type-check.js └── yarn.lock /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | ./index.js.flow 5 | 6 | [libs] 7 | 8 | [lints] 9 | 10 | 11 | [options] 12 | # from: https://medium.com/@gcanti/testing-flow-declaration-files-69915089fa68 13 | suppress_comment=\\(.\\|\n\\)*\\$ExpectError 14 | include_warnings=true 15 | 16 | [strict] 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | modules 4 | *.log 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .babelrc 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - 'node' 5 | - 'lts/*' 6 | - '10' 7 | - '8' 8 | script: npm run travis 9 | install: yarn install 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Alberto Leal (github.com/dashed) 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 | # shallowequal [![Build Status](https://travis-ci.org/dashed/shallowequal.svg)](https://travis-ci.org/dashed/shallowequal) [![Downloads](https://img.shields.io/npm/dm/shallowequal.svg)](https://npmjs.com/shallowequal) [![npm version](https://img.shields.io/npm/v/shallowequal.svg?style=flat)](https://www.npmjs.com/package/shallowequal) 2 | 3 | > `shallowequal` is like lodash's [`isEqual`](https://lodash.com/docs/3.10.1#isEqual) (v3.10.1) but for shallow (strict) equal. 4 | 5 | `shallowequal(value, other, [customizer], [thisArg])` 6 | 7 | Performs a **_shallow equality_** comparison between two values (i.e. `value` and `other`) to determine if they are equivalent. 8 | 9 | The equality check returns true if `value` and `other` are already strictly equal, OR when all the following are true: 10 | 11 | - `value` and `other` are both objects with the same keys 12 | - For each key, the value in `value` and `other` are **strictly equal** (`===`) 13 | 14 | If `customizer` (expected to be a function) is provided it is invoked to compare values. If `customizer` returns `undefined` (i.e. `void 0`), then comparisons are handled by the `shallowequal` function instead. 15 | 16 | The `customizer` is bound to `thisArg` and invoked with three arguments: `(value, other, key)`. 17 | 18 | **NOTE:** Docs are (shamelessly) adapted from [lodash's v3.x docs](https://lodash.com/docs/3.10.1#isEqual) 19 | 20 | ## Install 21 | 22 | ```sh 23 | $ yarn add shallowequal 24 | # npm v5+ 25 | $ npm install shallowequal 26 | # before npm v5 27 | $ npm install --save shallowequal 28 | ``` 29 | 30 | ## Usage 31 | 32 | ```js 33 | const shallowequal = require("shallowequal"); 34 | 35 | const object = { user: "fred" }; 36 | const other = { user: "fred" }; 37 | 38 | object == other; 39 | // → false 40 | 41 | shallowequal(object, other); 42 | // → true 43 | ``` 44 | 45 | ## Credit 46 | 47 | Code for `shallowEqual` originated from https://github.com/gaearon/react-pure-render/ and has since been refactored to have the exact same API as `lodash.isEqualWith` (as of `v4.17.4`). 48 | 49 | ## Development 50 | 51 | - `node.js` and `npm`. See: https://github.com/creationix/nvm#installation 52 | - `yarn`. See: https://yarnpkg.com/en/docs/install 53 | - `npm` dependencies. Run: `yarn install` 54 | 55 | ### Chores 56 | 57 | - Lint: `yarn lint` 58 | - Test: `yarn test` 59 | - Pretty: `yarn pretty` 60 | - Prepare: `yarn prepare` 61 | 62 | ## License 63 | 64 | MIT. 65 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | targets: { 7 | node: "current" 8 | } 9 | } 10 | ] 11 | ] 12 | }; 13 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // 2 | 3 | module.exports = function shallowEqual(objA, objB, compare, compareContext) { 4 | var ret = compare ? compare.call(compareContext, objA, objB) : void 0; 5 | 6 | if (ret !== void 0) { 7 | return !!ret; 8 | } 9 | 10 | if (Object.is(objA, objB)) { 11 | return true; 12 | } 13 | 14 | if (typeof objA !== "object" || !objA || typeof objB !== "object" || !objB) { 15 | return false; 16 | } 17 | 18 | var keysA = Object.keys(objA); 19 | var keysB = Object.keys(objB); 20 | 21 | if (keysA.length !== keysB.length) { 22 | return false; 23 | } 24 | 25 | var bHasOwnProperty = Object.prototype.hasOwnProperty.bind(objB); 26 | 27 | // Test for A's keys different from B. 28 | for (var idx = 0; idx < keysA.length; idx++) { 29 | var key = keysA[idx]; 30 | 31 | if (!bHasOwnProperty(key)) { 32 | return false; 33 | } 34 | 35 | var valueA = objA[key]; 36 | var valueB = objB[key]; 37 | 38 | ret = compare ? compare.call(compareContext, valueA, valueB, key) : void 0; 39 | 40 | if (ret === false || (ret === void 0 && !Object.is(valueA, valueB))) { 41 | return false; 42 | } 43 | } 44 | 45 | return true; 46 | }; 47 | -------------------------------------------------------------------------------- /index.js.flow: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | declare module.exports: ( 4 | objA?: ?T, 5 | objB?: ?U, 6 | compare?: ?(objValue: any, otherValue: any, key?: string) => boolean | void, 7 | compareContext?: ?any 8 | ) => boolean; 9 | -------------------------------------------------------------------------------- /index.original.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | module.exports = function shallowEqual( 4 | objA?: ?T, 5 | objB?: ?U, 6 | compare?: ?(objValue: any, otherValue: any, key?: string) => boolean | void, 7 | compareContext?: ?any 8 | ): boolean { 9 | var ret = compare ? compare.call(compareContext, objA, objB) : void 0; 10 | 11 | if (ret !== void 0) { 12 | return !!ret; 13 | } 14 | 15 | if (objA === objB) { 16 | return true; 17 | } 18 | 19 | if (typeof objA !== "object" || !objA || typeof objB !== "object" || !objB) { 20 | return false; 21 | } 22 | 23 | var keysA = Object.keys(objA); 24 | var keysB = Object.keys(objB); 25 | 26 | if (keysA.length !== keysB.length) { 27 | return false; 28 | } 29 | 30 | var bHasOwnProperty = Object.prototype.hasOwnProperty.bind(objB); 31 | 32 | // Test for A's keys different from B. 33 | for (var idx = 0; idx < keysA.length; idx++) { 34 | var key = keysA[idx]; 35 | 36 | if (!bHasOwnProperty(key)) { 37 | return false; 38 | } 39 | 40 | var valueA = objA[key]; 41 | var valueB = objB[key]; 42 | 43 | ret = compare ? compare.call(compareContext, valueA, valueB, key) : void 0; 44 | 45 | if (ret === false || (ret === void 0 && valueA !== valueB)) { 46 | return false; 47 | } 48 | } 49 | 50 | return true; 51 | }; 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shallowequal", 3 | "version": "1.1.0", 4 | "description": "Like lodash isEqualWith but for shallow equal.", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "eslint index.js test", 8 | "test": "jest .", 9 | "prepare": "npm run flow:check && npm run pretty && npm run lint && npm run test", 10 | "travis": "npm run flow:check && npm run lint && npm run test", 11 | "pretty": "prettier --write --tab-width 2 'test/**/*.js' '*.{js,json,js.flow,md}'", 12 | "precommit": "lint-staged", 13 | "flow:check": "flow check" 14 | }, 15 | "lint-staged": { 16 | "*.{js,json,js.flow,md}": [ 17 | "prettier --write", 18 | "git add" 19 | ] 20 | }, 21 | "author": { 22 | "name": "Alberto Leal", 23 | "email": "mailforalberto@gmail.com", 24 | "url": "github.com/dashed" 25 | }, 26 | "repository": "dashed/shallowequal", 27 | "license": "MIT", 28 | "files": [ 29 | "index.js", 30 | "index.js.flow", 31 | "index.original.js" 32 | ], 33 | "keywords": [ 34 | "shallowequal", 35 | "shallow", 36 | "equal", 37 | "isequal", 38 | "compare", 39 | "isequalwith" 40 | ], 41 | "eslintConfig": { 42 | "parser": "babel-eslint", 43 | "env": { 44 | "browser": true, 45 | "node": true, 46 | "jest": true 47 | }, 48 | "extends": [ 49 | "eslint:recommended" 50 | ], 51 | "plugins": [ 52 | "jest" 53 | ] 54 | }, 55 | "devDependencies": { 56 | "@babel/core": "7.9.0", 57 | "@babel/preset-env": "7.9.0", 58 | "babel-eslint": "10.1.0", 59 | "babel-jest": "25.2.3", 60 | "eslint": "6.8.0", 61 | "eslint-plugin-jest": "23.8.2", 62 | "flow-bin": "0.120.1", 63 | "husky": "3.1.0", 64 | "jest": "25.2.3", 65 | "lint-staged": "10.0.9", 66 | "prettier": "1.19.1" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /test/shallowequal.test.js: -------------------------------------------------------------------------------- 1 | describe("shallowequal", function() { 2 | let shallowequal; 3 | 4 | // eslint-disable-next-line no-sparse-arrays 5 | const falsey = [, "", 0, false, NaN, null, undefined]; 6 | 7 | beforeEach(() => { 8 | // isolated instances of shallowequal for each test. 9 | jest.resetModules(); 10 | shallowequal = jest.requireActual("../index.js"); 11 | }); 12 | 13 | // test cases copied from https://github.com/facebook/fbjs/blob/82247de1c33e6f02a199778203643eaee16ea4dc/src/core/__tests__/shallowEqual-test.js 14 | it("returns false if either argument is null", () => { 15 | expect(shallowequal(null, {})).toEqual(false); 16 | expect(shallowequal({}, null)).toEqual(false); 17 | }); 18 | 19 | it("returns true if both arguments are null or undefined", () => { 20 | expect(shallowequal(null, null)).toEqual(true); 21 | expect(shallowequal(undefined, undefined)).toEqual(true); 22 | }); 23 | 24 | it("returns true if arguments are shallow equal", () => { 25 | expect(shallowequal({ a: 1, b: 2, c: 3 }, { a: 1, b: 2, c: 3 })).toEqual( 26 | true 27 | ); 28 | }); 29 | 30 | it("returns false if arguments are not objects and not equal", () => { 31 | expect(shallowequal(1, 2)).toEqual(false); 32 | }); 33 | 34 | it("returns false if only one argument is not an object", () => { 35 | expect(shallowequal(1, {})).toEqual(false); 36 | }); 37 | 38 | it("returns false if first argument has too many keys", () => { 39 | expect(shallowequal({ a: 1, b: 2, c: 3 }, { a: 1, b: 2 })).toEqual(false); 40 | }); 41 | 42 | it("returns false if second argument has too many keys", () => { 43 | expect(shallowequal({ a: 1, b: 2 }, { a: 1, b: 2, c: 3 })).toEqual(false); 44 | }); 45 | 46 | it("returns true if values are not primitives but are ===", () => { 47 | let obj = {}; 48 | expect( 49 | shallowequal({ a: 1, b: 2, c: obj }, { a: 1, b: 2, c: obj }) 50 | ).toEqual(true); 51 | }); 52 | 53 | // subsequent test cases are copied from lodash tests 54 | it("returns false if arguments are not shallow equal", () => { 55 | expect(shallowequal({ a: 1, b: 2, c: {} }, { a: 1, b: 2, c: {} })).toEqual( 56 | false 57 | ); 58 | }); 59 | 60 | it("should provide the correct `customizer` arguments", () => { 61 | let argsList = []; 62 | const arry = [1, 2]; 63 | const object1 = { a: arry, b: null }; 64 | const object2 = { a: arry, b: null }; 65 | 66 | object1.b = object2; 67 | object2.b = object1; 68 | 69 | const expected = [ 70 | [object1, object2], 71 | [object1.a, object2.a, "a"], 72 | [object1.b, object2.b, "b"] 73 | ]; 74 | 75 | shallowequal(object1, object2, function(...args) { 76 | argsList.push(args); 77 | }); 78 | 79 | expect(argsList).toEqual(expected); 80 | }); 81 | 82 | it("should set the `this` binding", () => { 83 | const actual = shallowequal( 84 | "a", 85 | "b", 86 | function(a, b) { 87 | return this[a] == this[b]; 88 | }, 89 | { a: 1, b: 1 } 90 | ); 91 | 92 | expect(actual).toEqual(true); 93 | }); 94 | 95 | it("should handle comparisons if `customizer` returns `undefined`", () => { 96 | const noop = () => void 0; 97 | 98 | expect(shallowequal("a", "a", noop)).toEqual(true); 99 | expect(shallowequal(["a"], ["a"], noop)).toEqual(true); 100 | expect(shallowequal({ "0": "a" }, { "0": "a" }, noop)).toEqual(true); 101 | }); 102 | 103 | it("should not handle comparisons if `customizer` returns `true`", () => { 104 | const customizer = function(value) { 105 | return typeof value === "string" || undefined; 106 | }; 107 | 108 | expect(shallowequal("a", "b", customizer)).toEqual(true); 109 | expect(shallowequal(["a"], ["b"], customizer)).toEqual(true); 110 | expect(shallowequal({ "0": "a" }, { "0": "b" }, customizer)).toEqual(true); 111 | }); 112 | 113 | it("should not handle comparisons if `customizer` returns `false`", () => { 114 | const customizer = function(value) { 115 | return typeof value === "string" ? false : undefined; 116 | }; 117 | 118 | expect(shallowequal("a", "a", customizer)).toEqual(false); 119 | expect(shallowequal(["a"], ["a"], customizer)).toEqual(false); 120 | expect(shallowequal({ "0": "a" }, { "0": "a" }, customizer)).toEqual(false); 121 | }); 122 | 123 | it("should return a boolean value even if `customizer` does not", () => { 124 | let actual = shallowequal("a", "b", () => "c"); 125 | expect(actual).toEqual(true); 126 | 127 | const values = falsey.filter(v => v !== undefined); 128 | const expected = values.map(() => false); 129 | 130 | actual = []; 131 | values.forEach(value => { 132 | actual.push(shallowequal("a", "a", () => value)); 133 | }); 134 | 135 | expect(actual).toEqual(expected); 136 | }); 137 | 138 | it("should treat objects created by `Object.create(null)` like any other plain object", () => { 139 | function Foo() { 140 | this.a = 1; 141 | } 142 | Foo.prototype.constructor = null; 143 | 144 | const object2 = { a: 1 }; 145 | expect(shallowequal(new Foo(), object2)).toEqual(true); 146 | 147 | const object1 = Object.create(null); 148 | object1.a = 1; 149 | expect(shallowequal(object1, object2)).toEqual(true); 150 | }); 151 | }); 152 | -------------------------------------------------------------------------------- /type-check.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import shallowequal from "./index.original.js"; 4 | 5 | shallowequal("a", "b"); 6 | shallowequal(); 7 | 8 | // $ExpectError 9 | shallowequal("a", "b", false); 10 | 11 | // $ExpectError 12 | shallowequal("a", "b", 1); 13 | 14 | // $ExpectError 15 | shallowequal("a", "b", ""); 16 | 17 | shallowequal("a", "b", null); 18 | 19 | shallowequal("a", "b", void 0); 20 | 21 | // this is legal because function returns void 0 22 | shallowequal("a", "b", () => {}); 23 | 24 | shallowequal( 25 | "a", 26 | "b", 27 | (): boolean => { 28 | return true; 29 | } 30 | ); 31 | 32 | shallowequal( 33 | "a", 34 | "b", 35 | (x, y, z: ?string): boolean => { 36 | return true; 37 | } 38 | ); 39 | 40 | shallowequal( 41 | "a", 42 | "b", 43 | // $ExpectError 44 | (x, y, z: ?number): boolean => { 45 | return true; 46 | } 47 | ); 48 | 49 | shallowequal( 50 | "a", 51 | "b", 52 | // $ExpectError 53 | (): number => { 54 | return 42; 55 | } 56 | ); 57 | --------------------------------------------------------------------------------