-
Notifications
You must be signed in to change notification settings - Fork 29
/
deep-equal.js
208 lines (185 loc) · 6.56 KB
/
deep-equal.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
"use strict";
var getClass = require("./get-class");
var identical = require("./identical");
var isArguments = require("./is-arguments");
var isDate = require("./is-date");
var isElement = require("./is-element");
var isNaN = require("./is-nan");
var isObject = require("./is-object");
var isSet = require("./is-set");
var isSubset = require("./is-subset");
var getClassName = require("./get-class-name");
var every = Array.prototype.every;
var getTime = Date.prototype.getTime;
var hasOwnProperty = Object.prototype.hasOwnProperty;
var indexOf = Array.prototype.indexOf;
var keys = Object.keys;
/**
* @name samsam.deepEqual
* @param Object first
* @param Object second
*
* Deep equal comparison. Two values are "deep equal" if:
*
* - They are equal, according to samsam.identical
* - They are both date objects representing the same time
* - They are both arrays containing elements that are all deepEqual
* - They are objects with the same set of properties, and each property
* in ``first`` is deepEqual to the corresponding property in ``second``
*
* Supports cyclic objects.
*/
function deepEqualCyclic(first, second, match) {
// used for cyclic comparison
// contain already visited objects
var objects1 = [];
var objects2 = [];
// contain pathes (position in the object structure)
// of the already visited objects
// indexes same as in objects arrays
var paths1 = [];
var paths2 = [];
// contains combinations of already compared objects
// in the manner: { "$1['ref']$2['ref']": true }
var compared = {};
// does the recursion for the deep equal check
return (function deepEqual(obj1, obj2, path1, path2) {
// If both are matchers they must be the same instance in order to be
// considered equal If we didn't do that we would end up running one
// matcher against the other
if (match) {
if (match.isMatcher(obj1)) {
if (match.isMatcher(obj2)) {
return obj1 === obj2;
}
return obj1.test(obj2);
}
if (match.isMatcher(obj2)) {
return obj2.test(obj1);
}
}
var type1 = typeof obj1;
var type2 = typeof obj2;
// == null also matches undefined
if (
obj1 === obj2 ||
isNaN(obj1) ||
isNaN(obj2) ||
obj1 == null ||
obj2 == null ||
type1 !== "object" ||
type2 !== "object"
) {
return identical(obj1, obj2);
}
// Elements are only equal if identical(expected, actual)
if (isElement(obj1) || isElement(obj2)) {
return false;
}
var isDate1 = isDate(obj1);
var isDate2 = isDate(obj2);
if (isDate1 || isDate2) {
if (
!isDate1 ||
!isDate2 ||
getTime.call(obj1) !== getTime.call(obj2)
) {
return false;
}
}
if (obj1 instanceof RegExp && obj2 instanceof RegExp) {
if (obj1.toString() !== obj2.toString()) {
return false;
}
}
if (obj1 instanceof Error && obj2 instanceof Error) {
if (
obj1.constructor !== obj2.constructor ||
obj1.message !== obj2.message ||
obj1.stack !== obj2.stack
) {
return false;
}
}
var class1 = getClass(obj1);
var class2 = getClass(obj2);
var keys1 = keys(obj1);
var keys2 = keys(obj2);
var name1 = getClassName(obj1);
var name2 = getClassName(obj2);
if (isArguments(obj1) || isArguments(obj2)) {
if (obj1.length !== obj2.length) {
return false;
}
} else {
if (
type1 !== type2 ||
class1 !== class2 ||
keys1.length !== keys2.length ||
(name1 && name2 && name1 !== name2)
) {
return false;
}
}
if (isSet(obj1) || isSet(obj2)) {
if (!isSet(obj1) || !isSet(obj2) || obj1.size !== obj2.size) {
return false;
}
return isSubset(obj1, obj2, deepEqual);
}
return every.call(keys1, function(key) {
if (!hasOwnProperty.call(obj2, key)) {
return false;
}
var value1 = obj1[key];
var value2 = obj2[key];
var isObject1 = isObject(value1);
var isObject2 = isObject(value2);
// determines, if the objects were already visited
// (it's faster to check for isObject first, than to
// get -1 from getIndex for non objects)
var index1 = isObject1 ? indexOf.call(objects1, value1) : -1;
var index2 = isObject2 ? indexOf.call(objects2, value2) : -1;
// determines the new paths of the objects
// - for non cyclic objects the current path will be extended
// by current property name
// - for cyclic objects the stored path is taken
var newPath1 =
index1 !== -1
? paths1[index1]
: path1 + "[" + JSON.stringify(key) + "]";
var newPath2 =
index2 !== -1
? paths2[index2]
: path2 + "[" + JSON.stringify(key) + "]";
var combinedPath = newPath1 + newPath2;
// stop recursion if current objects are already compared
if (compared[combinedPath]) {
return true;
}
// remember the current objects and their paths
if (index1 === -1 && isObject1) {
objects1.push(value1);
paths1.push(newPath1);
}
if (index2 === -1 && isObject2) {
objects2.push(value2);
paths2.push(newPath2);
}
// remember that the current objects are already compared
if (isObject1 && isObject2) {
compared[combinedPath] = true;
}
// End of cyclic logic
// neither value1 nor value2 is a cycle
// continue with next level
return deepEqual(value1, value2, newPath1, newPath2);
});
})(first, second, "$1", "$2");
}
deepEqualCyclic.use = function(match) {
return function(a, b) {
return deepEqualCyclic(a, b, match);
};
};
module.exports = deepEqualCyclic;