js判断两个对象是否相等?整个小方法搞定

前言

作为一个刚入行不久的前端小菜鸡,写博客的原因一方面是为了记录一下自己平时踩的坑,给新入坑的小伙伴提供一个前车之鉴。另一方面则是分享一些自己工作、学习的心得,如果有跑偏的地方,希望能得到大佬们的批评指正,以免“误入歧途”。

大佬们如果不想看废话,请直接到总结看代码。

应用场景

众所周知,js中的Object、Array、Function等复杂数据类型,是无法直接用=====操作符进行比对的。

  • Object对比
const tar = {
  name:'张三',
  age:12,
  address:'上海市浦东新区'
}
const _tar = {
  name:'张三',
  age:12,
  address:'上海市浦东新区'
}
console.log(tar == _tar);  // false
console.log(tar === _tar); // false

  • Array对比
console.log([1,2,3] === [1,2,3]); // false
console.log([1,2,3] === [1,2,3]); // false

即使我们人眼看上去,这两个对象和数组都一毛一样,但是js还是认为他们两个不相等。这是为啥子呢?

太复杂的咱也讲不出来,简单说一下原理

js的数据类型分为简单数据类型和复杂数据类型。简单数据类型包括:Number、String、Boolean、 undifined、Null等,复杂数据类型包括:Object、Array、function等。

复杂数据类型都会存储在堆内存中,简单数据类型则是存储在栈内存里。我们在定义一个复杂数据类型的时候,会先在堆内存中开辟空间,把数据存进去后,再把内存地址返回给我们所定义的变量。这样看来,我们所进行的对比操作,原来不是值的对比,而是内存地址的对比。


图片来自网络

举个栗子
const tar = {
  name:'张三',
  age:12,
  address:'上海市浦东新区'
}
const _tar = {
  name:'张三',
  age:12,
  address:'上海市浦东新区'
}
// tar和_tar 其实指向的是对象的内存地址,

由于每次创建对象都会新开辟新空间,与之对应的也会产生新内存地址,
所以即使两个对象的属性与属性值都一模一样,地址也是不同的。
其他复杂数据类型也是同样的原理。

搞明白为什么,下面开始上干货。

一个简单的解决方案

既然复杂数据类型无法通过=====操作符判断,我们如果把它转换成字符串呢?
恰好,js提供了一个方法JSON.stringify,可以将Object和Array转换成JSON字符串。

const tar1 = {
  name:'张三',
  age:12,
  address:'上海市浦东新区'
}
const tar2 = {
  name:'张三',
  age:12,
  address:'上海市浦东新区'
}
const tar3 = {
  name:'张三',
  address:'上海市浦东新区',
  age:13,
}

const _tar1 = JSON.stringify(tar1) 
// {"name":"张三","age":12,"address":"上海市浦东新区"}

const _tar2 = JSON.stringify(tar2)
// {"name":"张三","age":12,"address":"上海市浦东新区"}

const _tar3 = JSON.stringify(tar3)
// {"name":"张三","address":"上海市浦东新区","age":13}

_tar1 === _tar2 //true
_tar1 === _tar2 //false

JSON.stringify([1,2,3]) === JSON.stringify([1,2,3])  //true
JSON.stringify([1,2,3]) === JSON.stringify([1,2,4])  //true

看来很顺利,假如顺序变换一下呢?

const tar1 = {
  name:'张三',
  age:12,
  address:'上海市浦东新区'
}

const tar2 = {
  name:'张三',
  address:'上海市浦东新区',
  age:12,
}

const _tar1 = JSON.stringify(tar1) 
// {"name":"张三","age":12,"address":"上海市浦东新区"}

const _tar2 = JSON.stringify(tar2)
// {"name":"张三","address":"上海市浦东新区","age":12}

_tar1===_tar2  // false

JSON.stringify([1,2,3]) === JSON.stringify([1,3,2]) // false

看来属性位置的变化,会影响JSON.stringify的转换结果,导致我们的判断出现了失误。

讲道理,数组内元素顺序变化,也应该理解为数组已经发生了改变,毕竟元素下标变了嘛。所以针对于数组的判断还是没有翻车滴!

所以我们现在碰到的第一个问题就是:如何保证两个对象的属性排列顺序是一致的呢?
实现的方法有很多,整一个我认为最简单的办法吧。

