学习underscore源码之比较两个对象'相等'

1,138 阅读6分钟

前言

学习的underscore.js 源码版本为1.13.1

_.isEqual

执行两个对象之间的深度比较,确定他们是否应被视为相等。

先看使用效果:

// +0 与 -0 不相等
_.isEqual(0, -0);  // false

// NaN 与 NaN 相等
_.isEqual(NaN, NaN);  // true

// '5' 与 new String('5') 相等
_.isEqual('5', new String('5'));  // true

// true 与 new Boolean(true) 相等
_.isEqual(true, new Boolean(true));  // true

// new Date('2021-12-06') 与 new Date('2021-12-06') 相等
_.isEqual(new Date('2021-12-06'), new Date('2021-12-06'));  // true

// /d+/g 与 new RegExp('d+', 'g') 相等
_.isEqual(/d+/g, new RegExp('d+', 'g'));  // true

// {name: 'zxx', age: 18, hobbies: ['movie', 'sport']} 与 {age: 18, hobbies: ['movie', 'sport'], name: 'zxx'} 相等
_.isEqual({name: 'zxx', age: 18, hobbies: ['movie', 'sport']}, {age: 18, hobbies: ['movie', 'sport'], name: 'zxx'});  // true

var a = {foo: {b: {foo: {c: {foo: null}}}}};
var b = {foo: {b: {foo: {c: {foo: null}}}}};
a.foo.b.foo.c.foo = a;
b.foo.b.foo.c.foo = b;
_.isEqual(a, b); // true

我们来一步步实现这个判断两个对象/参数是否相等的函数

function eq(a, b) {
    // 主体比较代码
}

+0 与 -0

如果a === b的结果为true, 那么 a 和 b 就是相等的吗?一般情况下,当然是这样的,但是有一个特殊的例子,就是 +0 和 -0。

console.log(+0 === -0); // true
 
(-0).toString(); // '0'
(+0).toString(); // '0'
 
-0 < +0; // false
+0 < -0; // false

但两者还是不同:

1 / +0 // Infinity
1 / -0 // -Infinity
 
1 / +0 === 1 / -0; // false 

之所以会有+0 和 -0 是因为javascript采用了IEEE_754浮点数表示法,这是一种二进制表示法,其中最高位表示符号位(0表示正,1表示负),剩下的用于表示大小。

以下情况会产生-0

Math.round(-0.1) // -0

实现 +0 与 -0 不相等的eq函数

function eq(a, b) {
  if (a === b) {
      return a !== 0 || 1 / a === 1 / b;
  }  
  return false;
}
console.log(eq(0, 0));  // true
console.log(eq(0, -0)); // false

NaN

利用NaN 不等于自身的特性

console.log(NaN === NaN); // false

可以很容易的实现eq:

function eq(a, b) {
  if (a !== a) {
      return b !== b;
  }
}
console.log(eq(NaN, NaN)); // true

比较的两参数中有null、undefined、 函数的情况

function eq(a, b) {
  // 参数中有null、undefined的情况
  if (a == null || b == null) return false; 
  // a是基本类型且b不是对象类型(没有必要判断b类型是不是function, 因为基本类型和函数肯定是不会相等的)就直接返回false
  var type = typeof a;
  if (type !== 'function' && type !== 'object' && typeof b != 'object') return false;
}

小结

第一版eq函数

// 第一版先用来过滤简单的类型比较,复杂类型使用deepEq函数进行处理
function eq(a, b) {
  // === 结果为true的区别出+0 和 -0
  if (a === b) return a !== 0 || 1 / a === 1 / b;

  // 参数中有null、undefined的情况
  if (a == null || b == null) return false;

  // 判断NaN
  if (a !== a) return b !== b;

 // a是基本类型且b不是对象类型(没有必要判断b类型是不是function, 因为基本类型和函数肯定是不会相等的)就直接返回false  
  var type = typeof a;
  if (type !== 'function' && type !== 'object' && typeof b != 'object') return false;
  // 深度比较两对象
  return deepEq(a, b);
}

处理RegExp、String、Number、Date、Boolean对象

先看如下代码:

'' + /d+/g = '' + new RegExp('d+', 'g'); // true

'5' === '' + new String('5');  // true

1 === + new Number(1);  // true

+ new Date('2021-12-06') === + new Date('2021-12-06'); // true

+ false === + new Boolean(false);  // true

很明显是利用了隐式类型转换进行最终的比较。

