likes
comments
collection
share

JavaScript拷贝详解

作者站长头像
站长
· 阅读数 13

前言

拷贝是在编程中常见的操作,它用于创建一个对象或数组的副本,以便在不影响原始对象的情况下进行操作和修改。一般情况下,拷贝通常针对引用类型进行操作,包括对象和数组。

一、浅拷贝

浅拷贝是指创建一个新对象或数组,并将原始对象或数组的引用复制给新对象或数组。这意味着新对象和原始对象仍然共享相同的内部数据。当对原始对象进行修改时,新对象也会受到影响。

以下是几种常见的浅拷贝方法:

  1. Object.create(obj):使用 Object.create() 方法可以创建一个新对象,新对象的原型是指定的对象 obj
  2. Object.assign({}, obj):通过 Object.assign() 方法可以将一个或多个源对象的属性复制到目标对象中,并返回目标对象。
  3. [].concat(arr):使用数组的 concat() 方法可以将一个或多个数组合并为一个新数组。
  4. 数组解构:通过解构赋值语法可以快速创建一个数组的浅拷贝。
  5. arr.slice():数组的 slice() 方法返回一个新数组,包含原数组的浅拷贝。

需要注意的是,浅拷贝只能处理一层的数据,如果原始对象或数组中包含嵌套的对象或数组,则浅拷贝只会复制它们的引用。这意味着对于嵌套对象或数组的修改会影响到新对象或数组。

浅拷贝示例:

1. 使用 Object.create(obj) 进行浅拷贝:
const obj = { a: [1, 2], b: { c: 3 } };
const newObj = Object.create(obj);

console.log(newObj); // 输出:{}
console.log(newObj.a); // 输出:[1, 2]
console.log(newObj.b); // 输出:{ c: 3 }

// 修改原始对象
obj.a.push(3);
obj.b.c = 4;
console.log(obj);      // 输出:{ a: [ 1, 2, 3 ], b: { c: 4 } }
console.log(newObj.a); // 输出:[1, 2, 3],新对象也受到影响
console.log(newObj.b); // 输出:{ c: 4 },新对象也受到影响

我们用 Object.create(obj) 方法创建了一个新对象 newObj,并将原始对象 obj 的引用复制给了它。当我们修改原始对象 obj 的值时,浅拷贝对象 newObj 的值也随之发生了变化。这是因为浅拷贝只是复制了原始对象的引用,而没有创建新的内存空间来存储数据。

2. 使用 Object.assign({}, obj) 进行浅拷贝:
const obj = { a: [1, 2], b: { c: 3 } };
const newObj = Object.assign({}, obj);

console.log(newObj); // 输出:{ a: [1, 2], b: { c: 3 } }

// 修改原始对象
obj.a.push(3);
obj.b.c = 4;
console.log(obj);      // 输出:{ a: [ 1, 2, 3 ], b: { c: 4 } }
console.log(newObj);   // 输出:{ a: [ 1, 2, 3 ], b: { c: 4 } }
console.log(newObj.a); // 输出:[1, 2, 3],新对象也受到影响
console.log(newObj.b); // 输出:{ c: 4 },新对象也受到影响

我们使 Object.assign() 方法将一个空对象和原始对象 obj 的属性进行合并,并返回一个新的对象 newObj。当我们修改原始对象 obj 的值时,浅拷贝对象 newObj 的值并会发生变化。这是因为浅拷贝复制了原始对象的一层属性,也复制嵌套对象的引用。

3. 使用数组的 concat() 方法进行浅拷贝:
const arr = [[1, 2], { a: 3 }];
const newArr = [].concat(arr);

console.log(newArr); // 输出:[[1, 2], { a: 3 }]

// 修改原始数组
arr[0].push(3);
arr[1].a = 4;
console.log(arr);    // 输出:[[1, 2, 3], { a: 4 } ]
console.log(newArr); // 输出:[[1, 2, 3], { a: 4 }],新数组也受到影响

我们用数组的 concat() 方法创建了一个新数组 newArr,其中包含原始数组 arr 的所有元素。当我们修改原始数组 arr 的值时,浅拷贝数组 newArr 的值并会发生变化。这是因为浅拷贝复制数组内部引用类型的引用。

4. 使用数组解构进行浅拷贝:
const arr = [[1, 2], { a: 3 }];
const [...newArr] = arr;

console.log(newArr); // 输出:[[1, 2], { a: 3 }]