利用Object.assign的复制特性,将对比对象的属性值复制到源对象的属性上,这样生成的新对象就可以保证是按照源对象的顺序排列的。

const tar1 = {
  name:'张三',
  age:12,
  address:'上海市浦东新区'
}

const tar2 = {
  name:'张三',
  address:'上海市浦东新区',
  age:13,
}

JSON.stringify(Object.assign(tar1,tar1)
// { name: '张三', age: 13, address: '上海市浦东新区' }

// 展开操作符也可以达到一样的效果
JSON.stringify(Object.assign({...tar1,...tar1})
// { name: '张三', age: 13, address: '上海市浦东新区' }

这样我们就可以定义一个方法来使用啦!

const tar1 = {
  name:'张三',
  age:12,
  address:'上海市浦东新区'
}
const tar2 = {
  name:'张三',
  address:'上海市浦东新区',
  age:12,
}
const tar3 = {
  name:'张三',
  address:'上海市浦东新区',
  age:13,
}
function isObjectChanged(source, comparison) {
  const _source = JSON.stringify(source)
  const _comparison = JSON.stringify({...source,...comparison})
  return _source !== _comparison
}

isObjectChanged(tar1,tar2)  //false
isObjectChanged(tar1,tar3)  //true

这样是不是就完美的解决了我们的问题呢?

对于简单结构的对象,这个方法确实已经能满足需求了。假如对象中还嵌套对象呢?

const tar1 = {
  name:'张三',
  age:12,
  address:'上海市浦东新区',
  children:{
    age:12,
    name:'李四',
    address:'上海市黄浦区',
  }
}
const tar2 = {
  name:'张三',
  age:12,
  address:'上海市浦东新区',
  children:{
    name:'李四',
    age:12,
    address:'上海市黄浦区',
  }
}
const tar3 = {
  age:12,
  name:'张三',
  address:'上海市浦东新区',
  children:{
    age:12,
    name:'李四',
    address:'上海市黄浦区',
  }
}


isObjectChanged(tar1,tar2)  //true
isObjectChanged(tar1,tar3)  //false

看,只要内部对象的顺序一改变,咱们的方法就又挂掉了。这样看来这个方法无法适用于存在复杂数据类型属性值的对象。

稍微复杂点的解决方案

对于存在嵌套结构的对象,我们就应该引入递归和类型判断系统了。

国际惯例三步走:

  1. 遍历对象
  2. 简单数据类型直接用===比对,如果false则直接return
  3. 复杂数据类型,进入递归

判断数据类型由于方法很多,也不是本篇的重点,所以不展开来讲,只放一个我喜欢用的方法,大家可以随意取用。

// 判断数据类型
function getDataType(data) {
  const temp = Object.prototype.toString.call(data);
  const type = temp.match(/\b\w+\b/g);
  return (type.length < 2) ? 'Undefined' : type[1];
}

有了类型判断方法后,我们就可以开始写新方法啦!

function isObjectChanged(source, comparison) {
  let isChanged = false
  for (let key in source) {
    // 由于Object和Array都属于我们要特殊判断的数据类型,所以要提前做一下判断
    if (getDataType(source[key]) === 'Object' || getDataType(source[key]) === 'Array') {
      // 由于isChanged默认值就是false,所以我们只在isObjectChanged返回true的时候改变状态
      if (isObjectChanged(source[key], comparison[key])) {
        isChanged = true
      }
    } else if (source[key] !== comparison[key]) {
      isChanged = true
    }
  }
  return isChanged
}

测试一下

const tar1 = {
  name: '张三',
  age: 12,
  address: '上海市浦东新区',
  array: [1, 2, 3],
  children: {
    age: 12,
    name: '李四',
    address: '上海市黄浦区',
  }
}
const tar2 = {
  name: '张三',
  age: 12,
  address: '上海市浦东新区',
  array: [1, 2, 3, 4],
  children: {
    name: '李四',
    age: 12,
    address: '上海市黄浦区',
  }
}
const tar3 = {
  age: 12,
  name: '张三',
  address: '上海市浦东新区',
  array: [1, 2, 3],
  children: {
    age: 13,
    name: '李四',
    address: '上海市黄浦区',
  }
}
const tar4 = {
  age: 12,
  name: '张三',
  address: '上海市浦东新区',
  array: [2, 3, 1],
  children: {
    name: '李四',
    age: 12,
    address: '上海市黄浦区',
  }
}
isObjectChanged(tar1, tar2) //false
isObjectChanged(tar1, tar3) //true
isObjectChanged(tar1, tar4) //true

目前看起来一切都如我们所愿,即使带上Array我们都不怕。
真的没问题了吗?我们加大难度再来测试一下。

const tar1 = {
  name: '张三',
  age: 12,
  address: '上海市浦东新区',
  array: [1, 2, 3],
  children: {
    age: 12,
    name: '李四',
    address: '上海市黄浦区',
  }
}
const tar2 = {
  name: '张三',
  age: 12,
  address: '上海市浦东新区',
  array: [1, 2, 3, 4],
  children: {
    age: 12,
    name: '李四',
    address: '上海市黄浦区',
    array: [1, 2, 3, 4],
  }
}
isObjectChanged(tar1, tar2) // false

什么情况?这么明显的区别却在我们的方法里直接被忽视了?这是为啥呢?

原来我们在判断的时候,只根据源数据的属性来进行判断,如果是对比数据包含了源数据,且对比数据与源数据重合的部分都没有发生改变,那我们的方法就好像被别人遮住了一部分视野,再怎么对比都对比不出来啦。

仔细审视一下我们的代码,其中还有一个缺陷: 我们这个方法的目的就是为了判断是否存在变化,假如存在变化就可以立即返回结果,而不需要再傻傻的把循环跑完。

那么我们就继续优化吧!

function isObjectChanged(source, comparison) {

 
  // 由于'Object','Array'都属于可遍历的数据类型,所以我们提前定义好判断方法,方便调用
  const iterable = (data) => ['Object', 'Array'].includes(getDataType(data));

  // 如果源数据不是可遍历数据,直接抛错,主要用于判断首次传入的值是否符合判断判断标准。
  if (!iterable(source)) {
    throw new Error(`source should be a Object or Array , but got ${getDataType(source)}`);
  }

  // 如果数据类型不一致,说明数据已经发生变化,可以直接return结果
  if (getDataType(source) !== getDataType(comparison)) {
    return true;
  }

  // 提取源数据的所有属性名
  const sourceKeys = Object.keys(source);

  // 将对比数据合并到源数据,并提取所有属性名。
  // 在这里进行对象合并,首先是要保证 对比数据>=源数据,好处一:后边遍历的遍历过程就不用做缺省判断了。
  const comparisonKeys = Object.keys({...source, ...comparison});

  // 好处二:如果属性数量不一致说明数据必然发生了变化,可以直接return结果
  if (sourceKeys.length !== comparisonKeys.length) {
    return true;
  }

  // 这里遍历使用some,some的特性一旦找到符合条件的值,则会立即return,不会进行无意义的遍历。完美符合我们当前的需求

  return comparisonKeys.some(key => {
    // 如果源数据属于可遍历数据类型,则递归调用
    if (iterable(source[key])) {
      return isObjectChanged(source[key], comparison[key]);
    } else {
      return source[key] !== comparison[key];
    }
  });
}

嗯~~ 一看代码量就知道很稳,直接上大招测试!

const tar1 = {
  name: '张三',
  age: 12,
  address: '上海市浦东新区',
  array: [1, 2, 3],
  children: {
    age: 12,
    name: '李四',
    address: '上海市黄浦区',
  }
}
const tar2 = {
  name: '张三',
  age: 12,
  array: [1, 2, 3],
  address: '上海市浦东新区',
  children: {
    age: 12,
    address: '上海市黄浦区',
    name: '李四',
  }
}
const tar3 = {
  name: '张三',
  age: 12,
  array: [1, 3, 2],
  address: '上海市浦东新区',
  children: {
    age: 12,
    address: '上海市黄浦区',
    name: '李四',
  }
}
const tar4 = {
  name: '张三',
  sex: '女',
  address: '上海市浦东新区',
  array: [1, 2, 3, 4],
  children: {
    age: 12,
    name: '李四',
    address: '上海市黄浦区',
    array: [1, 2, 3, 4],
  }
}
const tar5 = {
  name: '张三',
  age: 12,
  address: '上海市浦东新区',
  array: [1, 2, 3, 4],
  children: {
    age: 12,
    name: '李四',
    address: '上海市黄浦区',
    array: [1, 2, 3, 4],
  }
}
isObjectChanged(tar1, tar2)  //false
isObjectChanged(tar1, tar3)  //true
isObjectChanged(tar1, tar4)  //true
isObjectChanged(tar1, tar5)  //true

完美~ 所有变化都被我们的方法给判断出来了!

总结

我这个方法并不是最好的实现方式,在这里也是希望能够抛砖引玉,有什么问题希望各位大牛批评指正,第一次写,可能有点啰嗦,希望大家见谅。

下面直接帖代码

简单方法(针对于无嵌套情况)
function isObjectChanged(source, comparison) {
  const _source = JSON.stringify(source)
  const _comparison = JSON.stringify({...source,...comparison})
  return _source !== _comparison
}
almost最佳方案(无惧挑战,目前还没发现判断失误的情况)
// 判断数据类型
function getDataType(data) {
  const temp = Object.prototype.toString.call(data);
  const type = temp.match(/\b\w+\b/g);
  return (type.length < 2) ? 'Undefined' : type[1];
}

// 判断两个对象是否相等
function isObjectChanged(source, comparison) {
  const iterable = (data) => ['Object', 'Array'].includes(getDataType(data));
  if (!iterable(source)) {
    throw new Error(`source should be a Object or Array , but got ${getDataType(source)}`);
  }

  if (getDataType(source) !== getDataType(comparison)) {
    return true;
  }

  const sourceKeys = Object.keys(source);

  const comparisonKeys = Object.keys({...source, ...comparison});

  if (sourceKeys.length !== comparisonKeys.length) {
    return true;
  }

  return comparisonKeys.some(key => {
    if (iterable(source[key])) {
      return isObjectChanged(source[key], comparison[key]);
    } else {
      return source[key] !== comparison[key];
    }
  });
}

如果大家的项目中允许改动原型链,甚至可以把这个方法挂载到Object原型链中,方便调用。

function isObjectChanged() {
// 如果嫌麻烦可以直接把判断数据类型的方法放入函数内部,
  function getDataType(data) {
    const temp = Object.prototype.toString.call(data);
    const type = temp.match(/\b\w+\b/g);
    return (type.length < 2) ? 'Undefined' : type[1];
  }
  // 下面放入源代码
  ...
}
// 挂载原型链上
Object.prototype.isObjectChanged = isObjectChanged

// 在项目各处都可以随时调用
Object.isObjectChanged(tar1, tar2)

不过现在一般不提倡修改内置对象的原型链,特别eslint比较规范的项目,一般都会配置no-extend-native,这种情况建议大家新增一个公共类,把常用Object对象配置进去,作为公共工具来使用,这样调用起来也非常方便。

class ObjectUtils{
  getDataType(data) {
    const temp = Object.prototype.toString.call(data);
    const type = temp.match(/\b\w+\b/g);
    return (type.length < 2) ? 'Undefined' : type[1];
  }
  iterable(data){
    return ['Object', 'Array'].includes(this.getDataType(data));
  }
  isObjectChangedSimple(source, comparison){
    const _source = JSON.stringify(source)
    const _comparison = JSON.stringify({...source,...comparison})
    return _source !== _comparison
  }
  isObjectChanged(source, comparison) {
    if (!this.iterable(source)) {
      throw new Error(`source should be a Object or Array , but got ${this.getDataType(source)}`);
    }
    if (this.getDataType(source) !== this.getDataType(comparison)) {
      return true;
    }
    const sourceKeys = Object.keys(source);
    const comparisonKeys = Object.keys({...source, ...comparison});
    if (sourceKeys.length !== comparisonKeys.length) {
      return true;
    }
    return comparisonKeys.some(key => {
      if (this.iterable(source[key])) {
        return this.isObjectChanged(source[key], comparison[key]);
      } else {
        return source[key] !== comparison[key];
      }
    });
  }
}
ObjectUtils.isObjectChanged(tar1, tar2)
ObjectUtils.isObjectChangedSimple(tar1, tar2)
  • 50
    点赞
  • 89
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值