所以我们可以先利用Object.prototype.toString来判断类型是否一致

var toString = Object.prototype.toString;
toString.call('5') === toString.call(new String('5')); // true

再利用隐式类型转换进行最终的比较。

部分deepEq函数:

var toString = Object.prototype.toString;

function deepEq(a, b) {
  var className = toString.call(a);
  if (className !== toString.call(b)) return false;

  switch (className) {
    case "[object RegExp]":
    case "[object String]":
      return "" + a === "" + b;
    case "[object Number]":
      // 判断Object(NaN)、Number(NaN)
      if (+a !== +a) return +b !== +b;
      return +a === 0 ? 1 / +a === 1 / b : +a === +b;
    case "[object Date]":
    case "[object Boolean]":
      return +a === +b;
  }
  // ...
}

注: 需要考虑Object(NaN)、Number(NaN)

处理Symbol

Symbol表示独一无二的值,处不处理都一样

var SymbolProto = typeof Symbol !== 'undefined' ? Symbol.prototype : null;

// ...省略
case '[object Symbol]':
   return SymbolProto.valueOf.call(a) === SymbolProto.valueOf.call(b);

处理Object

isEqual测试用例77条

Objects with different constructors and identical own properties are not equal 具有不同构造函数和相同自身属性的对象不相等

function First () {
  this.value = 1;
}
function Second () {
  this.value = 1;
}
var first = new First();
var second = new Second();
eq(first, second);  // false

实现

function isFunction (func) {
  return Object.prototype.toString.call(func) === '[object Function]'
};

function deepEq (a, b) {
  // 接以上代码
  var areArrays = className === '[object Array]';
  // 不是数组
  if (!areArrays) {
    // 过滤掉函数的情况
    if (typeof a != 'object' || typeof b != 'object') return false;

    // 构造函数不相同且不全是Object 构造函数的情况下 不相等
    var aCtor = a.constructor, bCtor = b.constructor;
    if (aCtor !== bCtor && !(isFunction(aCtor) && aCtor instanceof aCtor && isFunction(bCtor) && bCtor instanceof bCtor) && ('constructor' in a && 'constructor' in b)) {
      return false;
    }
  }
}

isEqual测试用例69条

Objects containing equivalent members are equal 包含相同成员的对象是相等的

eq({a: /Curly/g, b: new Date(2009, 11, 13)}, {a: /Curly/g, b: new Date(2009, 11, 13)}); // true

所以我们需要比较属性、值是否一致

var nativeKeys = Object.keys;

function isObject(obj) {
  var type = typeof obj;
  return type === "function" || (type === "object" && !!obj);
}

// 获取对象自身可枚举属性组成的数组
function keys(obj) {
  if (!isObject(obj)) return [];
  if (nativeKeys) return nativeKeys(obj);
}

function has(obj, key) {
  return obj != null && Object.prototype.hasOwnProperty.call(obj, key);
}

function deepEq (a, b) {

  var areArrays = className === '[object Array]';
  if (!areArrays) {
    // 过滤掉函数的情况
    if (typeof a != 'object' || typeof b != 'object') return false;

    // 构造函数不相同且都不全是Object 构造函数的情况下 不相等
    var aCtor = a.constructor, bCtor = b.constructor;
    if (aCtor !== bCtor && !(isFunction(aCtor) && aCtor instanceof aCtor &&
    isFunction(bCtor) && bCtor instanceof bCtor)
                        && ('constructor' in a && 'constructor' in b)) {
      return false;
    }


    var _keys = keys(a), key;
    length = _keys.length;
    // 在递归比较之前,确保两个对象包含相同数量的属性
    if (keys(b).length !== length) return false;
    while (length--) {
      // 属性名及属性值比较
      key = _keys[length];
      if (!(has(b, key) && eq(a[key], b[key]))) return false;
    }
  }
  // 其他判断
  return true;
}

处理Array

function deepEq (a, b) {

  // 接以上
  if (areArrays) {
    // 先比较长度
    length = a.length;
    if (length !== b.length) return false;
    while (length--) {
      if (!eq(a[length], b[length])) return false;
    }
  } else {
    var _keys = keys(a), key;
    length = _keys.length;
    // 在递归比较之前,确保两个对象包含相同数量的属性
    if (keys(b).length !== length) return false;
    while (length--) {
      // 递归比较
      key = _keys[length];
      if (!(has(b, key) && eq(a[key], b[key]))) return false;
    }
  }
  return true;
}

