找回密码
 立即注册
首页 业界区 业界 【每日一面】实现一个深拷贝函数

【每日一面】实现一个深拷贝函数

泻缥 昨天 22:25
基础问答

问:知道浅拷贝和深拷贝吗?为什么要用深拷贝?
答:拷贝,可以认为是赋值,对于 JavaScript 中的基础类型,如 string, number, null, boolean, undefined, symbol 等,在赋值给一个变量的时候,是直接拷贝值给变量,而对于引用类型,如 object, array, function 等,则会拷贝其引用(地址)。
使用深拷贝,是为了避免操作公共对象的时候,影响到其他使用该对象的组件。
扩展延伸

一个拷贝函数,可以直接评估出来你对 JavaScript 基础能力掌握水平。
在理解浅拷贝和深拷贝前,需先明确拷贝的本质。在 JavaScript 中数据类型分为基本类型(string、number、boolean、null、undefined、symbol、bigint)和引用类型(object、array、function 等),这两种类型在内存中存储方式是不一样的:

  • 基本类型:值直接存储在栈内存中,赋值时直接拷贝值。
  • 引用类型:值存储在堆内存中,栈内存仅存储指向堆内存的引用地址,赋值时仅拷贝引用地址(而非实际值)。
所以,根据这两种存储方式很容易想到,浅拷贝和深拷贝的区别就在于 是否递归复制嵌套的引用类型。 这里给出一个简单的定义:

  • 浅拷贝(Shallow Copy):仅复制对象的表层属性,若属性值为引用类型(如嵌套对象、数组),则拷贝的是引用地址(引用地址就是表层属性),新旧对象共享嵌套数据。
  • 深拷贝(Deep Copy):递归复制对象的所有属性,包括嵌套的引用类型,新旧对象完全独立,修改拷贝后的对象不会影响原始对象的数据。
实现方式

浅拷贝

浅拷贝适用于无嵌套引用类型或无需独立嵌套数据的场景,实现方式简单,性能开销小。

  • 浅拷贝对象 Object.assign()
    Object.assign(target, ...sources) 方法将源对象的可枚举属性复制到目标对象,最后返回的是目标对象,使用这个方法时要注意:该方法仅拷贝对象自身属性(不包含继承属性),嵌套的对象仅拷贝引用,示例如下:
  1. const obj = { a: 1, b: { c: 2 } };
  2. const shallowCopy = Object.assign({}, obj);
  3. // 测试基本类型属性:修改不影响原对象
  4. shallowCopy.a = 100;
  5. console.log(obj.a); // 输出:1(原对象不变)
  6. // 测试嵌套对象:修改会影响原对象
  7. shallowCopy.b.c = 200;
  8. console.log(obj.b.c); // 输出:200(原对象被修改)
复制代码

  • 浅拷贝数组 Array.prototype.slice() 和 Array.prototype.concat()
    这两个方法返回的都是新数组(不在原数组上操作),示例如下:
  1. const arr = [1, [2, 3]];
  2. const shallowCopy1 = arr.slice(0); // 方法1:slice
  3. const shallowCopy2 = [].concat(arr); // 方法2:concat
  4. // 测试基本类型元素:修改不影响原数组
  5. shallowCopy1[0] = 100;
  6. console.log(arr[0]); // 输出:1(原数组不变)
  7. // 测试嵌套数组:修改会影响原数组
  8. shallowCopy2[1][0] = 200;
  9. console.log(arr[1][0]); // 输出:200(原数组被修改)
复制代码

  • 扩展运算符 ...
    这个是 es6 新增的运算符,可以用于对象和数组的浅拷贝,语法相较于上面两种方式比较简单,示例如下:
  1. // 对象浅拷贝
  2. const obj = { a: 1, b: { c: 2 } };
  3. const shallowObj = { ...obj };
  4. // 数组浅拷贝
  5. const arr = [1, [2, 3]];
  6. const shallowArr = [...arr];
复制代码
深拷贝

