定义

在开始之前,我们先定义一下什么叫相等。

  1. NaN 和 NaN 是相等
  2. [1] 和 [1] 是相等
  3. {value: 1} 和 {value: 1} 是相等

另外

  1. 1 和 new Number(1) 是相等
  2. ‘Curly’ 和 new String(‘Curly’) 是相等
  3. true 和 new Boolean(true) 是相等

一些有趣的现象

a === b 是 true 的话,a 和 b 就一定相等吗?一般情况下是这样的,但有个例外。

+0 和 -0

  1. console.log(+0 === -0);
  2. (+0).tostring() // 0
  3. (-0).toString() // 0
  4. -0 < +0 // false
  5. +0 < -0 // false

即使如此两者还是不同

  1. 1 / +0 // Infinity
  2. 1 / -0 // -Infiity
  3. 1 / +0 === -1 / 0 //false

为什么会有 +0 和 -0 了?那是因为 Javascript 采用了 IEEE_754 浮点数表法(这是一种二进制表示法,最高位是符号位(0 代表 + ,1 代表 -)),所以零有两个值(1000) 和 (0000)。

那如何判断出来了?可以利用 1 / + 0 === -1 / 0 => false 这特性。

  1. function eq(a, b) {
  2. if(a === b) return a !== 0 || 1 / a === 1 / b;
  3. return false
  4. }

NaN

虽然 NaN === NaN => false 但是我们认为 NaN 应该是等于自身的,我们可以利用他这个特性判断出来。

  1. function eq(a, b) {
  2. if(a !== a) return b !== b;
  3. }

eq 函数(第一版)

  1. function eq(a, b) {
  2. // 判断一些基本类型如: 1 === 1,true === true, 'str' === 'str', null === null
  3. // a !== 0 很简单就是用来判断不是 0 的情况
  4. // 我们认为 -0 !== +0,因为 1 / -0 => -Infinity,而 1 / +0 => Infinity,所以 1 / -0 === 1 / +0 => fasle
  5. if (a === b) return a !== 0 || 1 / a === 1 / b;
  6. //因为 null !== 任意类型(除非是自身,但是上面的 if 已经判断出来了)
  7. if (a === null || b === null) return false;
  8. // 这个专门用来判断 NaN,因为 NaN !== NaN
  9. if (a !== a) return b !== b;
  10. //来到这里的情况也就几种,
  11. // 1. a, b 都为基本类型但不相等分两种情况:同类型不相等(如:1 !== 2,true !== false,'str1' !== 'str2'),或者不同类型(1 !== true,'str' !== false)
  12. // 2. a, b 有一个是对象(object),另外一个是基本类型(如:a = 1,b = new Number(1); a = true, b = new Boolean(true); ....)
  13. // 3. a,b 都是对象(function,array, object)
  14. // 这里的 if 是判断第 1 种情况
  15. // 你可能会问 if 语句是不是差一个判断:type b !== 'function', 其实不是的因为如何假设 b 是一个函数,a 经过前两个 typeof 判断肯定是一个基本类型,但是基本类型肯定不等于函数啊。
  16. // 所以为了让这种情况早点判断出来所以就不写 type b !== 'function'
  17. const type = typeof a;
  18. if (type !== "function" && type !== "object" && typeof b !== "object") return false;
  19. // 把第 2,3 种情况放到 deepEq 函数来判断
  20. return deeepEq(a, b);
  21. }

string

在开始写 deepEq 函数之前,我们怎么判断如:’Curly’ 和 new String(‘Curly’) 这种情况了?

  1. console.log(typeof 'Curly'); // string
  2. console.log(typeof new String('Curly')); // object

一个是字符创,一个是对象。怎么办了?可以利用 Object.prototype.toString 方法。

  1. console.log(Object.prototype.toString.call('Curly')); // "[object String]"
  2. console.log(Object.prototype.toString.call(new String('Curly'))); // "[object String]"

只到调用 toString 方法结果就是一样的。

还可以利用隐式类型转换。

  1. console.log('Curly' + '' === new String('Curly') + '') // true

思路:如果 a 和 b 的 toString 的结果是一致,并且都是 “[object String]” 的话,那么可以利用 ‘’ + a === ‘’ + b 来判断。

更多对象

