×

JS基础知识:先搞懂浅拷贝和深拷贝的核心区别

作者:Terry2025.07.15来源:Web前端之家浏览:51评论:0
关键词:浅拷贝深拷贝

JS基础知识:先搞懂浅拷贝和深拷贝的核心区别

不少刚开始学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,就能精准定位问题啦~要是你还有啥细节没搞懂,评论区随时喊我!

您的支持是我们创作的动力!
温馨提示:本文作者系Terry ,经Web前端之家编辑修改或补充,转载请注明出处和本文链接:
https://jiangweishan.com/article/shenkaobeiqiankaobei.html

网友评论文明上网理性发言 已有0人参与

发表评论: