JS深拷贝和浅拷贝,真的只是复制一下这么简单吗?

155 阅读3分钟

大家好,我是大华! Javascript的深拷贝和浅拷贝是啥。这俩到底有啥区别,什么时候该用哪个。

一、浅拷贝例子

假设你有个用户信息对象:

let user = {
  name: '小明',
  age: 25,
  tags: ['前端', '篮球', '音乐']
};

现在你想复制一份,准备做点修改,但不想影响原数据。你可能会这么写:

let userCopy = Object.assign({}, user);
// 或者用扩展符
// let userCopy = { ...user };

这时候看起来一切正常:

userCopy.name = '小红';
console.log(user.name);     // 小明(没变)
console.log(userCopy.name); // 小红(变了)

但问题来了,如果你改的是tags

userCopy.tags.push('旅行');
console.log(user.tags);     // ['前端', '篮球', '音乐', '旅行']
console.log(userCopy.tags); // ['前端', '篮球', '音乐', '旅行']

咦?原对象的 tags 也被改了!

这就是浅拷贝的坑——它只复制了第一层。像nameage这种基本类型没问题,但tags是个数组(引用类型),它复制的只是一个指针,两个对象其实还共用同一个数组。

二、深拷贝能解决这个问题

深拷贝复制的不只是第一层,里面的每一层都完完整整地复制一份,彻底断开联系。

怎么实现?最简单粗暴的方法:

let userDeepCopy = JSON.parse(JSON.stringify(user));

再试试:

userDeepCopy.tags.push('电影');
console.log(user.tags);        // ['前端', '篮球', '音乐', '旅行'] (没变)
console.log(userDeepCopy.tags); // ['前端', '篮球', '音乐', '旅行', '电影']

这次终于互不影响了。

三、什么时候用哪个?

场景1:只是改个名字、状态,用浅拷贝就够了

比如你从接口拿到一个列表,想临时标记某个条目为“已选中”:

let list = getData(); // 假设这是接口返回的数据
let newList = list.map(item => ({
  ...item,
  selected: false
}));

// 后来用户点了某一项
newList[0].selected = true;

这种只改第一层属性的,完全可以用浅拷贝,简单又高效。

💡小提示:这种只读不改原数据的操作,浅拷贝足够安全。

场景2:要改嵌套结构,就得上深拷贝

我在做一个表单编辑器时遇到过这个问题。数据结构长这样:

let form = {
  title: '用户调查',
  fields: [
    { label: '姓名', type: 'text' },
    { label: '爱好', type: 'checkbox' }
  ]
};

用户想复制一个类似的表单来进行修改。如果用浅拷贝:

let newForm = { ...form };
newForm.fields.push({ label: '年龄', type: 'number' });

结果发现,原来的form里也多了一个字段!因为fields数组还是同一个。

后来改用深拷贝:

let newForm = JSON.parse(JSON.stringify(form));
// 再改就安全了

场景3:注意!深拷贝也有局限

别以为深拷贝是万能的。我后来发现,如果对象里有函数、undefined、Symbol,或者有循环引用,JSON.parse(JSON.stringify()) 就会出问题。

比如:

let obj = {
  name: 'test',
  fn: function() { console.log('hello'); }
};

let copy = JSON.parse(JSON.stringify(obj));
copy.fn(); // 报错!fn 变成 undefined 了

这时候就得用更高级的方案,比如 Lodash 的 cloneDeep

const _ = require('lodash');
let safeCopy = _.cloneDeep(obj);

四、一句话总结

  • 浅拷贝:只复制表面一层,速度快,适合只读或只改第一层的场景。
  • 深拷贝:里里外外全复制,安全但慢一点,适合要修改嵌套结构的情况。

我现在的习惯是:

  • 大部分时候先用扩展符{...obj},简单直接。
  • 如果发现改着改着原数据也被动了,马上换成JSON.stringify_.cloneDeep

希望这篇没那么多术语的分享,也能帮你少踩点坑。