每日一题之深浅拷贝总结

文章详细介绍了JavaScript中的浅拷贝和深拷贝概念,包括Object.assign、Array.prototype.slice、Array.prototype.concat等方法的浅拷贝实现,以及JSON.parse(JSON.stringify())的深拷贝方法,同时提到了这些方法的限制和注意事项,并提供了手动实现深拷贝的示例代码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

已经写过 n次了, 再记录一遍吧

深浅拷贝

浅拷贝

一个新的对象对原始对象的属性值进行精确地拷贝,如果拷贝的是基本数据类型,拷贝的就是基本数据类型的值,如果是引用数据类型,拷贝的就是内存地址。如果其中一个对象的引用内存地址发生改变,另一个对象也会发生变化。

let arr = [12, 2, 3];
let arr2 = arr;
arr[0] = arr[0] + 1;
console.log("arr2 :>> ", arr2);
console.log("arr :>> ", arr);
console.log("arr === arr2 :>> ", arr === arr2); // true
  • Object.assign

    可以理解成用于 JS对象的合并, 其中一个用途就是浅拷贝, 该方法接受多个参数, 第一个参数是目标对象, 其余参数是源对象 Object.assign(target, source, source2)

    注意:

    • 如果目标对象和源对象有同名属性, 或者多个源对象有同名属性, 则后面的属性会覆盖前面的属性
    • 如果只有一个参数, 当参数为对象时, 直接返回该对象, 当参数不是对象时, 会先将参数转为对象然后返回
    • 如果第一个参数为null或者undefined, 将会报错
    • 不会拷贝对象的继承属性, 不会拷贝对象的不可枚举的属性, 可以拷贝Symbol类型的属性
let targetObj = {
  name: "lm",
  age: 22,
  sex: 0,
};
let obj = {
  name: "dzl",
};
let obj2 = {
  name: "世界",
};

Object.assign(targetObj, obj, obj2);
console.log("targetObj :>> ", targetObj);
// {name: '世界', age: 22, sex: 0} 可以看到name被覆盖

let targetObj2 = { ...targetObj };
targetObj.job.key = "web前端";
targetObj.age = 18;
console.log("targetObj.age = 18 :>> ");
console.log("targetObj.job.key = 'web前端'");
console.log("targetObj :>> ", targetObj);
// {
//   age: 18,
//   job: { key: 'web前端', key2: '后端'},
//   name: '世界',
//   sex: 0
// }
console.log("targetObj2 :>> ", targetObj2);
// {
//   age: 22,
//   job: { key: 'web前端', key2: '后端'},
//   name: '世界',
//   sex: 0
// }

// 可以看到基本数据类型是深拷贝, 引用类型则是浅拷贝

  • Array.prototype.slice

    • 该方法会复制出一个副本, 也就是说不会改变原数组
    • 用法: arr.slice(start, end), 参数都不填意味着全复制
let arr = [1, 2, 3, 4, 5];
let arr1 = arr.slice();
arr1[0] = 0;
console.log("arr3 :>> ", arr3); // arr3 :>>  (5) [0, 2, 3, 4, 5]
console.log("arr4 :>> ", arr4); // arr4 :>>  (5) [1, 2, 3, 4, 5]
  • Array.prototype.concat

    • 该方法会返回一个新的数组, 也就是说不会改变原数组
    • 用法: arr.concat(array), 参数都不填意味着全复制
let arr = [1, 2, 3, 4, 5];
let arr1 = arr.concat();
arr1[0] = 0;
console.log("arr3 :>> ", arr3); // arr3 :>>  (5) [0, 2, 3, 4, 5]
console.log("arr4 :>> ", arr4); // arr4 :>>  (5) [1, 2, 3, 4, 5]
console.log("arr4 === arr3 :>> ", arr4); // false

深拷贝

对于简单数据类型直接拷贝值, 对于引用数据类型, 在堆内存中开辟一块新的内存用于存放复制的对象, 两个对象相互独立, 属于不同的内存地址

  • Object.assign

Object.assign 拷贝的对象的属性值只是简单类型(number, boolean, string);
得到的新对象的属性值是深拷贝,
如果属性值是对象或者其他引用类型,
那么拷贝的这个属性值是浅拷贝

const data = {
  name: "dzl",
  age: 20,
  job: {
    job1: "web",
    job2: "back",
    job3: {
      key: "嘻嘻",
    },
  },
};

let newObj = Object.assign({}, data);
newObj.age = 30;
newObj.job.job1 = "哈哈哈哈哈";
console.log("data :>> ", data);
// {
//   age: 20,
//   job: { job1: '哈哈哈哈哈', job2: 'back', job3: {...} },
//   name: 'dzl'
// }
console.log("newObj :>> ", newObj);
// {
//   age: 30,
//   job: { job1: '哈哈哈哈哈', job2: 'back', job3: {...} },
//   name: 'dzl'
// }
  • JSON.parse(JSON.stringify(cloneObj))

    通过JSON进行序列化反序列化可以实现深拷贝, 但是会存在几个问题

    • 会自动忽略拷贝对象中的函数
    • 对日期的半支持(如传入 GMT+0800 时间), 输出 ISO 8601 时间
    • 会忽略undefined类型
    • 会忽略 keysymbol类型的字段
    • 无法保持之前的原型链
    • 拷贝 RegExp 引用类型会变成空对象
    • 对象中包含 NaN, Infinity, 或 -Infinity, 结果会变成 null