深拷贝适用于包含嵌套引用类型且需要完全独立副本的场景,实现复杂度较高,需处理递归、循环引用等边界情况。属于前端八股面试必须准备的一个问题。

  • 序列化方式拷贝 JSON.parse(JSON.stringify())
    利用 JSON 序列化与反序列化实现深拷贝,语法简单,多数时候够用。
  1. const obj = { a: 1, b: { c: 2 }, d: [3, 4] };
  2. const deepCopy = JSON.parse(JSON.stringify(obj));
  3. // 测试嵌套对象:修改不影响原对象
  4. deepCopy.b.c = 200;
  5. console.log(obj.b.c); // 输出:2(原对象不变)
复制代码
但是这个方式有一定的局限性:

  • 不能拷贝函数(JSON不支持)
  • 不能拷贝 undefined ,Symbol 类型
  • 不能处理循环引用
  • 不支持 BigInt 类型
  • 对于日期对象和正则对象,有特殊处理,解析后可能得不到我们想要的结果

  • 自定义实现拷贝函数
    思路:遍历对象,每一次遍历过程中判断是否是引用类型(对象或数组),如果是,则递归的调用拷贝函数,若不是,则直接赋值进行下一步。
  1. function deepCopy(target) {
  2.   // 基本类型直接返回
  3.   if (target === null || typeof target !== 'object') {
  4.     return target;
  5.   }
  6.   // 区分数组和对象
  7.   let copy;
  8.   if (Array.isArray(target)) {
  9.     copy = [];
  10.   } else {
  11.     copy = {};
  12.   }
  13.   // 遍历属性并递归拷贝
  14.   for (const key in target) {
  15.     if (target.hasOwnProperty(key)) {
  16.       // 递归处理引用类型
  17.       copy[key] = deepCopy(target[key]);
  18.     }
  19.   }
  20.   return copy;
  21. }
  22. // 测试
  23. const obj = { a: 1, b: { c: 2 }, d: [3, 4] };
  24. const copyObj = deepCopy(obj);
  25. copyObj.b.c = 200;
  26. console.log(obj, copyObj, obj === copyObj, obj.b.c); // 对比输出结果,可以发现两个对象是不同的
  27. copyObj.d[0] = 300;
  28. console.log(obj, copyObj, obj === copyObj, obj.d[0]); // 同上
复制代码
但是这个没有处理边界情况,主要是两种情况:

  • 循环应用
    循环引用指对象引用自身(如 obj.self = obj),直接递归会导致无限循环栈溢出。可以用 WeakMap 存储已拷贝的对象,避免在递归过程中重复拷贝。
  • 特殊对象
    类似于 Date,RegExp 的对象,需要我们手动特殊处理(根据类型直接 new)
    完整的深拷贝示例:
  1. function deepCopy(target, hash = new WeakMap()) {
  2.   // 基本类型直接返回
  3.   if (target === null || typeof target !== 'object') {
  4.     return target;
  5.   }
  6.   // 处理循环引用:若已拷贝过,直接返回缓存的副本
  7.   if (hash.has(target)) {
  8.     return hash.get(target);
  9.   }
  10.   let copy;
  11.   // 处理Date
  12.   if (target instanceof Date) {
  13.     copy = new Date(target);
  14.     hash.set(target, copy);
  15.     return copy;
  16.   }
  17.   // 处理RegExp
  18.   if (target instanceof RegExp) {
  19.     copy = new RegExp(target.source, target.flags);
  20.     copy.lastIndex = target.lastIndex; // 保留lastIndex属性
  21.     hash.set(target, copy);
  22.     return copy;
  23.   }
  24.   // 处理数组和对象
  25.   if (Array.isArray(target)) {
  26.     copy = [];
  27.   } else {
  28.     // 处理普通对象(包括自定义对象)
  29.     copy = new target.constructor(); // 保持原型链
  30.   }
  31.   // 缓存已拷贝的对象,解决循环引用
  32.   hash.set(target, copy);
  33.   // 遍历属性并递归拷贝
  34.   // 处理Map
  35.   if (target instanceof Map) {
  36.     target.forEach((value, key) => {
  37.       copy.set(key, deepCopy(value, hash));
  38.     });
  39.     return copy;
  40.   }
  41.   // 处理Set
  42.   if (target instanceof Set) {
  43.     target.forEach(value => {
  44.       copy.add(deepCopy(value, hash));
  45.     });
  46.     return copy;
  47.   }
  48.   // 处理普通对象和数组的属性
  49.   for (const key in target) {
  50.     if (target.hasOwnProperty(key)) {
  51.       copy[key] = deepCopy(target[key], hash);
  52.     }
  53.   }
  54.   return copy;
  55. }
  56. // 测试循环引用
  57. const obj = { name: 'test' };
  58. obj.self = obj; // 循环引用
  59. const copyObj = deepCopy(obj);
  60. console.log(copyObj.self === copyObj, copyObj === obj, obj, copyObj);
  61. // 测试特殊对象
  62. const date = new Date();
  63. const copyDate = deepCopy(date);
  64. console.log(copyDate instanceof Date, copyDate === date, date, copyDate);
  65. const reg = /abc/gim;
  66. reg.lastIndex = 10;
  67. const copyReg = deepCopy(reg);
  68. console.log(copyReg, reg);
复制代码
差异对比

这里我简单总结一个表来让你快速理解二者异同:
对比方向浅拷贝深拷贝拷贝层级仅拷贝对象表层属性递归拷贝所有层级(包括嵌套的引用类型)内存占用较小(共享嵌套对象的内存)较大(完全复制所有数据,独立占用内存)性能开销低(无需递归,操作简单)高(递归处理,需处理边界情况)拷贝前后对象的独立性表层属性独立,嵌套引用类型共享完全独立,新旧对象无任何关联适用场景无嵌套引用类型、性能优先、无需独立嵌套数据的情况,简单来说,不需要前后独立的,都可以直接用浅拷贝有嵌套引用类型、需完全隔离数据、修改不能相互影响的情况实现复杂度简单(可通过原生方法或简单遍历实现)复杂(需处理递归、循环引用、特殊对象类型)面试追问


  • 直接使用 = 赋值算浅拷贝还是深拷贝?
    都不是,赋值运算符只是将一个值或者引用赋给一个变量,对于基本类型,赋值运算符是直接复制这个值给变量,对于引用类型,赋值运算符则是复制引用给变量,而非对象本身。
    这个和浅拷贝的定义略有差异。
  • 实现一个浅拷贝函数?
    思路就是,直接遍历浅层对象(第一层),赋给新的对象。
  1. function shallowCopy(target) {
  2.   // 区分目标是数组还是对象
  3.   if (Array.isArray(target)) {
  4.     const copy = [];
  5.     for (let i = 0; i < target.length; i++) {
  6.       copy[i] = target[i];
  7.     }
  8.     return copy;
  9.   } else if (target !== null && typeof target === 'object') {
  10.     const copy = {};
  11.     // 仅拷贝自身可枚举属性
  12.     for (const key in target) {
  13.       if (target.hasOwnProperty(key)) {
  14.         copy[key] = target[key];
  15.       }
  16.     }
  17.     return copy;
  18.   } else {
  19.     // 基本类型直接返回(无需拷贝)
  20.     return target;
  21.   }
  22. }
  23. // 测试
  24. const obj = { a: 1, b: { c: 2 }, d: [3, 4] };
  25. const copyObj = shallowCopy(obj);
  26. copyObj.b.c = 200;
  27. console.log(obj.b.c); // 输出:200(嵌套对象共享引用)
复制代码

  • 深拷贝的时候,怎么特殊处理函数类型?
    函数属于引用类型,通常不需要深拷贝,因为函数体是改不了的,通常直接复制引用就行了。
    如果面试时强烈要求你深拷贝,可以直接使用 toString() + eval 实现,但可能随之而来的会将话题转到 eval 上来问词法作用域、严格模式、安全问题等等,一般是来转换个话题。
  • 实际开发的时候,有经常用这两种模式吗?举个场景说明一下


  • 前端分页,displayData 通常是直接通过 slice 获取原始列表的一部分数据,由于不需要操作,所以也不需要深拷贝
  • 接口传参,有时候我们为了方便,会在请求数据信息之后,直接将这个返回的对象赋值给某个地方,之后再提交的时候,由于接口要求的信息不同,我们有可能会直接操作这个返回对象,导致使用返回对象的地方出现变化,这种情况就需要深拷贝。
友情链接:webfem.com
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册