// 修改原始数组
arr[0].push(3);
arr[1].a = 4;
console.log(arr);    // 输出:[[1, 2, 3], { a: 4 }]
console.log(newArr); // 输出:[[1, 2, 3], { a: 4 }],新数组也受到影响

我们用数组解构语法 [...arr] 创建了一个新数组 newArr,其中包含原始数组 arr 的所有元素。当我们修改原始数组 arr 的值时,浅拷贝数组 newArr 的值并不会发生变化。这是因为浅拷贝复制数组内部引用类型的引用。

5. 使用 arr.toReversed().reverse() 进行浅拷贝:
const arr = [[1, 2], { a: 3 }];
const newArr = arr.toReversed().reverse();

console.log(newArr); // 输出:[{ a: 3 }, [1, 2]]
// 修改原始数组
arr[0].push(3);
arr[1].a = 4;

console.log(newArr); // 输出:[{ a: 4 }, [1, 2, 3]],新数组也受到影响

toReversed()是新增的方法,需要更高版本的node才能使用,所以使用时可能会报错。

二、深拷贝

深拷贝是指创建一个与原始对象完全独立的新对象或数组,新对象拥有与原始对象相同的值,但内部数据不共享。这样,在对原始对象进行修改时,不会影响到新对象。

1. JSON.parse(JSON.stringify(obj))

在 js 中,常用的深拷贝方法是使用 JSON.parse(JSON.stringify(obj))。该方法通过将对象序列化为 JSON 字符串,然后再解析成新的对象来实现深拷贝。但是需要注意的是,这种方法也存在一些缺陷:

  1. 无法处理特殊的数据类型:undefinedfunctionSymbol 会在序列化过程中丢失。
  2. 无法处理循环引用:如果原始对象中存在循环引用(即对象内部存在相互引用的情况),那么深拷贝会陷入无限循环,在处理大型对象时可能导致堆栈溢出。
2. cloneDeep()

为了解决深拷贝的问题,我们可以借助第三方库如 Lodash 的 cloneDeep() 方法,它能够处理上述的特殊数据类型和循环引用。

const cloneObj = _.cloneDeep(obj);

深拷贝示例:

  1. 使用 JSON.parse(JSON.stringify(obj)) 进行深拷贝:
const obj = { name: 'Charlie', age: 35 };
const newObj = JSON.parse(JSON.stringify(obj));

console.log(newObj); // 输出: { name: 'Charlie', age: 35 }

// 修改原始对象
obj.age = 40;

console.log(obj); // 输出: { name: 'Charlie', age: 40 }
console.log(newObj); // 输出: { name: 'Charlie', age: 35 }

我们用 JSON.parse(JSON.stringify(obj)) 方法创建了一个新的对象 newObj,并将原始对象 obj 的所有属性值复制给了它。当我们修改原始对象 obj 的值时,深拷贝对象 newObj 的值并不会发生变化。这是因为深拷贝创建了新的内存空间来存储数据,并且递归地复制了所有嵌套的对象和数组。

需要注意的是,JSON.parse(JSON.stringify(obj)) 方法存在一些限制。例如,它无法处理特殊的数据类型(如 undefinedfunctionSymbol),并且在处理循环引用时可能会导致无限循环的问题。

  1. 使用第三方库 Lodash 的 cloneDeep() 进行深拷贝(需要先安装 Lodash):
const _ = require('lodash');

const obj = { name: 'David', age: 40 };
const newObj = _.cloneDeep(obj);

console.log(newObj); // 输出: { name: 'David', age: 40 }

// 修改原始对象
obj.age = 45;

console.log(obj); // 输出: { name: 'David', age: 45 }
console.log(newObj); // 输出: { name: 'David', age: 40 }

我们用第三方库 Lodash 的 cloneDeep() 方法创建了一个新的对象 newObj,并将原始对象 obj 的所有属性值复制给了它。当我们修改原始对象 obj 的值时,深拷贝对象 newObj 的值并不会发生变化。这是因为 cloneDeep() 方法能够处理特殊的数据类型和循环引用,并且递归地复制了所有嵌套的对象和数组。

三、总结

浅拷贝和深拷贝都是在编程中常见的操作,用于创建对象或数组的副本。浅拷贝只复制引用,而深拷贝创建独立的副本。对于浅拷贝,我们可以使用 Object.assign()concat() 或解构赋值等方法。对于深拷贝,可以使用 JSON.parse(JSON.stringify(obj)) 或第三方库提供的深拷贝方法

转载自:https://juejin.cn/post/7307653137584930867
评论
请登录