JavaScript数据处理之深拷贝

半梦半醒丶 2023-2-22 4,814 2/22

浅拷贝:

​ JavaScript的数据类型分为基本类型和引用类型,引用类型数据在传递时是通过地址传递的。在直接将一个引用类型数据用“=”赋值给一个变量时,其实是将该引用类型数据的地址赋值给了这个变量。例如:

 

let a = {
id: 1,
};

let b = a;

a.id = 2;

console.log(b); // { id: 2 }

 

此时变量a、b均指向同一个地址,访问的是一个相同的对象。当修改a.id时,b.id的值也发生了改变。这种直接复制引用地址的方式叫浅拷贝。

 

 

深拷贝:

​ 深拷贝就是在拷贝一个对象时在内存中开辟了一个新的空间来存储新的相同的对象,新旧对象之间没有联系互不干扰。

let a = {
id: 1,
};

let b = a;

a.id = 2;

console.log(b); // { id: 1 }

 

此时变量a、b指向同不同的地址,访问的是不同的对象。当修改a.id时,b.id的值也发生了改变。

 

 

利用序列化方式JSON.parse()和JSON.stringify()进行深拷贝

​ 如果一个 JavaScript 对象可以被序列化(Object/Array),则可以使用 JSON.stringify() 将该对象转换为 JSON 字符串,然后使用JSON.parse()可以将该字符串转换成一个全新的JavaScript 对象,例如:

 

let a = [
{
id: 1,
age: 18,
},
"此情可待成追忆,只是当时已惘然",
["HTML", "CSS", "JavaScript"],
];

// 序列化深拷贝
let b = JSON.parse(JSON.stringify(a));

a[0].id = 2;
a[1] = "It's hard to make money";
a[2][3] = "VUE";

console.log("a = ", a);
console.log("b = ", b);

// 得到的结果
a = [
{ id: 2, age: 18 },
"It's hard to make money",
[ 'HTML', 'CSS', 'JavaScript', 'VUE' ]
]

b = [
{ id: 1, age: 18 },
'此情可待成追忆,只是当时已惘然',
[ 'HTML', 'CSS', 'JavaScript' ]
]

 

在上面的代码中可以看出:拷贝完成后无论a的值如何发生改变都不会影响到b的值。

 

注意事项:序列化方法有一些局限性。

(1)在非数组的对象中,值为undefined、Symbol和Function类型数据在序列化时均会被忽略,而NaN、Infinity、-Infinity会被转化成null。

 

let a = {
id: 1,
age: 18,
uname: "",
oldName: null,
isNumber: NaN,
s1: Infinity, // 无穷大
sex: undefined,
isSymbol: Symbol("123"),
getMoney: () => {
console.log("It's hard to make money");
return 111;
},
};

// 序列化深拷贝
let b = JSON.parse(JSON.stringify(a));

console.log("b = ", b);

// 结果
b = { id: 1, age: 18, uname: '', oldName: null, isNumber: null, s1: null }

 

(2)在序列化数组时,Infinity、undefined、Symbol和Function类型数据均会被转化成null

 

let a = [
Infinity,
undefined,
null,
Symbol("123"),
() => {
console.log("It's hard to make money");
return 111;
},
];

console.log("a = ", a);
console.log("b = ", b);

// 结果
a = [ Infinity, undefined, null, Symbol(123), [Function (anonymous)] ]
b = [ null, null, null, null, null ]

 

(3)序列化循环嵌套对象时会报错:*TypeError: Converting circular structure to JSON*

 

let a = {};
let b = {};
a.b = b;
b.a = a;

console.log(JSON.parse(JSON.stringify(a)));
// TypeError: Converting circular structure to JSON

 

如果只是简单的数据进行深拷贝可以使用这种序列化的方式,但比较复杂的数据类型这种方式就会存在很多问题。

 

 

递归遍历深拷贝

​ 深拷贝主要使用的是递归的方式进行循环遍历,当数据类型是对象时就继续像内部进行深度遍历,最后达到深拷贝的目的。具体代码实现方式如下:

/**
 * 深拷贝数据(可处理:对象、数组、函数、Date、正则、Set、Map、循环引用)
 * @param target 被拷贝的数据
 * @param hash (无需传值)WeakMap,用于存储循环引用数据的关联性
 */
export function deepClone(target: any, hash = new WeakMap()) {
  if (typeof target !== 'object' || target === null) {
    return target // 不是对象或为null直接返回
  }

  if (hash.get(target)) {
    return hash.get(target) // 已经拷贝过的对象直接返回
  }

  let cloneObj: any
  if (target instanceof Date) {
    cloneObj = new Date(target)
  } else if (target instanceof RegExp) {
    cloneObj = new RegExp(target)
  } else if (target instanceof Set) {
    cloneObj = new Set([...target])
  } else if (target instanceof Map) {
    cloneObj = new Map([...target])
  } else if (typeof target === 'function') {
    cloneObj = target
  } else {
    cloneObj = Array.isArray(target) ? [] : {}
  }

  hash.set(target, cloneObj) // 关联原对象和拷贝对象

  // 处理对象或数组中的属性
  for (let key in target) {
    if (Object.prototype.hasOwnProperty.call(target, key)) {
      if (typeof target[key] === 'object' && target[key] !== null) {
        cloneObj[key] = deepClone(target[key], hash) // 递归拷贝子属性
      } else {
        cloneObj[key] = target[key]
      }
    }
  }

  // 复制原型链属性
  Object.setPrototypeOf(cloneObj, Object.getPrototypeOf(target))

  return cloneObj
}

 

该方法完全实现了对象的深拷贝,包括对象、数组、函数、Date、正则、Set、Map、循环引用等情况。

- THE END -

半梦半醒丶

6月29日17:25

最后修改:2024年6月29日
0

非特殊说明,本博所有文章均为博主原创。