一、浅拷贝与深拷贝基础概念
(一)浅拷贝
- 定义:创建新对象,新对象的顶层属性是原对象属性的副本。基本数据类型复制值,引用数据类型复制引用,新旧对象共享引用类型属性。
- 示例代码:
const original = { num: 10, arr: [1, 2, 3] }; const shallowCopy = Object.assign({}, original); shallowCopy.arr.push(4); console.log(original.arr); // 输出:[1, 2, 3, 4] - 面试深挖点:在实际开发场景中,如果原对象的引用类型属性发生频繁变化,使用浅拷贝会带来什么问题?
答案:可能导致不同用户的操作相互干扰,因为共享的引用类型属性会让一个用户的修改影响到其他用户的数据展示和操作结果。
(二)深拷贝
- 定义:递归复制对象及其嵌套引用类型属性,创建完全独立的新对象,修改新对象不影响原对象。
- 示例代码:
function deepCopy(obj) { if (typeof obj !== 'object' || obj === null) { return obj; } let newObj = Array.isArray(obj) ? [] : {}; for (let key in obj) { if (obj.hasOwnProperty(key)) { newObj[key] = deepCopy(obj[key]); } } return newObj; } const original = { num: 10, arr: [1, 2, 3] }; const deepCopyObj = deepCopy(original); deepCopyObj.arr.push(4); console.log(original.arr); // 输出:[1, 2, 3] - 面试深挖点:如果对象中存在循环引用,上述深拷贝函数会出现什么问题?
答案:会导致栈溢出错误。因为循环引用会使递归函数陷入无限循环,不断调用自身,最终耗尽栈内存。
二、内存层面差异
(一)浅拷贝
- 浅拷贝在栈内存为新对象创建新引用,引用类型属性的指针指向堆内存中同一对象。修改引用类型属性会同时影响原对象和新对象。
(二)深拷贝
- 深拷贝在堆内存为引用类型属性重新创建对象,新对象和原对象的引用类型属性指向不同堆内存地址,相互独立。
(三)面试深挖点
- 请描述一下,在 V8 引擎中,浅拷贝和深拷贝过程中内存的具体分配和回收机制是怎样的?
答案:- V8 引擎采用分代垃圾回收机制。
- 浅拷贝时,若新对象只复制引用,对原对象引用类型属性的修改可能会影响垃圾回收的标记过程。
- 深拷贝创建新的对象树,新对象及其属性占用的内存空间在不再被引用时,会由 V8 的垃圾回收器进行回收。
- 频繁进行深拷贝操作时,可能会导致大量临时对象产生,增加垃圾回收的压力,影响程序性能。
三、常见实现方式及优缺点
(一)浅拷贝实现方式
-
Object.assign()- 优点:使用方便,能快速复制对象的一层属性。
- 缺点:只能进行浅拷贝,对于嵌套对象处理不当。
- 示例代码:
const target = {}; const source = { a: { b: 1 } }; Object.assign(target, source); - 面试深挖点:
Object.assign()在复制对象时,对于继承属性和不可枚举属性是如何处理的?
答案:Object.assign()只会复制源对象自身的可枚举属性,不会复制继承属性和不可枚举属性。
-
扩展运算符(
...)- 优点:语法简洁,常用于数组和对象的浅拷贝。
- 缺点:同样只能进行浅拷贝。
- 示例代码:
const original = { a: 1, b: { c: 2 } }; const shallowCopy = {...original }; - 面试深挖点:扩展运算符在浅拷贝对象时,与
Object.assign()在性能上有什么差异?
答案:在处理简单对象时,两者性能差异不明显。但在处理复杂对象时,扩展运算符在语法解析和执行效率上可能略优于Object.assign(),因为扩展运算符的语法更简洁,引擎解析时可能更高效。
-
数组的
slice()和concat()方法- 优点:专门用于数组的浅拷贝,使用简单。
- 缺点:仅适用于数组,且为浅拷贝。
- 示例代码:
const originalArray = [1, [2, 3]]; const shallowCopyArray1 = originalArray.slice(); const shallowCopyArray2 = [].concat(originalArray); - 面试深挖点:
slice()和concat()方法在浅拷贝数组时,对于数组中的NaN值是如何处理的?
答案:slice()和concat()方法会直接复制NaN值。因为NaN虽然表示“非数字”,但它在内存中有自己的存储方式,这两个方法只是复制数组元素的引用或值,所以NaN会被原样复制到新数组中。
(二)深拷贝实现方式
-
递归函数
- 优点:可以自定义实现,能处理大部分对象的深拷贝。
- 缺点:性能较差,尤其是对于嵌套层次很深的对象;无法处理循环引用。
- 示例代码:见前文的
deepCopy函数。 - 面试深挖点:如何优化递归函数实现的深拷贝,以提高性能?
答案:可以通过缓存已拷贝的对象来减少重复拷贝,如使用WeakMap存储已拷贝对象及其副本的映射关系,在递归拷贝时先检查对象是否已被拷贝过,若已拷贝则直接返回缓存的副本,避免重复递归操作。
-
JSON.parse(JSON.stringify())- 优点:使用简单,能快速实现深拷贝。
- 缺点:无法处理函数、正则表达式、
Symbol等特殊类型的属性;会忽略undefined和Symbol类型的属性;不能处理循环引用。 - 示例代码:
const original = { a: 1, b: { c: 2 } }; const deepCopyObj = JSON.parse(JSON.stringify(original)); - 面试深挖点:如果对象中包含函数和正则表达式,使用
JSON.parse(JSON.stringify())进行深拷贝后,数据丢失的原理是什么?
答案:JSON.stringify()在转换对象为 JSON 字符串时,会忽略函数,因为 JSON 规范中不支持函数类型。对于正则表达式,它会将其转换为空对象,因为 JSON 无法表示正则表达式的复杂结构和功能。
-
使用第三方库(如 Lodash)
- 优点:功能强大,能处理各种复杂情况,包括循环引用;性能相对较好。
- 缺点:需要引入额外的库,增加项目体积。
- 示例代码:
const _ = require('lodash'); const original = { a: 1, b: { c: 2 } }; const deepCopyObj = _.cloneDeep(original); - 面试深挖点:Lodash 的
_.cloneDeep()内部是如何实现对循环引用的处理的?
答案:_.cloneDeep()内部使用一个栈来跟踪正在被克隆的对象,在克隆过程中,每当遇到一个对象,先检查该对象是否已经在栈中。如果存在,说明存在循环引用,直接返回已克隆的对应对象,避免无限递归。同时,使用WeakMap来存储已克隆的对象及其副本,以提高查找效率。
四、性能分析
- 递归函数实现的深拷贝:由于需要递归遍历对象的每个属性,时间复杂度较高,尤其是对于嵌套层次深、属性多的对象,性能开销较大。
JSON.parse(JSON.stringify()):性能相对较好,但由于其存在诸多局限性,适用场景有限。- 使用第三方库(如 Lodash):在处理复杂情况时性能表现较为稳定,但引入库会增加项目的体积和加载时间。
五、应用场景
(一)浅拷贝
- 当只需要复制对象的一层结构,且不关心内部引用类型属性的共享时,可使用浅拷贝,以提高性能。
- 在简单的数据传递场景中,避免不必要的内存开销。
(二)深拷贝
- 需要对对象进行完全独立的修改,而不影响原对象时,如实现撤销和重做功能。
- 在多线程或异步操作中,为避免数据竞争和意外修改,保证数据的安全性。
(三)面试深挖点
请举例说明,在前端框架(如 Vue 或 React)的实际开发中,浅拷贝和深拷贝分别在哪些场景下被使用?在 Vue 中,当使用 Object.assign() 对组件的 data 对象进行合并时,若 data 对象包含引用类型属性,就是浅拷贝的应用场景;在 React 中,当需要对组件的 props 进行深度复制,以确保子组件接收的数据不会被意外修改时,可能会使用深拷贝。比如在 Redux 的状态管理中,为了避免直接修改状态导致的不可预测问题,会对状态对象进行深拷贝后再进行操作。