deep-equal.js 6.3 KB

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