Boolean

  1. const a = true;
  2. const b = new Boolean(true);
  3. console.log(+a === +b) // true

Date

  1. const a = new Date(2009, 9, 25);
  2. const b = new Date(2009, 9, 25);
  3. console.log(+a === +b) // true

RegExp

  1. const a = /a/i;
  2. const b = new RegExp(/a/i);
  3. console.log('' + a === '' + b) // true

Number

  1. const a = 1;
  2. const b = new Number(1);
  3. console.log(+a === +b) // true

Number 就这么容易判断出来吗?

  1. const a = Number(NaN);
  2. const b = Number(NaN);
  3. console.log(+a === +b); // false

期望结果是 true,但是却是 false。

修改成这样

  1. const a = Number(NaN);
  2. const b = Number(NaN);
  3. function eq() {
  4. // 判断 Number(NaN) Object(NaN) 等情况
  5. if (+a !== +a) return +b !== +b;
  6. // 其他判断 ...
  7. }
  8. console.log(eq(a, b)); // true

deepEq 函数(第一版)

  1. // 原理(以 string 为例)
  2. // var toString = Object.prototype.toString;
  3. // const a = 'Curly', b = new String('Curly')
  4. // toString.call(a); // "[object String]"
  5. // toString.call(b); // "[object String]"
  6. // "" + a === "" + b => true
  7. //下面是判断第 2 种情况
  8. var toString = Object.prototype.toString;
  9. function deepEq(a, b) {
  10. const className = toString.call(a);
  11. // 如果不是同一个构造函数出来的直接出局
  12. if (className !== toString.call(b)) return false;
  13. switch (className) {
  14. case "[object RegExp]":
  15. case "[object String]":
  16. return "" + a === "" + b;
  17. case "[object Number]":
  18. // 用来判断 a = new Number(NaN),b = new Number(NaN)
  19. if (+a !== +a) return +b !== +b;
  20. // 如果 a 是 +0,-0,0 (注意:+a 只是把 a 变成数字,不会改变符号 +(-0) => -0)则进入第二个判断,否则进入第三个判断。
  21. return +a === 0 ? 1 / +a === 1 / b : +a === +b;
  22. case "[object Date]":
  23. case "[object Boolean]":
  24. return +a === +b;
  25. }
  26. }

构造函数实例

例子

  1. function Person() {
  2. this.name = name;
  3. }
  4. function Animal() {
  5. this.name = name
  6. }
  7. const person = new Person('David');
  8. const animal = new Animal('David');

虽然都是 {name: "David"} 但是我们认为这两个对象是不想的,因为分别属于不用的构造函数实例。

但是如何两个对象所属的构造函数所属不同,两个对象就一定不相等吗?

不一定,举个例子

  1. const attrs = Object.create(null);
  2. attr.name = 'Bob';
  3. eq(attrs, {name: 'Bob'}); // ???

虽然 attrs 没有原型, {name: "Bob"} 的原型是 Object ,但是在实际应用中,只要他们有着相同的键值对,就认为是相等。

继续写判断,对于不同的构造函数下的实例直接返回 false。

  1. // 现在只剩下第三种情况了,a,b 都为对象(function,array,object)
  2. function isFunction(obj) {
  3. return toString.call(obj) === "[object Function]";
  4. }
  5. function deepEq(a, b) {
  6. // 接着上面的内容
  7. var areArrays = className === "[object Array]";
  8. // 不是数组,只能是(object,function)
  9. if (!areArrays) {
  10. // 过滤掉两个函数的情况(只要有一个是函数都不可能相等)
  11. if (typeof a !== "object" || typeof b !== "object") return false;
  12. // a,b 都为对象(object)
  13. var aCtor = a.constructor,
  14. bCtor = b.constructor;
  15. // aCtor 和 bCtor 必须都存在并且都不是 Object 构造函数的情况下,aCtor 不等于 bCtor, 那这两个对象就真的不相等啦
  16. // 这个 if 可以过滤掉 第1种情况
  17. if (
  18. aCtor !== bCtor &&
  19. !(isFunction(aCtor) && aCtor instanceof aCtor && isFunction(bCtor) && bCtor instanceof bCtor) &&
  20. "constructor" in a &&
  21. "constructor" in b
  22. ) {
  23. return false;
  24. }
  25. }
  26. // 下面还有好多判断
  27. }
  28. // a,b 可能的情况
  29. function Person(name) {
  30. this.name = name;
  31. }
  32. function Animal(name) {
  33. this.name = name;
  34. }
  35. // 第1种
  36. a = new Person("test");
  37. b = new Animal("test");
  38. // 第2种
  39. a = Object.create(null);
  40. a.test = 1;
  41. b = { test: 1 };
  42. // 第3种
  43. a = { test: 1 };
  44. b = { test: 2 };
  45. // 第4种
  46. a = [1,2,3];
  47. b = [1,2,3];

