关于JS对象的二三事

Huy大约 4 分钟javascriptjavascript

关于 JS 对象的二三事

记录一些 JavaScript 实用的小技巧

  • 对象的比较

    由于对象不是常量,所以比较俩个对象是否相同不能用 === 全等或是 == 比较符进行比较。我们很快可以想到用 JSON.stringigy() 函数进行比较。

    let a = { name: 'Dionysia', age: 29 }
    let b = { name: 'Dionysia', age: 29 }
    
    console.log(JSON.stringify(a) === JSON.stringify(b)) // true
    

    当然,这里依然有局限性,就是键值的顺序问题,并且 JSON并不能代表所有的类型,它不能识别 undefined

    let a = { name: 'Dionysia'};
    let b = { name: 'Dionysia', age: undefined};
    
    console.log(JSON.stringify(a) === JSON.stringify(b)); //true
    

    为此,我们的目标比较俩个对象是否相等的要素有:键值对属性一一对应(属性长度)、是否存在嵌套对象?以下是一种较为朴素的解法:

    function deepEqual(objA, objB) {
      // 俩对象指向同一片内存
      if (objA === objB) return true
      // 判断是否为对象, 若不为对象且不指向同一片内存,则返回 false
      if (
        typeof objA === 'object' &&
        objA !== null &&
        typeof objB === 'object' &&
        objB !== null
      ) {
        // 两者均为对象, 开始缩小比较范围
        if (Object.keys(objA).length !== Object.keys(objB).length) {
          return false
        } else {
          // 长度满足要求, 进行深层次比较
          for (let item in objA) {
            // 对象枚举遍历, 检查是否有对应属性
            if (objB.hasOwnProperty(item)) {
              // 迭代遍历属性 防止其为对象
              if (!deepEqual(objA[item], objB[item])) return false
            } else {
              return false
            }
          }
          // 都通过了, 返回 true
          return true
        }
      } else {
        return false
      }
    }
    

    但是这依旧有问题,为此较好的处理边界的方式是 Lodash 库里的 _.isEqual()(或者是 Underscore库里的 _.isEqual())。

手写深拷贝

既然谈到了对象的深度比较,那也有深拷贝,简单的就是利用 JSON 两次转化了:

const A = {
  a: 1,
  b: 2,
  c: [4, 5, 6],
}
const B = JSON.parse(JSON.stringify(A)) // 转化
B.c[1] = 99
console.log(A) // { a: 1, b: 2, c: [ 4, 5, 6 ] }
console.log(B) // { a: 1, b: 2, c: [ 4, 99, 6 ] }

接下来手写一个兼容数组 + 递归调用的深拷贝:

function deepClone(target) {
  let result
  // 判断是否为非 null 型 Object 类型
  if (target !== null && typeof target === 'object') {
    // 判断是否为数组
    result = Array.isArray(target) ? [] : {}

    // 递归遍历
    for (let item in target) {
      result[item] = deepClone(target[item])
    }
  } else {
    // 不为 object 为常量直接返回
    return target
  }
  return result
}

// 测试
const A = {
  a: 1,
  b: 2,
  c: [4, 5, 6],
}
const B = deepClone(A) // 转化
B.c[1] = 99
console.log(A) // { a: 1, b: 2, c: [ 4, 5, 6 ] }
console.log(B) // { a: 1, b: 2, c: [ 4, 99, 6 ] }

上面的深拷贝解决了常见的拷贝问题,但还不够,属性中可能存在自引用,从而导致循环引用的问题。

// 循环引用
const A = {
  a: 1,
  b: A, // 此处自引用
}

那如何解决呢?很简单,在进行深拷贝之前,再做一层拦截,将对象存储到 Map (ES6 中的新语法)中即可。解决循环引用问题,

function deepClone(target, map = new Map()) {
  // 此处 new Map 会在全程起作用, 递归调用时会将初始 map 传入保证同步
  let result
  if (target !== null && typeof target === 'object') {
    // 类型判断
    result = Array.isArray(target) ? [] : {}

    // 循环守卫
    if (map.has(target)) return map.get(target)
    map.set(target, result)

    for (let item in target) {
      result[item] = deepClone(target[item], map)
    }
  } else {
    return target
  }
  return result
}

// 测试
const A = {
  a: 1,
  b: 2,
  c: [4, 5, 6],
}
A.d = A // 自引用
const B = deepClone(A) // 转化
B.c[1] = 99
console.log(A) // { a: 1, b: 2, c: [ 4, 5, 6 ] }
console.log(B) // { a: 1, b: 2, c: [ 4, 99, 6 ] }

当然,深拷贝不止于此,还有各种函数、正则等深拷贝。可以看该文《JS 从零手写一个深拷贝(进阶篇)》open in new window

在浏览器中, 可以使用 structuredClone() 方法。这是在浏览器环境中使用的一种深拷贝方法,它可以复制包括函数在内的大多数 JavaScript 数据类型。这个方法通常用于复制可传输的对象,比如在 Web Workers、IndexedDB、postMessage 等场景中。

1. **使用方法**:`structuredClone()` 方法是作为`Window`对象的一个方法存在的,因此在浏览器中可以直接调用。例如:

```javascript
const clonedObject = window.structuredClone(obj)
```

2. **支持的数据类型**:`structuredClone()` 方法可以复制大多数 JavaScript 数据类型,包括对象、数组、字符串、数字、布尔值、日期对象、正则表达式、Map、Set 等。它还可以复制函数,但是不会复制函数的闭包。

3. **不支持的数据类型**:`structuredClone()` 方法无法复制一些特殊的对象,比如 DOM 元素、Error 对象、WeakMap、WeakSet、Symbol 等。

4. **注意事项**:

   - `structuredClone()` 方法是一个异步操作,因为它可能需要复制大量数据。
   - 由于它是在浏览器环境中使用的,因此在 Node.js 等非浏览器环境中无法直接使用。

5. **示例**:

```javascript
const obj = {
  name: 'Alice',
  age: 30,
  hobbies: ['reading', 'painting'],
}

const clonedObj = window.structuredClone(obj)
console.log(clonedObj)
```
Loading...