-
Notifications
You must be signed in to change notification settings - Fork 780
/
equiv.js
346 lines (276 loc) · 9.03 KB
/
equiv.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
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
import { objectType } from "./core/utilities";
// Test for equality any JavaScript type.
// Authors: Philippe Rathé <prathe@gmail.com>, David Chan <david@troi.org>
export default ( function() {
// Value pairs queued for comparison. Used for breadth-first processing order, recursion
// detection and avoiding repeated comparison (see below for details).
// Elements are { a: val, b: val }.
var pairs = [];
var getProto = Object.getPrototypeOf || function( obj ) {
return obj.__proto__;
};
function useStrictEquality( a, b ) {
// This only gets called if a and b are not strict equal, and is used to compare on
// the primitive values inside object wrappers. For example:
// `var i = 1;`
// `var j = new Number(1);`
// Neither a nor b can be null, as a !== b and they have the same type.
if ( typeof a === "object" ) {
a = a.valueOf();
}
if ( typeof b === "object" ) {
b = b.valueOf();
}
return a === b;
}
function compareConstructors( a, b ) {
var protoA = getProto( a );
var protoB = getProto( b );
// Comparing constructors is more strict than using `instanceof`
if ( a.constructor === b.constructor ) {
return true;
}
// Ref #851
// If the obj prototype descends from a null constructor, treat it
// as a null prototype.
if ( protoA && protoA.constructor === null ) {
protoA = null;
}
if ( protoB && protoB.constructor === null ) {
protoB = null;
}
// Allow objects with no prototype to be equivalent to
// objects with Object as their constructor.
if ( ( protoA === null && protoB === Object.prototype ) ||
( protoB === null && protoA === Object.prototype ) ) {
return true;
}
return false;
}
function getRegExpFlags( regexp ) {
return "flags" in regexp ? regexp.flags : regexp.toString().match( /[gimuy]*$/ )[ 0 ];
}
function isContainer( val ) {
return [ "object", "array", "map", "set" ].indexOf( objectType( val ) ) !== -1;
}
function breadthFirstCompareChild( a, b ) {
// If a is a container not reference-equal to b, postpone the comparison to the
// end of the pairs queue -- unless (a, b) has been seen before, in which case skip
// over the pair.
if ( a === b ) {
return true;
}
if ( !isContainer( a ) ) {
return typeEquiv( a, b );
}
if ( pairs.every( function( pair ) {
return pair.a !== a || pair.b !== b;
} ) ) {
// Not yet started comparing this pair
pairs.push( { a: a, b: b } );
}
return true;
}
var callbacks = {
"string": useStrictEquality,
"boolean": useStrictEquality,
"number": useStrictEquality,
"null": useStrictEquality,
"undefined": useStrictEquality,
"symbol": useStrictEquality,
"date": useStrictEquality,
"nan": function() {
return true;
},
"regexp": function( a, b ) {
return a.source === b.source &&
// Include flags in the comparison
getRegExpFlags( a ) === getRegExpFlags( b );
},
// abort (identical references / instance methods were skipped earlier)
"function": function() {
return false;
},
"array": function( a, b ) {
var i, len;
len = a.length;
if ( len !== b.length ) {
// Safe and faster
return false;
}
for ( i = 0; i < len; i++ ) {
// Compare non-containers; queue non-reference-equal containers
if ( !breadthFirstCompareChild( a[ i ], b[ i ] ) ) {
return false;
}
}
return true;
},
// Define sets a and b to be equivalent if for each element aVal in a, there
// is some element bVal in b such that aVal and bVal are equivalent. Element
// repetitions are not counted, so these are equivalent:
// a = new Set( [ {}, [], [] ] );
// b = new Set( [ {}, {}, [] ] );
"set": function( a, b ) {
var innerEq,
outerEq = true;
if ( a.size !== b.size ) {
// This optimization has certain quirks because of the lack of
// repetition counting. For instance, adding the same
// (reference-identical) element to two equivalent sets can
// make them non-equivalent.
return false;
}
a.forEach( function( aVal ) {
// Short-circuit if the result is already known. (Using for...of
// with a break clause would be cleaner here, but it would cause
// a syntax error on older Javascript implementations even if
// Set is unused)
if ( !outerEq ) {
return;
}
innerEq = false;
b.forEach( function( bVal ) {
var parentPairs;
// Likewise, short-circuit if the result is already known
if ( innerEq ) {
return;
}
// Swap out the global pairs list, as the nested call to
// innerEquiv will clobber its contents
parentPairs = pairs;
if ( innerEquiv( bVal, aVal ) ) {
innerEq = true;
}
// Replace the global pairs list
pairs = parentPairs;
} );
if ( !innerEq ) {
outerEq = false;
}
} );
return outerEq;
},
// Define maps a and b to be equivalent if for each key-value pair (aKey, aVal)
// in a, there is some key-value pair (bKey, bVal) in b such that
// [ aKey, aVal ] and [ bKey, bVal ] are equivalent. Key repetitions are not
// counted, so these are equivalent:
// a = new Map( [ [ {}, 1 ], [ {}, 1 ], [ [], 1 ] ] );
// b = new Map( [ [ {}, 1 ], [ [], 1 ], [ [], 1 ] ] );
"map": function( a, b ) {
var innerEq,
outerEq = true;
if ( a.size !== b.size ) {
// This optimization has certain quirks because of the lack of
// repetition counting. For instance, adding the same
// (reference-identical) key-value pair to two equivalent maps
// can make them non-equivalent.
return false;
}
a.forEach( function( aVal, aKey ) {
// Short-circuit if the result is already known. (Using for...of
// with a break clause would be cleaner here, but it would cause
// a syntax error on older Javascript implementations even if
// Map is unused)
if ( !outerEq ) {
return;
}
innerEq = false;
b.forEach( function( bVal, bKey ) {
var parentPairs;
// Likewise, short-circuit if the result is already known
if ( innerEq ) {
return;
}
// Swap out the global pairs list, as the nested call to
// innerEquiv will clobber its contents
parentPairs = pairs;
if ( innerEquiv( [ bVal, bKey ], [ aVal, aKey ] ) ) {
innerEq = true;
}
// Replace the global pairs list
pairs = parentPairs;
} );
if ( !innerEq ) {
outerEq = false;
}
} );
return outerEq;
},
"object": function( a, b ) {
var i,
aProperties = [],
bProperties = [];
if ( compareConstructors( a, b ) === false ) {
return false;
}
// Be strict: don't ensure hasOwnProperty and go deep
for ( i in a ) {
// Collect a's properties
aProperties.push( i );
// Skip OOP methods that look the same
if (
a.constructor !== Object &&
typeof a.constructor !== "undefined" &&
typeof a[ i ] === "function" &&
typeof b[ i ] === "function" &&
a[ i ].toString() === b[ i ].toString()
) {
continue;
}
// Compare non-containers; queue non-reference-equal containers
if ( !breadthFirstCompareChild( a[ i ], b[ i ] ) ) {
return false;
}
}
for ( i in b ) {
// Collect b's properties
bProperties.push( i );
}
// Ensures identical properties name
return typeEquiv( aProperties.sort(), bProperties.sort() );
}
};
function typeEquiv( a, b ) {
var type = objectType( a );
// Callbacks for containers will append to the pairs queue to achieve breadth-first
// search order. The pairs queue is also used to avoid reprocessing any pair of
// containers that are reference-equal to a previously visited pair (a special case
// this being recursion detection).
//
// Because of this approach, once typeEquiv returns a false value, it should not be
// called again without clearing the pair queue else it may wrongly report a visited
// pair as being equivalent.
return objectType( b ) === type && callbacks[ type ]( a, b );
}
function innerEquiv( a, b ) {
var i, pair;
// We're done when there's nothing more to compare
if ( arguments.length < 2 ) {
return true;
}
// Clear the global pair queue and add the top-level values being compared
pairs = [ { a: a, b: b } ];
for ( i = 0; i < pairs.length; i++ ) {
pair = pairs[ i ];
// Perform type-specific comparison on any pairs that are not strictly
// equal. For container types, that comparison will postpone comparison
// of any sub-container pair to the end of the pair queue. This gives
// breadth-first search order. It also avoids the reprocessing of
// reference-equal siblings, cousins etc, which can have a significant speed
// impact when comparing a container of small objects each of which has a
// reference to the same (singleton) large object.
if ( pair.a !== pair.b && !typeEquiv( pair.a, pair.b ) ) {
return false;
}
}
// ...across all consecutive argument pairs
return arguments.length === 2 || innerEquiv.apply( this, [].slice.call( arguments, 1 ) );
}
return ( ...args ) => {
const result = innerEquiv( ...args );
// Release any retained objects
pairs.length = 0;
return result;
};
}() );