剩下的 2,3,4 留到下面判断。

数组相等

就是递归遍历一遍,代码。

  1. function deepEq(a, b) {
  2. // 再接着上面的内容
  3. if (areArrays) {
  4. length = a.length;
  5. if (length !== b.length) return false;
  6. while (length--) {
  7. if (!eq(a[length], b[length])) return false;
  8. }
  9. }
  10. else {
  11. let keys = Object.keys(a), key;
  12. length = keys.length;
  13. if (Object.keys(b).length !== length) return false;
  14. while (length--) {
  15. key = keys[length];
  16. if (!(b.hasOwnProperty(key) && eq(a[key], b[key]))) return false;
  17. }
  18. }
  19. return true;
  20. }

循环引用

你以就这么简单,那太天真了!!!最难的问题来了:循环引用。

例子

  1. const a = {abc: null};
  2. const b = {abc: null};
  3. a.abc = a;
  4. b.abc = b;
  5. eq(a, b)

再复杂一点。

  1. const a = {foo: {b: {foo: {c: {foo: null}}}}};
  2. const b = {foo: {b: {foo: {c: {foo: null}}}}};
  3. a.foo.b.foo.c.foo = a;
  4. b.foo.b.foo.c.foo = b;
  5. eq(a, b)

为了演示,写一个精简后的代码。

  1. const a, b;
  2. a = { foo: { b: { foo: { c: { foo: null } } } } };
  3. b = { foo: { b: { foo: { c: { foo: null } } } } };
  4. a.foo.b.foo.c.foo = a;
  5. b.foo.b.foo.c.foo = b;
  6. function eq(a, b, aStack, bStack) {
  7. if (typeof a == 'number') {
  8. return a === b;
  9. }
  10. return deepEq(a, b)
  11. }
  12. function deepEq(a, b) {
  13. let keys = Object.keys(a);
  14. let length = keys.length;
  15. let key;
  16. while (length--) {
  17. key = keys[length]
  18. // 这是为了让你看到代码其实一直在执行
  19. console.log(a[key], b[key])
  20. if (!eq(a[key], b[key])) return false;
  21. }
  22. return true;
  23. }
  24. eq(a, b)

死循环了!!!,那怎样解决了?underscore 的思路是 eq 的时候,多传递两个参数为 aStack 和 bStack,用来储存 a 和 b 递归比较过程中的 a 和 b 的值,咋说的这么绕口呢?

例子

  1. const a, b;
  2. a = { foo: { b: { foo: { c: { foo: null } } } } };
  3. b = { foo: { b: { foo: { c: { foo: null } } } } };
  4. a.foo.b.foo.c.foo = a;
  5. b.foo.b.foo.c.foo = b;
  6. function eq(a, b, aStack, bStack) {
  7. if (typeof a == 'number') {
  8. return a === b;
  9. }
  10. return deepEq(a, b, aStack, bStack)
  11. }
  12. function deepEq(a, b, aStack, bStack) {
  13. aStack = aStack || [];
  14. bStack = bStack || [];
  15. var length = aStack.length;
  16. while (length--) {
  17. if (aStack[length] === a) {
  18. return bStack[length] === b;
  19. }
  20. }
  21. aStack.push(a);
  22. bStack.push(b);
  23. var keys = Object.keys(a);
  24. var length = keys.length;
  25. var key;
  26. while (length--) {
  27. key = keys[length]
  28. console.log(a[key], b[key], aStack, bStack)
  29. if (!eq(a[key], b[key], aStack, bStack)) return false;
  30. }
  31. // aStack.pop();
  32. // bStack.pop();
  33. return true;
  34. }
  35. console.log(eq(a, b))

