关于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 从零手写一个深拷贝(进阶篇)》。
在浏览器中, 可以使用 structuredClone()
方法。这是在浏览器环境中使用的一种深拷贝方法,它可以复制包括函数在内的大多数 JavaScript 数据类型。这个方法通常用于复制可传输的对象,比如在 Web Workers、IndexedDB、postMessage 等场景中。
使用方法 :
structuredClone()
方法是作为Window
对象的一个方法存在的,因此在浏览器中可以直接调用。例如:const clonedObject = window.structuredClone(obj)
支持的数据类型 :
structuredClone()
方法可以复制大多数 JavaScript 数据类型,包括对象、数组、字符串、数字、布尔值、日期对象、正则表达式、Map、Set 等。它还可以复制函数,但是不会复制函数的闭包。不支持的数据类型 :
structuredClone()
方法无法复制一些特殊的对象,比如 DOM 元素、Error 对象、WeakMap、WeakSet、Symbol 等。注意事项 :
structuredClone()
方法是一个异步操作,因为它可能需要复制大量数据。- 由于它是在浏览器环境中使用的,因此在 Node.js 等非浏览器环境中无法直接使用。
示例 :
const obj = { name: 'Alice', age: 30, hobbies: ['reading', 'painting'], } const clonedObj = window.structuredClone(obj) console.log(clonedObj)