处理循环引用

var a = {abc: null};
var b = {abc: null};
a.abc = a;
b.abc = b;

// 更复杂的
a = {foo: {b: {foo: {c: {foo: null}}}}};
b = {foo: {b: {foo: {c: {foo: null}}}}};
a.foo.b.foo.c.foo = a;
b.foo.b.foo.c.foo = b;

eq(a, b); // true

underscore 处理这个问题是多传递两个参数为 aStack 和 bStack,用来储存 a 和 b 递归比较过程中的 a 和 b 的值:


function eq(a, b, aStack, bStack) {
  // ...
  return deepEq(a, b, aStack, bStack);
}

function deepEq (a, b, aStack, bStack) {
  var className = toString.call(a);
  if (className !== toString.call(b)) return false;

  switch (className) {
    case "[object RegExp]":
    case "[object String]":
      return "" + a === "" + b;
    case "[object Number]":
      // 判断Object(NaN)、Number(NaN)
      if (+a !== +a) return +b !== +b;
      return +a === 0 ? 1 / +a === 1 / b : +a === +b;
    case "[object Date]":
    case "[object Boolean]":
      return +a === +b;
    case '[object Symbol]':
      return SymbolProto.valueOf.call(a) === SymbolProto.valueOf.call(b);
  }
  var areArrays = className === '[object Array]';
  if (!areArrays) {
    // 过滤掉函数的情况
    if (typeof a != 'object' || typeof b != 'object') return false;

    // 构造函数不相同且都不全是Object 构造函数的情况下 不相等
    var aCtor = a.constructor, bCtor = b.constructor;
    if (aCtor !== bCtor && !(isFunction(aCtor) && aCtor instanceof aCtor &&
    isFunction(bCtor) && bCtor instanceof bCtor)
                        && ('constructor' in a && 'constructor' in b)) {
      return false;
    }
  }
  aStack = aStack || [];
  bStack = bStack || [];
  var length = aStack.length;
  while (length--) {
    if (aStack[length] === a) return bStack[length] === b;
  }

  // 将第一个对象添加到遍历对象的栈中
  aStack.push(a);
  bStack.push(b);

  if (areArrays) {
    // 先比较长度
    length = a.length;
    if (length !== b.length) return false;
    while (length--) {
      if (!eq(a[length], b[length], aStack, bStack)) return false;
    }
  } else {
    var _keys = keys(a), key;
    length = _keys.length;
    // 在递归比较之前,确保两个对象包含相同数量的属性
    if (keys(b).length !== length) return false;
    while (length--) {
      // 递归比较
      key = _keys[length];
      if (!(has(b, key) && eq(a[key], b[key], aStack, bStack))) return false;
    }
  }
  return true;
}

其它

处理ArrayBuffer及DataView

在IE 10 - Edge 13浏览器中

var buffer = new ArrayBuffer(16);
var view = new DataView(buffer);
// IE 10 - Edge 13浏览器中为true
Object.prototype.toString.call(DataView) === '[object Object]'

image.png

可以将它们转换成Uint8Array进行比较

var tagDataView = '[object DataView]';
var supportsDataView = typeof DataView !== 'undefined';
var hasObjectTag = function (obj) {
  return Object.prototype.toString.call(obj) === '[object Object]'
};
var isDataView = function (dataView) {
  return Object.prototype.toString.call(dataView) === '[object DataView]'
};
//  IE 10 - Edge 13浏览器, `DataView` has string tag `'[object Object]'`.
var hasStringTagBug = (supportsDataView && hasObjectTag(new DataView(new ArrayBuffer(8))));

function ie10IsDataView(obj) {
  return obj != null && isFunction(obj.getInt8) && isArrayBuffer(obj.buffer);
}

var isDataView$1 = (hasStringTagBug ? ie10IsDataView : isDataView);

function toBufferView(bufferSource) {
  return new Uint8Array(
    bufferSource.buffer || bufferSource,
    bufferSource.byteOffset || 0,
    (bufferSource == null ? void 0 : bufferSource['byteLength'])
  );
}

function deepEq(a, b, aStack, bStack) {
  switch (className) {
    // ...
    case '[object ArrayBuffer]':
    case tagDataView:
      return deepEq(toBufferView(a), toBufferView(b), aStack, bStack);
  }
}

处理 TypedArray

