JavaScript拷贝详解

前言

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

一、浅拷贝

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

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

  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) 进行浅拷贝:
javascript 复制代码
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) 进行浅拷贝:
javascript 复制代码
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() 方法进行浅拷贝:
javascript 复制代码
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. 使用数组解构进行浅拷贝:
javascript 复制代码
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() 进行浅拷贝:
javascript 复制代码
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))

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

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

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

javascript 复制代码
const cloneObj = _.cloneDeep(obj);

使用第三方库可以更安全、可靠地进行深拷贝操作。

深拷贝示例:

  1. 使用 JSON.parse(JSON.stringify(obj)) 进行深拷贝:
javascript 复制代码
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):
javascript 复制代码
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)) 或第三方库提供的深拷贝方法。在实际应用中,我们需要根据具体情况选择适合的拷贝方式,以确保数据的完整性和安全性。

相关推荐
PAK向日葵29 分钟前
【算法导论】PDD 0817笔试题题解
算法·面试
uzong2 小时前
技术故障复盘模版
后端
GetcharZp2 小时前
基于 Dify + 通义千问的多模态大模型 搭建发票识别 Agent
后端·llm·agent
桦说编程3 小时前
Java 中如何创建不可变类型
java·后端·函数式编程
IT毕设实战小研3 小时前
基于Spring Boot 4s店车辆管理系统 租车管理系统 停车位管理系统 智慧车辆管理系统
java·开发语言·spring boot·后端·spring·毕业设计·课程设计
wyiyiyi3 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
gnip4 小时前
vite和webpack打包结构控制
前端·javascript
阿华的代码王国4 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端
Jimmy4 小时前
AI 代理是什么,其有助于我们实现更智能编程
前端·后端·ai编程
AntBlack5 小时前
不当韭菜V1.1 :增强能力 ,辅助构建自己的交易规则
后端·python·pyqt