在JavaScript中,对象和数组是最常用的引用类型,用于存储复杂的数据结构。当我们在函数间传递或在不同作用域间共享这些数据时,常常需要考虑数据的独立性——即如何确保操作一个拷贝的副本不会影响到原始数据。这便是拷贝技术的用武之地。我们将从理解基本概念入手,然后逐步深入到具体的实现方式及它们之间的区别。
原始类型与引用类型
在开始之前,了解JavaScript中的两种主要数据类型至关重要,在本篇文章之前,我已经通过一篇文章去详细的讲解这两种数据类型了 # 很难吗?十分钟over数据类型、判断和转换 。如果有点忘记了的大佬可以再去看看,不过我们在升深入探讨拷贝之前,再来简单的介绍一些数据类型的存储方式:
- 原始类型(如字符串、数字、布尔值)可以直接存储在变量中(栈),每个变量都有自己的值。
- 引用类型(如对象、数组)要存储在内存中的某个位置(堆),变量保存的是指向该位置的引用地址。这意味着两个变量可以指向同一块内存,从而共享数据。
深拷贝与浅拷贝的区别
-
拷贝:从一个现有的数据实例生成一个新的实例,使得新实例具有与原实例相同的数据状态,也就是我们经常用的Copy或者说复制,拷贝过来的数据与原数据一模一样
-
浅拷贝:顾名思义,是浅浅的拷贝并不深入。所以只复制对象的第一层属性,对于引用类型的属性,只是复制了该引用类型的引用地址,而非实际的数据。因此,对拷贝后对象的修改可能会影响原对象。
-
深拷贝:完全复制对象及其所有嵌套的对象,产生一个全新的独立对象,无论怎样修改都不会影响到原对象。对于引用类型的属性,不是只是复制该引用的地址,而是将引用地址所指向的数据全部拷贝过来,用一个新的引用地址去指向,也就是会创建一个新的引用类型去存储这些数据,虽然这些数据与原数据一模一样,但是引用地址不用。如果用
===去判断的话会返回false
浅拷贝
浅拷贝的实现方法包括:
-
Object.create():创建一个新对象,其原型为提供的对象。let obj = { a: 1 } let obj2 = Object.create(obj) obj.a=2 console.log(obj2.a);//输出的 2 -> 浅拷贝 -
Object.assign():运用对象中合并两个对象的方法来实现,该方法接受俩个参数,将后者合并到前者。let obj = { a: 1, b: [1, 2] } let obj2 = Object.assign({}, obj) obj.b.push(3) console.log(obj2);//输出的 { a: 1,b: [1, 2, 3]} -> 浅拷贝 -
数组方法:如:合并数组的方法
[].concat(结构r)、数组的解构[...arr]、切割数组的方法arr.slice(0)和 数组的顺序倒转arr.toReversed().reverse(),适用于数组的浅拷贝。let arr = [1, 2, 3, {a: 1}] let arr2 = [].concat(arr)// 输出[ 1, 2, 3, { a: 2 } ] let arr2 = [...arr]// 输出[ 1, 2, 3, { a: 2 } ] let arr2 = arr.slice(0)// 输出[ 1, 2, 3, { a: 2 } ] let arr2 = arr.toReversed().reverse() // 输出[ 1, 2, 3, { a: 2 } ] arr[3].a = 2 console.log(arr2); -
手写一个浅拷贝函数:使用
for...in循环结合hasOwnProperty检查,仅复制对象自身的属性。function shallowCopy(obj) { let newobj = {}; for (let key in obj) { // key 是不是obj显示具有的 if (obj.hasOwnProperty(key)) { newobj[key] = obj[key]; } } return newobj; }
然而,浅拷贝不能保证对象内部引用类型属性的独立性。所以一般面试考的不是浅拷贝的手写而是深拷贝的手写,无伤大雅,我们继续。
深拷贝
深拷贝确保了所有层级的属性都是独立的,主要方法包括:
-
JSON.parse(JSON.stringify(obj)):通过序列化再反序列化,但无法识别bigInt类型以及无法拷贝 undefined,function,Symbol这些类型,且无法处理循环引用。
let obj = { a: 1, b: {n: 2}, c: 'cc', d: true, e: undefined, //无法识别 f: null, g: function() {}, //无法识别 h: Symbol(1), //无法识别 // i: 123n // 报错:Do not know how to serialize a BigInt } obj.a = obj.b obj.b.m = obj.a //循环引用 报错:Converting circular structure to JSON let newObj = JSON.parse(JSON.stringify(obj)) console.log(newObj); //输出{ a: 1, b: { n: 2 }, c: 'cc', d: true, f: null } -
structuredClone():现代浏览器中新增的深拷贝方法,能更安全地处理复杂数据结构,包括循环引用,但支持度有限。例如function和Symbol这俩类型无法拷贝let obj = { a: 1, b: {n: 2}, c: 'cc', d: true, e: undefined, f: null, i: 123n, // g: function() {},// 报错:function() {} could not be cloned. // h: Symbol(1) // 报错:Symbol(1) could not be cloned. } const newObj = structuredClone(obj) obj.b.n = 3 console.log(newObj); // 输出{ a: 1, b: { n: 2 }, c: 'cc', d: true, e: undefined, f: null, i: 123n } -
手写一个深拷贝函数:使用递归机制,逐层复制对象及其属性,确保每一层都是独立的。其他的和浅拷贝差不多
let obj = { a: 1, b: {n: 2} } function deepCopy(obj) { let newObj = {} for (let key in obj) { if (obj.hasOwnProperty(key)) { // obj[key] 是不是对象 判断是不是引用类型 // typeof obj[key] === 'object' && obj[key] !== null if (obj[key] instanceof Object) { newObj[key] = deepCopy(obj[key]) } else { newObj[key] = obj[key] } } } return newObj }
写在最后
选择浅拷贝还是深拷贝取决于具体需求。浅拷贝快速且节省内存,适用于不需要深度独立性的场景;而深拷贝虽然更全面,但也意味着更高的资源消耗。掌握这些技巧,可以让我们在处理复杂数据结构时更加得心应手。
无论是浅拷贝还是深拷贝,理解它们的工作原理和适用场景都是提升编程技能的关键。希望本文能帮助你更好地掌握JavaScript中的对象拷贝技巧。