前言
JS中,并没有直接提供方法帮助开发者判断两个对象是否相等,但在开发中,却绕不开这种判断,本文实现了一种比较全面的、可扩展的对象对比方法。
基本思路
- 该方法接受两个参数,作为比较的对象,这两个参数可以是任何值(原始类型、引用类型皆可);
- 先判断两个参数是否相等,如果相等,则返回
true;因为无论是原始类型还是引用类型,如果相等,说明要么值相等,要么是同一个引用对象,那么必然是相等的;- 判断两个参数的数据类型是否相同,如果不相同,直接返回
false;- 如果两个参数的数据类型相同,那么判断是否为引用类型,如果不是,返回
false;因为如果不是引用类型,那就是原始类型了,而原始类型在第2步中已经判断过了,所以返回false即可;到这一步,已经将全部原始类型排除,接下去处理,都是引用类型;- 准备一个映射表(
JSON),该映射表记录各种引用类型的对比方法,同时,也方便后续数据类型的对比方法的扩展和修改;- 从映射表中获取当前引用类型相应的对比方法,执行该方法,最后返回该方法执行结果。
话不多说,跟着思路开始撸代码。
前置准备
上面思路中提到的记录各种引用类型的对比方法的映射表,可以先定义出来,不在方法内部定义:
// 策略映射表
const policyMap = {}
还有两个常用的方法:
/** 判断是否为引用类型 */
function isReferenceValue (target) {
// 判断是否为引用类型时,需要先排除 null
return target !== null && typeof target === 'object'
}
/** 获取类型方法 */
const getDataType = Object.prototype.toString
判断函数
根据上面思路写出来的判断函数如下:
/** 接收两个参数 */
function isEqual (target1, target2) {
/** 值 或者 地址比较,如果相同返回 true,主要是处理原始类型的对比
* 此处使用`Object.is`进行判断,而不用`===`,是因为它们对待有符号的零和`NaN`不同。
* 例如,`===` 运算符(也包括 `==` 运算符)将数字 `-0` 和 `+0` 视为相等,而将`NaN` 与 `NaN` 视为不相等;
* 而`Object.is`将 `-0` 和 `+0` 视为不相等,`NaN` 与 `NaN` 视为相等。
*/
if (Object.is(target1, target2)) { return true }
// 数据类型进行比较,数据类型不同,或者都不是引用类型,则返回 false
const typeOfTarget1 = getDataType.call(target1)
if (typeOfTarget1 !== getDataType.call(target2) || !isReferenceValue(target1)) { return false }
// 根据引用类型,获取比较方法
const policy = policyMap[typeOfTarget1]
// 对比的方法接受两个参数,返回方法的比较结果
return policy ? policy(target1, target2) : String(target1) === String(target2)
}
主要流程的代码都已经准备完毕了,那么接下来,自然就是完善映射表,实现各种引用类型的对比方法。
实现引用类型的对比方法
本节只记叙实现的思路和代码,测试数据都放在下一节。
Object
判断思路:
- 获取比较的两个对象的键;
- 判断两个对象的键的数量是否相同,如果不相同返回
false;- 如果键的数量相同,则遍历每一个键的值,逐一进行对比,直到发现不相等的值,返回
false;如果没有发现,则返回true。
/** Object 对比 */
function equalObject (obj1, obj2) {
// 获取两个对象的键
const keys1 = Object.keys(obj1)
const keys2 = Object.keys(obj2)
// 判断键的长度是否相等
// 如果相等,则遍历所有键,直到找出不相等的值,或者遍历完成
return keys1.length === keys2.length && keys1.every(key => isEqual(obj1[key], obj2[key]))
}
// 将方法加入映射表中,键为 Object.prototype.toString.call({}) 的值
policyMap['[object Object]'] = equalObject
Array
判断思路:
- 比较两个数组的长度,如果不一致,返回
false;- 如果长度相同,则遍历两个数组,直到发现不相等的值,返回
false;如果没有发现不相等的值,返回true。
/** Array 对比 */
function equalArray (arr1, arr2) {
return arr1.length === arr2.length && arr1.every((item, index) => isEqual(item, arr2[index]))
}
// 将方法加入映射表中,键为 Object.prototype.toString.call([]) 的值
policyMap['[object Array]'] = equalArray
Date
判断思路:
- 时间对象的判断是否相同,其实就是判断是否为同一个时间点;
- 只需要将两个时间对象,转换为字符串或者数字,就能判断是否为同一个时间点;
- 获取两个时间对象的毫秒数,进行对比即可
/** Date 对比 */
function equalDate (date1, date2) {
return date1.getTime() === date2.getTime()
}
// 将方法加入映射表中,键为 Object.prototype.toString.call(new Date()) 的值
policyMap['[object Date]'] = equalDate
RegExp
判断思路:
- 将两个正则表达式转换为字符串;
- 判断这两个字符串是否相同;
/** RegExp 对比 */
function equalRegExp (reg1, reg2) {
return reg1.toString() === reg2.toString()
}
// 将方法加入映射表中,键为 Object.prototype.toString.call(new RegExp()) 的值
policyMap['[object RegExp]'] = equalRegExp
Map
Map难以处理的地方有两点:
Map其实是有序的,是按插入的顺序排列的;Map不只是值可以是任何类型,键也可以是任何类型,包括原始类型和引用类型;
判断思路:
- 判断两个
Map的大小是否相同,如果不相同,返回false;如果相同,执行以下步骤;- 获取两个
Map的迭代器,逐个遍历相应位置的键值对;- 对于每一个键与值,都进行对比;
/** Map 对比 */
function equalMap (map1, map2) {
// 判断两个 Map 的大小是否校相同,不相同返回 false
if (map1.size !== map2.size) { return false }
// 获取 Map 的迭代器
const iterator1 = map1.entries()
const iterator2 = map2.entries()
// 记录下第一组对比的键值对,是一个数组的形式
let value1 = iterator1.next().value
let value2 = iterator2.next().value
// 通过循环遍历,将 Map 中每一个键、每一个值进行对比
while(value1 && value2) {
for (let i = 0; i < 2; i++) {
// 调用对比,递归,如果有一个不相同,则返回 false,结束循环
if (!isEqual(value1[i], value2[i])) {
return false
}
}
// 获取下一组对比的键值对
value1 = iterator1.next().value
value2 = iterator2.next().value
}
// 遍历完成之后,说明没有不同的键值对,返回 true
return true
}
// 将方法加入映射表中,键为 Object.prototype.toString.call(new Map()) 的值
policyMap['[object Map]'] = equalMap
Set
判断思路:
基本上与Map相同;
区别在于Set迭代器返回的是[value, value] 形式的数组,因为没有Key的存在,为了与Map对象的API形式保持一致,故使得每一个entry的key和value都拥有相同的值,因而最终返回一个[value, value]形式的数组,详细。
所以最终只需对比迭代器返回的数组中第一个值即可。
/** Set 对比 */
function equalSet (set1, set2) {
// 判断两个 Set 的大小是否校相同,不相同返回 false
if (set1.size !== set2.size) { return false }
// 获取 Map 的迭代器
const iterator1 = set1.entries()
const iterator2 = set2.entries()
// 记录下第一组对比的值,是一个数组的形式
let value1 = iterator1.next().value
let value2 = iterator2.next().value
// 通过循环遍历,将 Map 中每一个值进行对比
while (value1 && value2) {
// 只对比第一个值
if (!isEqual(value1[0], value2[0])) { return false }
// 获取下一组对比数据
value1 = iterator1.next().value
value2 = iterator2.next().value
}
// 遍历完成之后,说明没有不同的键值对,返回 true
return true
}
// 将方法加入映射表中,键为 Object.prototype.toString.call(new Set()) 的值
policyMap['[object Set]'] = equalSet
Error
判断思路:
Error对象的相关属性不是很多,本文主要对比错误名称Error.prototype.name、错误描述Error.prototype.message;- 可以调用
Error.prototype.toString()同时将错误名称和错误描述转换为字符串,进行对比即可;- 除了通用的
Error构造函数外,JavaScript还有其它类型的错误构造函数,但是通过Object.prototype.toString.call()获取类型,最终都是[object Error],所以不必特殊处理;
/** Error 对比 */
function equalError (err1, err2) {
return err1.toString() === err2.toString()
}
// 将方法加入映射表中,键为 Object.prototype.toString.call(new Error()) 的值
policyMap['[object Error]'] = equalError
最终对比方法映射表
// 映射表
const policyMap = {
'[object Object]': equalObject,
'[object Array]': equalArray,
'[object Date]': equalDate,
'[object RegExp]': equalRegExp,
'[object Map]': equalMap,
'[object Set]': equalSet,
'[object Error]': equalError
}
如果还需要添加其他数据类型的对比方法,直接在映射表中添加即可。
测试
测试数据如下:
const sym = Symbol('Sym')
// 测试数据 a
const mapA = new Map()
mapA.set({ b: 2 }, true)
mapA.set(false, { a: 1 })
mapA.set('key', [ 'value' ])
const setA = new Set()
setA.add(NaN)
setA.add('string')
setA.add([1, 2, 3, { c: 3 }])
setA.add(false)
const errorA = new Error('test error !')
errorA.name = 'Test'
const a = {
num: 1,
nan: NaN,
str: 'abc',
bool: true,
arr: [1, 2, 3, { a: 1 }, setA, errorA],
date: new Date(),
obj: {
bool: false,
arr: [
new Date(),
'abc',
'123',
{ reg: /^(,?[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+\.?)$/g }
],
map: mapA,
sym
},
reg: /\./g,
nul: null,
undef: undefined
}
// 测试数据 b
const mapB = new Map()
mapB.set({ b: 2 }, true)
mapB.set(false, { a: 1 })
mapB.set('key', [ 'value' ])
const setB = new Set()
setB.add(NaN)
setB.add('string')
setB.add([1, 2, 3, { c: 3 }])
setB.add(false)
const errorB = new Error('test error !')
errorB.name = 'Test'
const b = {
num: 1,
nan: NaN,
str: 'abc',
bool: true,
arr: [1, 2, 3, { a: 1 }, setB, errorB],
date: new Date(),
obj: {
bool: false,
arr: [
new Date(),
'abc',
'123',
{ reg: /^(,?[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+\.?)$/g }
],
map: mapB,
sym
},
reg: /\./g,
nul: null,
undef: undefined
}
console.log('isEqual:', isEqual(a, b))
// true
可以更改任意数据来测试false的情况,不再一一列举。
最终代码
请查看script部分 !
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。