不少刚开始学JavaScript的朋友,总会被深拷贝、浅拷贝搞得晕头转向——明明看着复制了数据,咋改新数据老数据也跟着变?其实这俩概念和JS里“引用类型”的特性紧紧挂钩,弄懂实现方法和适用场景,就能避开很多坑,今天咱就唠唠:JavaScript里深拷贝和浅拷贝咋实现?不同场景该咋选?
要分清实现方法,得先明白浅拷贝和深拷贝到底啥区别,JS里数据分“基本类型”(像字符串、数字、布尔这些,存的是值本身)和“引用类型”(对象、数组、函数这些,变量存的是内存地址,相当于“引用”)。
浅拷贝呢,只复制“表面一层”:如果是基本类型,直接复制值;但要是遇到引用类型(比如对象里嵌套对象、数组里放对象),只复制引用地址,这就导致——新数据和原数据里的嵌套引用“拴在一根绳上”,改了新的,原数据也会跟着变。
深拷贝就不一样了,它是递归式复制所有层级:不管嵌套多少层对象、数组,每一层都会生成全新的独立数据,新数据和原数据在内存里完全分开,改新数据时原数据绝对不会受影响。
举个例子更直观:
// 原对象,嵌套了b对象 const original = { a: 1, b: { c: 2 } };// 浅拷贝后新对象 const shallowCopy = { ...original }; shallowCopy.b.c = 3; console.log(original.b.c); // 输出3,原数据被改了!// 深拷贝后新对象 const deepCopy = JSON.parse(JSON.stringify(original)); deepCopy.b.c = 4; console.log(original.b.c); // 输出3,原数据没变化~
JavaScript浅拷贝的常见实现方法
浅拷贝在“只需要第一层独立,嵌套数据共用”的场景里很实用,性能也比深拷贝好,这些常用方法得掌握:
对象扩展运算符(...)
用{ ...原对象 }
能快速复制对象的可枚举属性。
const obj = { name: '小明', info: { age: 18 } }; const copy = { ...obj }; copy.name = '小红'; // 原obj.name还是'小明'(基本类型复制值) copy.info.age = 20; // 原obj.info.age也变成20(嵌套对象复制引用)
优点是写法简洁,适合简单对象;缺点是只处理第一层,嵌套引用改了会影响原数据。
Object.assign()方法
Object.assign(目标对象, 原对象)
会把原对象的可枚举属性复制到目标对象,用法:
const target = {}; const source = { x: 1, y: { z: 2 } }; Object.assign(target, source); target.y.z = 3; console.log(source.y.z); // 输出3,原数据被修改
它和扩展运算符逻辑类似,都是浅层次合并/复制,适合处理简单结构的对象合并。
数组的slice()和concat()
数组里的浅拷贝更简单:arr.slice()
或arr.concat()
能生成新数组,但数组里的引用类型(比如对象元素)还是共享引用。
const arr = [1, { id: 100 }]; const newArr = arr.slice(); newArr[1].id = 200; console.log(arr[1].id); // 输出200,原数组里的对象被修改
这俩方法适合快速复制“纯基本类型元素”的数组,有对象元素时得小心嵌套引用问题。
手写循环实现浅拷贝
如果想更灵活控制,可以自己写循环遍历属性,比如给对象做浅拷贝:
function shallowClone(obj) { if (typeof obj !== 'object' || obj === null) return obj; const newObj = Array.isArray(obj) ? [] : {}; for (let key in obj) { if (obj.hasOwnProperty(key)) { newObj[key] = obj[key]; // 直接赋值,引用类型复制地址 } } return newObj; }
这种方式和内置方法逻辑一致,好处是能自定义逻辑(比如过滤某些属性),但同样解决不了嵌套引用的问题。
JavaScript深拷贝的常见实现方法
当需要“完全独立的副本”(比如表单编辑、状态管理里的不可变数据),就得用深拷贝,这些方法各有优缺点,得根据场景选:
JSON.parse(JSON.stringify())——简单场景首选
这是最“偷懒”的方法:先把对象转成JSON字符串(去掉引用关系),再解析成新对象,用法:
const obj = { a: 1, b: { c: 2 }, d: [3, 4] }; const deepCopy = JSON.parse(JSON.stringify(obj)); deepCopy.b.c = 5; console.log(obj.b.c); // 输出2,原数据不受影响
优点是写法极简,不用自己写逻辑;但局限性很大:
函数、undefined、Symbol类型的属性会被直接忽略;
循环引用(比如obj.self = obj)会报错;
Date对象会被转成字符串,解析后不是Date类型;
正则表达式会被转成空对象;
所以它只适合纯数据、无复杂类型、无循环引用的对象/数组。
递归函数实现深拷贝——灵活但要处理细节
自己写递归函数,能精确控制每一层的复制,还能处理循环引用、特殊类型,核心思路是:判断数据类型→基本类型直接返回→引用类型递归复制。
基础版递归(处理对象、数组):
function deepClone(target, map = new WeakMap()) { // 基本类型直接返回 if (target === null || typeof target !== 'object') return target; // 处理循环引用:用WeakMap缓存已拷贝的对象 if (map.has(target)) return map.get(target); // 判断是数组还是对象,创建新容器 const cloneTarget = Array.isArray(target) ? [] : {}; map.set(target, cloneTarget); // 缓存当前对象,避免循环引用 // 遍历属性,递归复制 for (let key in target) { if (target.hasOwnProperty(key)) { cloneTarget[key] = deepClone(target[key], map); } } return cloneTarget; }
这个版本能处理对象、数组、循环引用,但还得优化——比如处理Date、RegExp、Set、Map这些特殊对象:
function deepClone(target, map = new WeakMap()) { if (target === null) return null; if (typeof target !== 'object') return target; if (map.has(target)) return map.get(target);// 处理特殊对象:Date if (target instanceof Date) { return new Date(target.getTime()); } // 处理特殊对象:RegExp if (target instanceof RegExp) { return new RegExp(target.source, target.flags); } // 处理特殊对象:Set if (target instanceof Set) { const newSet = new Set(); target.forEach(item => newSet.add(deepClone(item, map))); return newSet; } // 处理特殊对象:Map if (target instanceof Map) { const newMap = new Map(); target.forEach((value, key) => newMap.set(key, deepClone(value, map))); return newMap; }// 普通对象/数组 const cloneTarget = Array.isArray(target) ? [] : {}; map.set(target, cloneTarget); for (let key in target) { if (target.hasOwnProperty(key)) { cloneTarget[key] = deepClone(target[key], map); } } return cloneTarget; }
递归方法的优点是完全自定义,能覆盖所有数据类型;缺点是代码复杂,得考虑各种边界情况,新手容易写错。
借助第三方库——生产环境省心选
如果是项目开发,直接用成熟库更稳,比如Lodash的_.cloneDeep
方法,能处理循环引用、所有JS数据类型,性能和兼容性都经过验证,用法很简单:
import _ from 'lodash'; const obj = { a: 1, b: { c: 2 } }; const deepCopy = _.cloneDeep(obj); deepCopy.b.c = 3; console.log(obj.b.c); // 输出2,原数据安全~
优点是开箱即用,不用自己处理复杂逻辑;缺点是要引入第三方库(如果项目没在用Lodash,可能增加包体积)。
不同场景下,浅拷贝和深拷贝咋选?
不是所有场景都要上深拷贝,得看需求和性能:
浅拷贝适合的场景
数据结构简单:只有一层属性,没有嵌套对象/数组,比如复制一个{ name: 'xxx', age: 18 },用浅拷贝足够安全。
不需要修改嵌套数据:比如把对象传给子组件,子组件只改第一层属性(像切换按钮状态),浅拷贝性能更好。
性能敏感场景:如果数据量极大,深拷贝的递归会拖慢速度,浅拷贝更轻量。
深拷贝适合的场景
数据有多层嵌套:比如表单数据里有地址对象({ province: 'xx', city: 'xx' }),编辑时必须保证原数据不被修改,就得深拷贝。
不可变数据场景:像Redux、Vuex这类状态管理库,修改状态时要返回新对象(避免直接改原状态),深拷贝能保证状态的“不可变性”。
需要完全隔离的副本:比如用户上传草稿,编辑时复制一份独立草稿,原草稿要保留,这时候必须深拷贝。
踩坑预警:别搞混场景!
很多bug都是“该深拷贝时用了浅拷贝”导致的,比如做购物车,每个商品是对象,修改某个商品的数量,结果所有商品数量都变了——就是因为浅拷贝后嵌套对象共享引用。
反过来,“该浅拷贝时用了深拷贝”会浪费性能,比如一个巨型数组里全是数字,用深拷贝递归遍历完全没必要,浅拷贝的slice()足够。
记住核心逻辑,按需选择
浅拷贝和深拷贝的本质是“复制到哪一层”的问题:浅拷贝停在第一层,深拷贝钻到最底层,实现方法没有绝对的好坏,只有合不合适。
日常开发里,先分析数据结构和修改需求:
简单结构、只改表层 → 浅拷贝(扩展运算符、Object.assign()这些足够);
复杂嵌套、要完全隔离 → 深拷贝(JSON方法应急,递归/库方法兜底);
把这些逻辑理顺,再遇到“复制后数据联动变化”的bug,就能精准定位问题啦~要是你还有啥细节没搞懂,评论区随时喊我!
网友评论文明上网理性发言 已有0人参与
发表评论: