JavaScript 原理

浅拷贝与深拷贝详解:前端对象复制的那些事儿

这篇文章带你深入理解前端对象复制中浅拷贝和深拷贝的区别与实现方法,帮你避免常见的引用陷阱。

🎯 引言

前端开发中,操作对象是非常频繁的。但很多小白朋友们在复制对象时容易踩坑,比如修改一个对象的副本,结果却影响了原对象。这篇文章会帮你搞懂对象的引用地址、浅拷贝、深拷贝到底是什么,怎么实现它们,避免不必要的错误,让你的代码更加健壮灵活。


🧩 什么是对象的引用地址?

值类型和引用类型的区别在于它们在内存中存储的方式不同,值类型的数据是存储在栈中,而引用类型的数据是存储在堆中。对象就是属于引用类型。

值类型存储方式

var a = 'xiaoming';
var b = 'libai';
var c = b;

栈模型

变量名称类型
aString"xiaoming"
bString"libai"
cString"libai"

引用类型存储方式

var a = { name: 'xiaoming', age: 18 };
var b = a;

栈模型

变量名称类型
aObject0x000001
bObject0x000001

堆模型

地址信息属性 name属性 age
0x000001xiaoming18

⚠️ 直接复制对象会出现什么问题?

如果直接用赋值操作复制对象,实际上只是复制了引用地址,而不是复制对象本身。本质上,两个变量指向的是同一个对象,改动任何一个都会影响到另一个。

这会导致很多难以察觉的 Bug,特别是当复杂对象被多个地方使用时,在某一处对该对象的修改会影响到所有使用它的地方。


✂️ 什么是浅拷贝?

浅拷贝就是复制对象的第一层属性,如果属性值是值类型(字符串、数字等),直接复制值;如果属性值是对象或数组,仍然复制的是它们的引用地址。

举个例子:

var obj1 = {
    name: '小明',
    hobby: ['篮球', '足球'],
};
var obj2 = Object.assign({}, obj1);
obj2.name = '小红';
obj2.hobby.push('游泳');

console.log(obj1.name); // 小明,值类型改动不影响原对象
console.log(obj1.hobby); // ["篮球", "足球", "游泳"],引用类型改动影响到了原对象

这里 obj2name 字段修改不会影响 obj1,但是 hobby 数组被修改了,原对象也跟着变了。


🔨 如何实现一个浅拷贝?

JavaScript 提供了几个常用方法实现浅拷贝:

  • 使用 Object.assign()
  • 使用扩展运算符(ES6,但本文章避免 ES6 用法)
  • 自己写一个循环赋值的函数

示例:用 Object.assign 实现浅拷贝

var source = { a: 1, b: { c: 2 } };
var copy = Object.assign({}, source);
console.log(copy.b === source.b); // true,b 是引用地址相同
浅拷贝适合对象属性值都是值类型,或者不需要完全独立的时候用。

🌊 什么是深拷贝?

深拷贝则是递归地复制对象的所有层级,确保复制出来的对象和原对象完全独立,彼此之间不会互相影响。

简单说,就是不仅复制第一层属性,还要复制嵌套的对象、数组,直到最底层的值都是新的。

🛠 如何实现一个深拷贝?

深拷贝要递归地复制所有属性,常见实现方式有:

  • 使用 JSON.parse(JSON.stringify(obj)),这是最简单快速的深拷贝方案,但有些限制,比如不能拷贝函数、undefined、循环引用等。
  • 自己写递归函数,判断数据类型,逐层复制。

示例:使用 JSON 实现深拷贝

var source = {
    a: 1,
    b: { c: 2 },
    d: function () {
        console.log('hello');
    },
};
var deepCopy = JSON.parse(JSON.stringify(source));
console.log(deepCopy); // { a: 1, b: { c: 2 } }, 函数被忽略了

下面演示一个简单的递归深拷贝实现:

function deepClone(obj) {
    if (obj === null) return null;
    if (typeof obj !== 'object') return obj;

    var copy = Array.isArray(obj) ? [] : {};

    for (var key in obj) {
        if (obj.hasOwnProperty(key)) {
            copy[key] = deepClone(obj[key]);
        }
    }

    return copy;
}

var source = { a: 1, b: { c: 2 } };
var copy = deepClone(source);
copy.b.c = 3;
console.log(source.b.c); // 2,互不影响
深拷贝的递归实现要注意循环引用的问题,否则容易造成死循环。实际项目中可考虑使用成熟库,比如 Lodash 的 _.cloneDeep

💡 补充知识点:为什么要区分浅拷贝和深拷贝?

在前端开发中,数据状态管理非常重要。比如在 React、Vue 中操作状态时,如果不正确复制对象,可能导致没有触发视图更新,或者出现数据污染。

此外,性能也是考虑因素。深拷贝对大对象很耗性能,浅拷贝虽然不完全复制,但速度快。合理选择拷贝方式,既保证正确性又提高效率。


🧰 实际案例:更新数组中的对象

假设我们有一个用户列表对象数组,需要更新其中某个用户的年龄。

如果直接浅拷贝数组,修改其中一个用户对象,原数组也会改变。用深拷贝就能避免这个问题。

var users = [
    { id: 1, name: '小明', age: 18 },
    { id: 2, name: '小红', age: 20 },
];

var copiedUsers = JSON.parse(JSON.stringify(users));
copiedUsers[0].age = 19;

console.log(users[0].age); // 18,原数组不受影响

🧾 小节总结

  • 对象变量存储的是引用地址,复制时要注意引用共享问题
  • 浅拷贝只复制第一层属性,引用类型仍指向同一地址
  • 深拷贝递归复制所有层级,确保数据完全独立
  • 简单快速深拷贝可用 JSON.parse(JSON.stringify()),但有局限
  • 复杂应用建议用递归函数或成熟库实现深拷贝,避免循环引用陷阱

❓ 知识问答(Q&A)

Q:浅拷贝会复制对象里的函数吗?
A:浅拷贝会复制函数的引用,函数本身不会被复制,引用还是同一个。

Q:为什么用 JSON.parse(JSON.stringify()) 不能深拷贝函数?
A:因为 JSON 格式不支持函数类型,序列化时会被忽略。

Q:有没有简单方法检测对象是否有循环引用?
A:可以借助 WeakSet 来检测遍历过程中是否访问过某个对象,避免死循环。


🧪 小练习

练习 1:请使用 Object.assign 实现一个简单的浅拷贝函数,并测试修改值类型和引用类型属性对原对象的影响。

function shallowCopy(obj) {
    // 请在这里实现代码
}

var original = { name: '小明', hobby: ['篮球', '足球'] };
var copy = shallowCopy(original);
copy.name = '小红';
copy.hobby.push('游泳');

console.log(original.name); // ?
console.log(original.hobby); // ?

练习 2:写一个递归函数,实现深拷贝,注意避免循环引用导致死循环。写完后测试复制一个多层嵌套对象。

function deepClone(obj, hash) {
    // 请在这里实现代码
}

var complexObj = { a: 1, b: { c: 2, d: { e: 3 } } };
var copy = deepClone(complexObj);
copy.b.d.e = 4;

console.log(complexObj.b.d.e); // ?
console.log(copy.b.d.e); // ?

🎉 恭喜你已经掌握了对象引用地址、浅拷贝、深拷贝的核心知识和实现方法啦!这对写出更安全、性能更优的前端代码帮助很大,继续加油吧!