在JavaScript开发中,我们经常需要比较两个对象是否相等。然而,JavaScript中的对象比较并非总是一目了然,尤其是当涉及到复杂数据结构和循环引用时。今天,我们就来深入分析一段实现深度比较的经典代码,其中涵盖了从基本类型到正则、日期、Map、Set等复杂类型的全面判断。
我们先来看这段代码的整体结构。代码主要由三个部分组成:`getType`函数用于获取对象的准确类型,`compare`函数用于比较两个对象的类型,而核心的
`areDeeplyEqual`函数则实现了深度比较的逻辑。同时,`TYPE_ENUM`定义了各种数据类型的枚举值,方便代码中的类型判断。
```javascript
function getType(obj) {
return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase()
}
function compare(obj1, obj2) {
const t1 = getType(obj1)
const t2 = getType(obj2)
return {
isSameType: t1 === t2,
types: [t1, t2]
}
}
const TYPE_ENUM = {
ARRAY: 'array',
OBJECT: 'object',
DATE: 'date',
REGEXP: 'regexp',
MAP: 'map',
SET: 'set',
FUNCTION: 'function',
SYMBOL: 'symbol',
BOOLEAN: 'boolean',
NUMBER: 'number',
STRING: 'string',
NULL: 'null',
UNDEFINED: 'undefined'
}
var areDeeplyEqual = function (o1, o2, visited = new WeakMap()) {
// 处理循环引用
if (visited.has(o1) && visited.get(o1) === o2) {
return true
}
const {isSameType, types} = compare(o1, o2)
if (!isSameType) {
return false
}
const [type] = types
//...各种类型比较逻辑
}
```
对于基本类型,如`null`、`undefined`、`boolean`、`number`、`string`、`symbol`,代码直接进行相等比较。但有一个特殊情况,就是
`number`类型中的`NaN`。在JavaScript中,`NaN`与任何值(包括自身)都不相等,然而在深度比较中,我们希望两个`NaN`被视为相等。
```javascript
if ([TYPE_ENUM.NULL, TYPE_ENUM.UNDEFINED, TYPE_ENUM.BOOLEAN,
TYPE_ENUM.NUMBER, TYPE_ENUM.STRING, TYPE_ENUM.SYMBOL].includes(type)) {
// 特殊处理NaN,NaN!== NaN但应视为相等
if (type === TYPE_ENUM.NUMBER && isNaN(o1) && isNaN(o2)) {
return true
}
return o1 === o2
}
```
函数在JavaScript中是一等公民,这里通过比较函数的字符串表示来判断两个函数是否相等。虽然这种方式不能完全精确地判断两个函数是否功能相同,但能满足比较函数引用的基本需求。
```javascript
if (type === TYPE_ENUM.FUNCTION) {
return o1.toString() === o2.toString()
}
```
对于日期类型`Date`,代码通过比较`getTime()`方法返回的值来判断两个日期是否相等。因为`getTime()`返回从1970年1月1日00:00:00
UTC到指定日期的毫秒数,所以只要两个日期的这个时间戳相同,就认为它们相等。
```javascript
if (type === TYPE_ENUM.DATE) {
return o1.getTime() === o2.getTime()
}
```
正则表达式的比较需要考虑三个方面:正则表达式的内容(`source`)、修饰符(`flags`)以及最后匹配位置(`lastIndex`
)。只有当这三个属性都相同时,两个正则表达式才被认为是相等的。
```javascript
if (type === TYPE_ENUM.REGEXP) {
return o1.source === o2.source &&
o1.flags === o2.flags &&
o1.lastIndex === o2.lastIndex
}
```
数组比较首先检查长度是否相等,如果长度不同则直接返回`false`。然后,为了处理循环引用,将当前比较的数组对记录到`visited`
中。最后,遍历数组,递归调用`areDeeplyEqual`来比较每一个元素。
```javascript
if (type === TYPE_ENUM.ARRAY) {
if (o1.length !== o2.length) {
return false
}
// 记录已访问的数组,防止循环引用
visited.set(o1, o2)
for (let i = 0
if (!areDeeplyEqual(o1[i], o2[i], visited)) {
return false
}
}
return true
}
```
`Map`的比较首先检查大小是否相等。接着,将当前比较的`Map`对记录到`visited`中。然后遍历`o1`,检查`o2`是否包含相同的键,并且对应键的值通过递归调用
`areDeeplyEqual`比较是否相等。
```javascript
if (type === TYPE_ENUM.MAP) {
if (o1.size !== o2.size) {
return false
}
visited.set(o1, o2)
// map可以使用for of 进行循环,拿到是键值对的数组项
for (const [key, value] of o1) {
// 判断o2是否有o1中的key 或者判断o1 o2对应的key值是否相等
if (!o2.has(key) || !areDeeplyEqual(value, o2.get(key), visited)) {
return false
}
}
return true
}
```
`Set`的比较同样先检查大小是否相等,记录到`visited`中。由于`Set`没有直接的键值对概念,这里将`Set`转换为数组并排序,然后递归调用
`areDeeplyEqual`比较两个数组,以此来判断两个`Set`是否相等。
```javascript
if (type === TYPE_ENUM.SET) {
if (o1.size !== o2.size) {
return false
}
visited.set(o1, o2)
const arr1 = Array.from(o1).sort()
const arr2 = Array.from(o2).sort()
return areDeeplyEqual(arr1, arr2, visited)
}
```
普通对象的比较首先获取两个对象的键数组,比较键的数量。如果数量不同则返回`false`。然后将当前比较的对象对记录到`visited`中,遍历
`o1`的键,检查`o2`是否包含相同的键,并且对应键的值通过递归调用`areDeeplyEqual`比较是否相等。
```javascript
if (type === TYPE_ENUM.OBJECT) {
const keys1 = Object.keys(o1)
const keys2 = Object.keys(o2)
if (keys1.length !== keys2.length) {
return false
}
// 记录已访问的对象,防止循环引用
visited.set(o1, o2)
for (const key of keys1) {
if (!keys2.includes(key) || !areDeeplyEqual(o1[key], o2[key], visited)) {
return false
}
}
return true
}
```
通过这样全面且细致的比较方式,这段代码能够准确地判断两个对象在深度上是否相等,无论是简单的基本类型还是复杂的嵌套结构,甚至包括对象之间的循环引用。希望通过这次分析,能让大家对JavaScript对象的深度比较有更深入的理解。