之所以注释掉 aStack.pop()bStack.pop() 这两句,是为了方便大家查看 aStack bStack的值。

最终的 eq 函数


  1. var toString = Object.prototype.toString;
  2. function isFunction(obj) {
  3. return toString.call(obj) === '[object Function]'
  4. }
  5. function eq(a, b, aStack, bStack) {
  6. // === 结果为 true 的区别出 +0 和 -0
  7. if (a === b) return a !== 0 || 1 / a === 1 / b;
  8. // typeof null 的结果为 object ,这里做判断,是为了让有 null 的情况尽早退出函数
  9. if (a == null || b == null) return false;
  10. // 判断 NaN
  11. if (a !== a) return b !== b;
  12. // 判断参数 a 类型,如果是基本类型,在这里可以直接返回 false
  13. var type = typeof a;
  14. if (type !== 'function' && type !== 'object' && typeof b != 'object') return false;
  15. // 更复杂的对象使用 deepEq 函数进行深度比较
  16. return deepEq(a, b, aStack, bStack);
  17. };
  18. function deepEq(a, b, aStack, bStack) {
  19. // a 和 b 的内部属性 [[class]] 相同时 返回 true
  20. var className = toString.call(a);
  21. if (className !== toString.call(b)) return false;
  22. switch (className) {
  23. case '[object RegExp]':
  24. case '[object String]':
  25. return '' + a === '' + b;
  26. case '[object Number]':
  27. if (+a !== +a) return +b !== +b;
  28. return +a === 0 ? 1 / +a === 1 / b : +a === +b;
  29. case '[object Date]':
  30. case '[object Boolean]':
  31. return +a === +b;
  32. }
  33. var areArrays = className === '[object Array]';
  34. // 不是数组
  35. if (!areArrays) {
  36. // 过滤掉两个函数的情况
  37. if (typeof a != 'object' || typeof b != 'object') return false;
  38. var aCtor = a.constructor,
  39. bCtor = b.constructor;
  40. // aCtor 和 bCtor 必须都存在并且都不是 Object 构造函数的情况下,aCtor 不等于 bCtor, 那这两个对象就真的不相等啦
  41. if (aCtor !== bCtor && !(isFunction(aCtor) && aCtor instanceof aCtor && isFunction(bCtor) && bCtor instanceof bCtor) && ('constructor' in a && 'constructor' in b)) {
  42. return false;
  43. }
  44. }
  45. aStack = aStack || [];
  46. bStack = bStack || [];
  47. var length = aStack.length;
  48. // 检查是否有循环引用的部分
  49. while (length--) {
  50. if (aStack[length] === a) {
  51. return bStack[length] === b;
  52. }
  53. }
  54. aStack.push(a);
  55. bStack.push(b);
  56. // 数组判断
  57. if (areArrays) {
  58. length = a.length;
  59. if (length !== b.length) return false;
  60. while (length--) {
  61. if (!eq(a[length], b[length], aStack, bStack)) return false;
  62. }
  63. }
  64. // 对象判断
  65. else {
  66. var keys = Object.keys(a),
  67. key;
  68. length = keys.length;
  69. if (Object.keys(b).length !== length) return false;
  70. while (length--) {
  71. key = keys[length];
  72. if (!(b.hasOwnProperty(key) && eq(a[key], b[key], aStack, bStack))) return false;
  73. }
  74. }
  75. aStack.pop();
  76. bStack.pop();
  77. return true;
  78. }
  79. console.log(eq(0, 0)) // true
  80. console.log(eq(0, -0)) // false
  81. console.log(eq(NaN, NaN)); // true
  82. console.log(eq(Number(NaN), Number(NaN))); // true
  83. console.log(eq('Curly', new String('Curly'))); // true
  84. console.log(eq([1], [1])); // true
  85. console.log(eq({ value: 1 }, { value: 1 })); // true
  86. var a, b;
  87. a = { foo: { b: { foo: { c: { foo: null } } } } };
  88. b = { foo: { b: { foo: { c: { foo: null } } } } };
  89. a.foo.b.foo.c.foo = a;
  90. b.foo.b.foo.c.foo = b;
  91. console.log(eq(a, b)) // true

参考:

[1] JavaScript专题之如何判断两个对象相等