const b = {
  name: "dzl",
  age: 20,
  fn: undefined,
  date: new Date(), // Mon Jun 20 2022 11:21:08 GMT+0800 (中国标准时间) {}
};

const b1 = JSON.parse(JSON.stringify(b));
console.log(b1);
// {
//   age: 20,
//   name: "dzl",
//   date: "2022-06-20T03:21:08.213Z", ISO 8601 国际标准化组织的国际标准
// }
  • 手写深拷贝
function cloneDeep(target, map = new map()) {
  if (target === null) return null;
  if (typeof target !== "object") return target;
  // 处理正则, Date类型
  if (target.constructor === Date) return new Date(target);
  if (target.constructor === RegExp) return new RegExp(target);

  // 处理对象内的循环引用1
  if (map.has(target)) return map.get(target);

  // 保持之前的原型链
  const newTarget = new target.constructor();
  // 没有保持原型链判断
  // const newTarget = Array.isArray(target) ? [] : {}

  // 处理对象内的循环引用2
  map.set(target, newTarget);

  // for .. in .. 遍历不到Symbol类型的key, 这里使用 Reflect.ownKeys方法, 它可以遍历当前对象的所有key
  // for(let key in target) {
  //   if (target.hasOwnProperty(key)) {
  //     newTarget[key] = cloneDeep(target[key])
  //   }
  // }

  Reflect.ownKeys(target).forEach((key) => {
    // newTarget[key] = cloneDeep(target[key])
    // 处理对象内的循环引用3
    newTarget[key] = cloneDeep(target[key], map);
  });

  // 处理对象内的循环引用使用Map即可
  return newTarget;
}

浅拷贝总结:

  • Object.assign(target, source, source2)

    • 如果目标对象和源对象有同名属性, 或者多个源对象有同名属性, 则后面的属性会覆盖前面的属性
    • 如果只有一个参数, 当参数为对象时, 直接返回该对象, 当参数不是对象时, 会先将参数转为对象然后返回
    • 如果第一个参数为null或者undefined, 将会报错
    • 不会拷贝对象的继承属性, 不会拷贝对象的不可枚举的属性, 可以拷贝Symbol类型的属性
  • Array.prototype.slice

  • Array.prototype.concat

深拷贝总结:

  • JSON.parse(JSON.stringify(cloneObj))

    通过JSON进行序列化反序列化可以实现深拷贝, 但是会存在几个问题

    • 会自动忽略拷贝对象中的函数
    • 对日期的半支持(如传入 GMT+0800 时间), 输出 ISO 8601 时间
    • 会忽略undefined类型
    • 会忽略 keysymbol类型的字段
    • 无法保持之前的原型链
    • 拷贝 RegExp 引用类型会变成空对象
    • 对象中包含 NaN, Infinity, 或 -Infinity, 结果会变成 null
  • lodash.deepClone

  • 自己写一遍

### JavaScript 中深拷贝与浅拷贝的区别 在 JavaScript 中,深拷贝和浅拷贝是两种不同的对象复制方法。浅拷贝仅复制对象的第一层属性引用,而不会递归地复制嵌套的对象或数组;因此,如果修改了深层嵌套的数据,则会影响原对象。相比之下,深拷贝会完全克隆整个对象及其内部的所有层次结构,从而创建一个独立的新副本。 #### 浅拷贝的特点 当执行浅拷贝操作时,新对象的顶层属性会被单独分配内存空间,但如果这些属性本身是指向其他对象的引用,则它们仍然共享相同的底层数据[^1]。这意味着对子级对象所做的任何更改都会反映到源对象上。 实现方式可以采用 `Object.assign()` 方法或者扩展运算符 (`...`) 来完成简单的浅拷贝: ```javascript const obj1 = { a: 1, b: { c: 2 } }; // 使用 Object.assign 进行浅拷贝 const shallowCopyObjAssign = Object.assign({}, obj1); // 或者使用扩展语法 const shallowCopySpreadSyntax = { ...obj1 }; shallowCopyObjAssign.b.c = 99; // 修改后的值也会同步影响到 original object 的 'b' 属性 console.log(obj1); // 输出:{ a: 1, b: { c: 99 } } ``` #### 深拷贝的特点 为了真正隔离两个变量之间的关系并防止意外更新原有数据,在某些情况下需要进行深拷贝。通过这种方法得到的目标对象与其原型之间没有任何关联——即使后者发生变化也不会波及前者[^3]。 一种常用的简单形式就是利用 JSON 序列化/反序列化的组合来达成目的: ```javascript function deepCloneWithJsonParse(target){ return JSON.parse(JSON.stringify(target)); } let complexObj = { name:"example", details:{ age:28, job:'developer' } }; var clonedComplexObj=deepCloneWithJsonParse(complexObj); clonedComplexObj.details.age=30; console.log(clonedComplexObj.details.age===complexObj.details.age)//false ``` 然而需要注意的是此技术存在局限性,比如无法处理 Date 类型实例、RegExp 对象以及 Function 定义等内容。 另外还有更复杂的递归算法可用于构建更加健壮可靠的解决方案以应对上述缺陷情况下的需求。 #### 常见陷阱 - **循环引用**:如果目标对象含有指向自身的成员项那么无论是哪种类型的复制都可能导致无限递归错误。 - **特殊类型丢失**:像 Map/Set/Symbol 等 ES6 新增特性可能得不到妥善保留。 --- ### 面试相关问题探讨 以下是围绕该主题的一些典型面试题目建议思考方向:
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值