var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;
var supportsArrayBuffer = typeof ArrayBuffer !== "undefined";
var nativeIsView = supportsArrayBuffer && ArrayBuffer.isView;
var typedArrayPattern = /\[object ((I|Ui)nt(8|16|32)|Float(32|64)|Uint8Clamped|Big(I|Ui)nt64)Array\]/;

var isBufferLike = function (collection) {
  var sizeProperty = collection == null ? void 0 : collection["byteLength"];
  return typeof sizeProperty == "number" && sizeProperty >= 0 && sizeProperty <= MAX_ARRAY_INDEX;
};

function constant(value) {
  return function () {
    return value;
  };
}

function isTypedArray(obj) {
  return nativeIsView
    ? nativeIsView(obj) && !isDataView$1(obj)
    : isBufferLike(obj) && typedArrayPattern.test(toString.call(obj));
}

var isTypedArray$1 = supportsArrayBuffer ? isTypedArray : constant(false);


function deepEq(a, b, aStack, bStack) {
  // ...
  // 处理类型化数组
  if (!areArrays && isTypedArray$1(a)) {
    var byteLength = a.byteLength;
    if (byteLength !== b.byteLength) return false;
    if (a.buffer === b.buffer && a.byteOffset === b.byteOffset) return true;
    areArrays = true;
  }
  // ...
}

最终代码

var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;
var tagDataView = '[object DataView]';
var supportsDataView = typeof DataView !== 'undefined';
var hasObjectTag = function (obj) {
  return Object.prototype.toString.call(obj) === '[object Object]'
};
var isDataView = function (dataView) {
  return Object.prototype.toString.call(dataView) === '[object DataView]'
};
//  IE 10 - Edge 13浏览器, `DataView` has string tag `'[object Object]'`.
var hasStringTagBug = (supportsDataView && hasObjectTag(new DataView(new ArrayBuffer(8))));

var supportsArrayBuffer = typeof ArrayBuffer !== "undefined";
var nativeIsView = supportsArrayBuffer && ArrayBuffer.isView;
var typedArrayPattern = /\[object ((I|Ui)nt(8|16|32)|Float(32|64)|Uint8Clamped|Big(I|Ui)nt64)Array\]/;

var isBufferLike = function (collection) {
  var sizeProperty = collection == null ? void 0 : collection["byteLength"];
  return typeof sizeProperty == "number" && sizeProperty >= 0 && sizeProperty <= MAX_ARRAY_INDEX;
};

function ie10IsDataView(obj) {
  return obj != null && isFunction(obj.getInt8) && isArrayBuffer(obj.buffer);
}

var isDataView$1 = (hasStringTagBug ? ie10IsDataView : isDataView);

function constant(value) {
  return function () {
    return value;
  };
}

function isTypedArray(obj) {
  return nativeIsView
    ? nativeIsView(obj) && !isDataView$1(obj)
    : isBufferLike(obj) && typedArrayPattern.test(toString.call(obj));
}

var isTypedArray$1 = supportsArrayBuffer ? isTypedArray : constant(false);

function toBufferView(bufferSource) {
  return new Uint8Array(
    bufferSource.buffer || bufferSource,
    bufferSource.byteOffset || 0,
    (bufferSource == null ? void 0 : bufferSource['byteLength'])
  );
}

var SymbolProto = typeof Symbol !== 'undefined' ? Symbol.prototype : null;
var toString = Object.prototype.toString;

var nativeKeys = Object.keys;

function isObject(obj) {
  var type = typeof obj;
  return type === "function" || (type === "object" && !!obj);
}

function isFunction (func) {
  return Object.prototype.toString.call(func) === '[object Function]'
};

// 获取对象自身可枚举属性组成的数组
function keys(obj) {
  if (!isObject(obj)) return [];
  if (nativeKeys) return nativeKeys(obj);
}

function has(obj, key) {
  return obj != null && Object.prototype.hasOwnProperty.call(obj, key);
}


function eq(a, b, aStack, bStack) {
  // === 结果为true的区别出+0 和 -0
  if (a === b) return a !== 0 || 1 / a === 1 / b;

  // 参数中有null、undefined的情况
  if (a == null || b == null) return false;

  // 判断NaN
  if (a !== a) return b !== b;

 // a是基本类型且b不是对象类型(没有必要判断b类型是不是function, 因为基本类型和函数肯定是不会相等的)就直接返回false  
  var type = typeof a;
  if (type !== 'function' && type !== 'object' && typeof b != 'object') return false;
  // 深度比较两对象
  return deepEq(a, b, aStack, bStack);
}

