深度剖析JavaScript对象深度比较:从基础到复杂类型

64 阅读4分钟
# 深度剖析JavaScript对象深度比较:从基础到复杂类型

在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; i < o1.length; i++) {
        if (!areDeeplyEqual(o1[i], o2[i], visited)) {
            return false;
        }
    }
    return true;
}
```

### Map比较

`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比较

`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对象的深度比较有更深入的理解。