各位观众老爷,大家好!欢迎来到今天的技术讲座,我是你们的老朋友——代码界的老司机。今天我们要聊聊JavaScript中的深拷贝和浅拷贝,这俩兄弟听起来高深莫测,其实理解起来就像吃冰淇淋一样简单(希望如此!)。准备好了吗?系好安全带,咱们要发车啦!
一、什么是拷贝?为什么要拷贝?
在开始深浅拷贝的“爱恨情仇”之前,我们先来聊聊什么是拷贝。简单来说,拷贝就是把一个东西复制一份。在JavaScript里,这个“东西”通常指的是对象或数组。
为什么要拷贝呢?想象一下,你有一个存着重要数据的对象,你不希望直接修改它,而是想基于它创建一个新的对象,然后在新对象上进行各种操作,这样原始数据就能保持不变了。这时候,你就需要拷贝。
二、浅拷贝:只拷贝一层皮
浅拷贝就像克隆了一只绵羊,虽然外表看起来一样,但内核(内部器官)还是原来的那只绵羊的。换句话说,浅拷贝只复制了对象的顶层属性,如果属性值是原始类型(如数字、字符串、布尔值等),那么就直接复制这些值;如果属性值是引用类型(如对象、数组等),那么就仅仅复制这些引用,而不是复制引用指向的实际对象。
这意味着,如果原始对象和浅拷贝对象共享同一个引用类型的属性,那么修改其中一个对象的这个属性,会影响到另一个对象。
浅拷贝的几种常见方式:
-
Object.assign()
这是ES6提供的一个用于对象合并的方法,也可以用来做浅拷贝。
const obj1 = { name: '张三', age: 25, address: { city: '北京', street: '长安街' } }; const obj2 = Object.assign({}, obj1); obj2.name = '李四'; obj2.address.city = '上海'; console.log(obj1.name); // 张三 (原始对象的name属性没变) console.log(obj1.address.city); // 上海 (原始对象的address.city属性变了!)
看到了吗?
obj2.name
的修改没有影响到obj1.name
,因为name
是原始类型。但是,obj2.address.city
的修改影响到了obj1.address.city
,因为address
是引用类型,obj1
和obj2
的address
属性指向的是同一个对象。 -
展开运算符 (
...
)这也是ES6提供的一个方便的语法糖,可以用来展开对象或数组,从而实现浅拷贝。
const obj1 = { name: '张三', age: 25, address: { city: '北京', street: '长安街' } }; const obj2 = { ...obj1 }; obj2.name = '李四'; obj2.address.city = '上海'; console.log(obj1.name); // 张三 console.log(obj1.address.city); // 上海
效果和
Object.assign()
一样。 -
数组的
slice()
和concat()
方法对于数组,
slice()
和concat()
方法在不传入参数的情况下,可以实现浅拷贝。const arr1 = [1, 2, { name: '张三' }]; const arr2 = arr1.slice(); // 或者 const arr2 = arr1.concat(); arr2[0] = 3; arr2[2].name = '李四'; console.log(arr1[0]); // 1 console.log(arr1[2].name); // 李四
同样的,修改
arr2[0]
不会影响到arr1[0]
,但修改arr2[2].name
会影响到arr1[2].name
,因为数组中的对象是引用类型。
总结一下浅拷贝的特点:
特性 | 描述 |
---|---|
拷贝深度 | 只拷贝顶层属性 |
原始类型 | 复制值,修改拷贝对象不会影响原始对象 |
引用类型 | 复制引用,修改拷贝对象的属性会影响原始对象 |
适用场景 | 只需要拷贝顶层属性,不需要深度拷贝,或者明确知道对象中没有嵌套的引用类型时。 |
三、深拷贝:连皮带肉一起复制
深拷贝就像克隆了一只完整的绵羊,从外表到内核都是全新的,和原来的绵羊没有任何关系。换句话说,深拷贝会递归地复制对象的所有属性,包括嵌套的引用类型,创建一个完全独立的新对象。
这意味着,修改原始对象或深拷贝对象的任何属性,都不会影响到另一个对象。
深拷贝的几种常见方式:
-
JSON.parse(JSON.stringify(obj))
这是一种简单粗暴的深拷贝方法,利用了
JSON.stringify()
将对象序列化成JSON字符串,然后再用JSON.parse()
将JSON字符串解析成新的对象。const obj1 = { name: '张三', age: 25, address: { city: '北京', street: '长安街' }, hobbies: ['coding', 'reading'], birthday: new Date() }; const obj2 = JSON.parse(JSON.stringify(obj1)); obj2.name = '李四'; obj2.address.city = '上海'; obj2.hobbies.push('swimming'); console.log(obj1.name); // 张三 console.log(obj1.address.city); // 北京 console.log(obj1.hobbies); // ['coding', 'reading'] console.log(obj1.birthday); // 原来的Date对象 console.log(typeof obj2.birthday); // string
看起来很完美,对不对?但是,这种方法有一些局限性:
- 无法拷贝函数:
JSON.stringify()
会忽略函数。 - 无法拷贝
undefined
:JSON.stringify()
会将undefined
转换为null
。 - 无法拷贝
Symbol
:JSON.stringify()
会忽略Symbol
类型的属性。 - 无法拷贝循环引用: 如果对象存在循环引用,会报错。
Date
对象会被转换成字符串:JSON.stringify()
会将Date
对象转换为 ISO 格式的字符串。- 无法拷贝 RegExp、Error 等特殊对象。
所以,这种方法只适用于简单的数据对象,不适用于复杂的对象结构。
- 无法拷贝函数:
-
递归实现深拷贝
这是一种更通用的深拷贝方法,通过递归遍历对象的属性,如果属性值是原始类型,则直接复制;如果属性值是引用类型,则递归调用深拷贝函数,直到所有属性都被复制。
function deepClone(obj, map = new WeakMap()) { // 处理null和undefined if (obj === null || obj === undefined) { return obj; } // 处理Date if (obj instanceof Date) { return new Date(obj); } // 处理RegExp if (obj instanceof RegExp) { return new RegExp(obj); } // 处理循环引用 if (map.has(obj)) { return map.get(obj); } // 处理对象和数组 if (typeof obj === 'object') { const newObj = Array.isArray(obj) ? [] : {}; map.set(obj, newObj); // 存储已拷贝的对象,用于处理循环引用 for (let key in obj) { if (obj.hasOwnProperty(key)) { newObj[key] = deepClone(obj[key], map); // 递归调用深拷贝函数 } } return newObj; } // 处理原始类型 return obj; } const obj1 = { name: '张三', age: 25, address: { city: '北京', street: '长安街' }, hobbies: ['coding', 'reading'], birthday: new Date(), reg: /abc/g, func: function() { console.log('hello'); } ,//函数不会被拷贝 undef: undefined }; // 循环引用示例 obj1.circular = obj1; const obj2 = deepClone(obj1); obj2.name = '李四'; obj2.address.city = '上海'; obj2.hobbies.push('swimming'); console.log(obj1.name); // 张三 console.log(obj1.address.city); // 北京 console.log(obj1.hobbies); // ['coding', 'reading'] console.log(obj1.birthday); // 原来的Date对象 console.log(obj2.birthday); // 拷贝后的Date对象 console.log(obj1.func); // function() { console.log('hello'); } console.log(obj2.func); // undefined console.log(obj1.circular); // [Circular] console.log(obj2.circular); // [Circular]
这个
deepClone
函数做了以下几件事:- 处理
null
和undefined
: 直接返回null
或undefined
。 - 处理
Date
对象: 创建一个新的Date
对象,并复制原始Date
对象的时间戳。 - 处理
RegExp
对象: 创建一个新的RegExp
对象,并复制原始RegExp
对象的模式和标志。 - 处理循环引用: 使用
WeakMap
存储已经拷贝过的对象,如果再次遇到相同的对象,则直接返回之前拷贝的对象,避免无限递归。 - 处理对象和数组: 创建一个新的对象或数组,然后递归地拷贝所有属性。
- 处理原始类型: 直接返回原始值。
为什么要用
WeakMap
处理循环引用?WeakMap
是一种特殊的Map
,它的键必须是对象。WeakMap
的特点是:- 弱引用: 如果一个对象只被
WeakMap
引用,那么当垃圾回收器运行时,这个对象会被回收,WeakMap
中的引用也会被移除。 - 不可枚举:
WeakMap
中的键是不可枚举的,这意味着你无法遍历WeakMap
中的键。
使用
WeakMap
可以避免内存泄漏。如果使用普通的Map
,即使原始对象不再使用,Map
中仍然会保留对这个对象的引用,导致垃圾回收器无法回收这个对象,从而造成内存泄漏。 - 处理
-
使用第三方库
有很多第三方库提供了深拷贝的功能,例如:
- Lodash:
_.cloneDeep()
- jQuery:
$.extend(true, {}, obj)
这些库通常经过了高度优化,性能更好,也更健壮。
- Lodash:
总结一下深拷贝的特点:
特性 | 描述 |
---|---|
拷贝深度 | 递归拷贝所有属性 |
原始类型 | 复制值,修改拷贝对象不会影响原始对象 |
引用类型 | 递归复制引用指向的对象,修改拷贝对象的属性不会影响原始对象 |
适用场景 | 需要完全复制一个对象,并且不希望修改拷贝对象影响原始对象时。 |
额外注意 | 需要处理循环引用,避免内存泄漏。 |
局限性 | 无法拷贝函数,无法拷贝 Symbol 类型的属性(需要特殊处理)。 |
四、深拷贝 vs 浅拷贝:选择困难症怎么办?
对比项 | 浅拷贝 | 深拷贝 |
---|---|---|
拷贝深度 | 仅拷贝顶层属性 | 递归拷贝所有属性 |
内存占用 | 较小 | 较大 |
性能 | 较快 | 较慢 |
适用场景 | 只需要拷贝顶层属性,或者明确知道对象中没有嵌套的引用类型时。 | 需要完全复制一个对象,并且不希望修改拷贝对象影响原始对象时。 |
修改影响 | 修改拷贝对象的引用类型属性会影响原始对象 | 修改拷贝对象的任何属性都不会影响原始对象 |
实现方式 | Object.assign() 、展开运算符 (... )、数组的 slice() 和 concat() 方法等。 |
JSON.parse(JSON.stringify(obj)) 、递归实现、使用第三方库(如 Lodash 的 _.cloneDeep() )等。 |
局限性 | 无法处理嵌套的引用类型 | JSON.parse(JSON.stringify(obj)) 无法拷贝函数、undefined 、Symbol 、循环引用、Date 对象会被转换成字符串等;递归实现需要处理循环引用,避免内存泄漏;某些情况下,可能需要特殊处理一些特殊对象(如 Error 、RegExp 等)。 |
如何选择?
就像选择冰淇淋口味一样,深拷贝和浅拷贝各有各的优点和缺点。选择哪种方式取决于你的具体需求。
-
如果你的对象很简单,没有嵌套的引用类型,或者你明确知道只需要拷贝顶层属性,那么浅拷贝就足够了。 比如,你只是想创建一个新的对象,然后修改一些简单的属性,而不需要修改嵌套的对象。
-
如果你的对象很复杂,包含嵌套的引用类型,并且你希望修改拷贝对象不会影响原始对象,那么就需要深拷贝。 比如,你有一个包含大量数据的对象,你需要在新对象上进行复杂的计算,并且不希望破坏原始数据。
-
如果你对性能有很高的要求,并且对象结构比较简单,可以考虑使用
JSON.parse(JSON.stringify(obj))
,但要注意它的局限性。 -
如果你需要处理循环引用,或者需要拷贝特殊对象,那么最好使用递归实现深拷贝,或者使用第三方库。
五、总结
深拷贝和浅拷贝是JavaScript中一个重要的概念,理解它们的区别和适用场景,可以帮助你编写更健壮、更可维护的代码。记住,没有银弹,选择哪种方式取决于你的具体需求。
好了,今天的讲座就到这里。希望大家有所收获!如果还有什么疑问,欢迎留言提问。下次再见!