function deepEq (a, b, aStack, bStack) {
  var className = toString.call(a);
  if (className !== toString.call(b)) return false;

  switch (className) {
    case "[object RegExp]":
    case "[object String]":
      return "" + a === "" + b;
    case "[object Number]":
      // 判断Object(NaN)、Number(NaN)
      if (+a !== +a) return +b !== +b;
      return +a === 0 ? 1 / +a === 1 / b : +a === +b;
    case "[object Date]":
    case "[object Boolean]":
      return +a === +b;
    case '[object Symbol]':
      return SymbolProto.valueOf.call(a) === SymbolProto.valueOf.call(b);
    case '[object ArrayBuffer]':
    case tagDataView:
      return deepEq(toBufferView(a), toBufferView(b), aStack, bStack);
  }

 
  var areArrays = className === '[object Array]';
  
  // 处理类型化数组
  if (!areArrays && isTypedArray$1(a)) {
    var byteLength = a.byteLength;
    if (byteLength !== b.byteLength) return false;
    if (a.buffer === b.buffer && a.byteOffset === b.byteOffset) return true;
    areArrays = true;
  }
  
  if (!areArrays) {
    // 过滤掉函数的情况
    if (typeof a != 'object' || typeof b != 'object') return false;

    // 构造函数不相同且都不全是Object 构造函数的情况下 不相等
    var aCtor = a.constructor, bCtor = b.constructor;
    if (aCtor !== bCtor && !(isFunction(aCtor) && aCtor instanceof aCtor &&
    isFunction(bCtor) && bCtor instanceof bCtor)
                        && ('constructor' in a && 'constructor' in b)) {
      return false;
    }
  }
  aStack = aStack || [];
  bStack = bStack || [];
  var length = aStack.length;
  while (length--) {
    if (aStack[length] === a) return bStack[length] === b;
  }

  // 将第一个对象添加到遍历对象的栈中
  aStack.push(a);
  bStack.push(b);

  if (areArrays) {
    // 先比较长度
    length = a.length;
    if (length !== b.length) return false;
    while (length--) {
      if (!eq(a[length], b[length], aStack, bStack)) return false;
    }
  } else {
    var _keys = keys(a), key;
    length = _keys.length;
    // 在递归比较之前,确保两个对象包含相同数量的属性
    if (keys(b).length !== length) return false;
    while (length--) {
      // 递归比较
      key = _keys[length];
      if (!(has(b, key) && eq(a[key], b[key], aStack, bStack))) return false;
    }
  }
  return true;
}

测试:

可以看下underscorejs.net/test/object… 关于_.isEqual的测试用例

eq(0, -0);  // false

eq(NaN, NaN);  // false

var a = {
  name: new String("Moe Howard"),
  age: new Number(77),
  stooge: true,
  hobbies: ["acting"],
  film: {
    name: "Sing a Song of Six Pants",
    release: new Date(1947, 9, 30),
    stars: [new String("Larry Fine"), "Shemp Howard"],
    minutes: new Number(16),
    seconds: 54,
  },
};

var b = {
  name: new String("Moe Howard"),
  age: new Number(77),
  stooge: true,
  hobbies: ["acting"],
  film: {
    name: "Sing a Song of Six Pants",
    release: new Date(1947, 9, 30),
    stars: [new String("Larry Fine"), "Shemp Howard"],
    minutes: new Number(16),
    seconds: 54,
  },
};
eq(a, b);  // true



a = Object.create(null, {x: {value: 1, enumerable: true}});
b = {x: 1};
eq(a, b);  // true

// Circular Arrays.
(a = []).push(a);
(b = []).push(b);
eq(a, b);  // true




a = {foo: {b: {foo: {c: {foo: null}}}}};
b = {foo: {b: {foo: {c: {foo: null}}}}};
a.foo.b.foo.c.foo = a;
b.foo.b.foo.c.foo = b;
eq(a, b);  // true



var buffer = new ArrayBuffer(16);
var view = new DataView(buffer, 0);

view.setInt16(1, 42);

var buffer1 = new ArrayBuffer(16);
var view1 = new DataView(buffer1, 0);
view1.setInt16(1, 42);

eq(view, view1);  // true

参考资料:

github.com/mqyqingfeng…

underscorejs.net