前言
作为一个刚入行不久的前端小菜鸡,写博客的原因一方面是为了记录一下自己平时踩的坑,给新入坑的小伙伴提供一个前车之鉴。另一方面则是分享一些自己工作、学习的心得,如果有跑偏的地方,希望能得到大佬们的批评指正,以免“误入歧途”。
大佬们如果不想看废话,请直接到总结看代码。
应用场景
众所周知,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
看,只要内部对象的顺序一改变,咱们的方法就又挂掉了。这样看来这个方法无法适用于存在复杂数据类型属性值的对象。
稍微复杂点的解决方案
对于存在嵌套结构的对象,我们就应该引入递归和类型判断系统了。
国际惯例三步走:
- 遍历对象
- 简单数据类型直接用
===
比对,如果false
则直接return
- 复杂数据类型,进入递归
判断数据类型由于方法很多,也不是本篇的重点,所以不展开来讲,只放一个我喜欢用的方法,大家可以随意取用。
// 判断数据类型
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)