基础篇-深浅拷贝-《前端知识进阶》

admin 2025-11-01 15:21:35 编程 来源:ZONE.CI 全球网 0 阅读模式
  • 1. 浅拷贝
    • 浅拷贝的方法
    • (1)直接赋值
    • (2)Object.assign()
    • (3)扩展运算符
    • (4)数组方法实现数组浅拷贝
      • 1)Array.prototype.slice
      • 2)Array.prototype.concat
  • (5)手写实现浅拷贝
  • 2. 深拷贝
    • (1)Object.assign()
    • (2)JSON.stringify()
    • (3)函数库lodash的_.cloneDeep方法
    • (4)手写实现深拷贝函数
  • 3. 解决递归爆栈
  • 4. 解决循环引用
  • 5. 总结

    1. 浅拷贝

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

    浅拷贝的方法

    (1)直接赋值

    1. let arr1 = [1,2,3];
    2. let arr2 = arr1;
    3. new2[0] = 0;
    4. console.log(arr1); // [0, 2, 3]
    5. console.log(arr1); // [0, 2, 3]
    6. console.log(arr1 === arr2); // true

    (2)Object.assign()

    object.assign 是 ES6 中 object 的一个方法,该方法可以用于 JS 对象的合并等多个用途,其中一个用途就是可以进行浅拷贝。该方法接受的第一个参数是目标对象,其余参数是源对象,用法:Object.assign(target, source_1, ···),该方法可以实现浅拷贝,也可以实现一维对象的深拷贝。注意:**

    • 如果目标对象和源对象有同名属性,或者多个源对象有同名属性,则后面的属性会覆盖前面的属性。
    • 如果该函数只有一个参数,当参数为对象时,直接返回该对象;当参数不是对象时,会先将参数转为对象然后返回。
    • 因为nullundefined 不能转化为对象,所以第一个参数不能为nullundefined,会报错。
    • 它不会拷贝对象的继承属性,不会拷贝对象的不可枚举的属性,可以拷贝 Symbol 类型的属性。

      1. let target = {a: 1};
      2. let object2 = {b: 2};
      3. let object3 = {c: 3};
      4. Object.assign(target,object2,object3);
      5. console.log(target); // {a: 1, b: 2, c: 3}

      (3)扩展运算符

      使用扩展运算符可以在构造字面量对象的时候,进行属性的拷贝。语法:let cloneObj = { ...obj };

      1. let obj1 = {a:1,b:{c:1}}
      2. let obj2 = {...obj1};
      3. obj1.a = 2;
      4. console.log(obj1); //{a:2,b:{c:1}}
      5. console.log(obj2); //{a:1,b:{c:1}}
      6. obj1.b.c = 2;
      7. console.log(obj1); //{a:2,b:{c:2}}
      8. console.log(obj2); //{a:1,b:{c:2}}

      扩展运算符 和 object.assign 有同样的缺陷,也就是实现的浅拷贝的功能差不多,但是如果属性都是基本类型的值,使用扩展运算符进行浅拷贝会更加方便。

      (4)数组方法实现数组浅拷贝

      1)Array.prototype.slice

      slice()方法是JavaScript数组的一个方法,这个方法可以从已有数组中返回选定的元素:用法:array.slice(start, end),该方法不会改变原始数组。该方法有两个参数,两个参数都可选,如果两个参数都不写,就可以实现一个数组的浅拷贝。

      1. let arr = [1,2,3,4];
      2. console.log(arr.slice()); // [1,2,3,4]
      3. console.log(arr.slice() === arr); //false

      2)Array.prototype.concat

      concat() 方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。该方法有两个参数,两个参数都可选,如果两个参数都不写,就可以实现一个数组的浅拷贝。

      1. let arr = [1,2,3,4];
      2. console.log(arr.concat()); // [1,2,3,4]
      3. console.log(arr.concat() === arr); //false

      (5)手写实现浅拷贝

      根据以上对浅拷贝的理解,实现一个浅拷贝的大致思路分为两点:

    • 对基础类型做一个最基本的一个拷贝;

    • 对引用类型开辟一个新的存储,并且拷贝一层对象属性。
      1. // 浅拷贝的实现;
      2. function shallowCopy(object) {
      3. // 只拷贝对象
      4. if (!object || typeof object !== "object") return;
      5. // 根据 object 的类型判断是新建一个数组还是对象
      6. let newObject = Array.isArray(object) ? [] : {};
      7. // 遍历 object,并且判断是 object 的属性才拷贝
      8. for (let key in object) {
      9. if (object.hasOwnProperty(key)) {
      10. newObject[key] = object[key];
      11. }
      12. }
      13. return newObject;
      14. }

      2. 深拷贝

      深拷贝是指,对于简单数据类型直接拷贝他的值,对于引用数据类型,在堆内存中开辟一块内存用于存放复制的对象,并把原有的对象类型数据拷贝过来,这两个对象相互独立,属于两个不同的内存地址,修改其中一个,另一个不会发生改变。

      (1)Object.assign()

      Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。
      1. let obj1 = { person: {name: "kobe", age: 41},sports:'basketball' };
      2. let obj2 = Object.assign({}, obj1);
      3. obj2.person.name = "wade";
      4. obj2.sports = 'football'
      5. console.log(obj1); // { person: { name: 'wade', age: 41 }, sports: 'basketball' }

      (2)JSON.stringify()

      JSON.parse(JSON.stringify(obj))是目前比较常用的深拷贝方法之一,它的原理就是利用JSON.stringifyjs对象序列化(JSON字符串),再使用JSON.parse来反序列化(还原)js对象。

    这个方法可以简单粗暴的实现深拷贝,但是还存在问题,拷贝的对象中如果有函数,undefined,symbol,当使用过JSON.stringify()进行处理之后,都会消失。

    1. let obj1 = { a: 0,
    2. b: {
    3. c: 0
    4. }
    5. };
    6. let obj2 = JSON.parse(JSON.stringify(obj1));
    7. obj1.a = 1;
    8. obj1.b.c = 1;
    9. console.log(obj1); // {a: 1, b: {c: 1}}
    10. console.log(obj2); // {a: 0, b: {c: 0}}

    使用该方法时,需要注意以下几点:

    • 无法拷贝不可枚举的属性;
    • 无法拷贝对象的原型链;
    • 拷贝 RegExp 引用类型会变成空对象;
    • 对象中含有 NaN、Infinity 以及 -Infinity,JSON 序列化的结果会变成 null;

    无法拷贝对象的循环应用,即对象成环 (obj[key] = obj)。

    (3)函数库lodash的_.cloneDeep方法

    该函数库也有提供_.cloneDeep用来做 Deep Copy

    1. var _ = require('lodash');
    2. var obj1 = {
    3. a: 1,
    4. b: { f: { g: 1 } },
    5. c: [1, 2, 3]
    6. };
    7. var obj2 = _.cloneDeep(obj1);
    8. console.log(obj1.b.f === obj2.b.f);// false

    (4)手写实现深拷贝函数

    1. function clone(source) {
    2. //判断source是不是对象
    3. if (source instanceof Object == false) return source;
    4. //判断source是对象还是数组
    5. let target = Array.isArray(source) ? [] : {};
    6. for (let i in source) {
    7. if (source.hasOwnProperty(i)) {
    8. //判断数据i的类型
    9. if (typeof source[i] === 'object') {
    10. target[i] = clone(source[i]);
    11. } else {
    12. target[i] = source[i];
    13. }
    14. }
    15. }
    16. return target;
    17. }
    18. console.log(clone({b: {c: {d: 1}}})); // {b: {c: {d: 1}}})

    虽然利用递归能实现一个深拷贝,但是同上面的 JSON.stringfy 一样,还是有一些问题没有完全解决,例如:

    • 这个深拷贝函数并不能复制不可枚举的属性以及 Symbol 类型;
    • 这种方法只是针对普通的引用类型的值做递归复制,而对于 Array、Date、RegExp、Error、Function 这样的引用类型并不能正确地拷贝;
    • 对象的属性里面成环,即循环引用没有解决。

      3. 解决递归爆栈

      我们使用递归的方法对数据进行拷贝,但是这也会出现一个问题,递归的深度的深度太深就会引发栈内存的溢出,我们使用下面的方法来解决递归爆栈的问题:将待拷贝的对象放入栈中,循环直至栈为空。

      1. function cloneLoop(x) {
      2. const root = {};
      3. // 栈
      4. const loopList = [
      5. {
      6. parent: root,
      7. key: undefined,
      8. data: x,
      9. }
      10. ];
      11. while(loopList.length) {
      12. // 深度优先
      13. const node = loopList.pop();
      14. const parent = node.parent;
      15. const key = node.key;
      16. const data = node.data;
      17. // 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素
      18. let res = parent;
      19. if (typeof key !== 'undefined') {
      20. res = parent[key] = {};
      21. }
      22. for(let k in data) {
      23. if (data.hasOwnProperty(k)) {
      24. if (typeof data[k] === 'object') {
      25. // 下一次循环
      26. loopList.push({
      27. parent: res,
      28. key: k,
      29. data: data[k],
      30. });
      31. } else {
      32. res[k] = data[k];
      33. }
      34. }
      35. }
      36. }
      37. return root;
      38. }

      这样我们就解决了递归爆栈的问题,但是循环引用的问题依然存在。

      4. 解决循环引用

      举例:当a对象的中的某属性值为a对象,这样就会造成循环引用。我们使用暴力破解的方法来解决循环引用的问题。思路:引入一个数组uniqueList用来存储已经拷贝的数组,每次循环遍历时,先判断对象是否在uniqueList中了,如果在的话就不执行拷贝逻辑了

      1. function cloneForce(x) {
      2. const uniqueList = []; // 用来去重
      3. let root = {};
      4. const loopList = [
      5. {
      6. parent: root,
      7. key: undefined,
      8. data: x,
      9. }
      10. ];
      11. while(loopList.length) {
      12. const node = loopList.pop();
      13. const parent = node.parent;
      14. const key = node.key;
      15. const data = node.data;
      16. // 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素
      17. let res = parent;
      18. if (typeof key !== 'undefined') {
      19. res = parent[key] = {};
      20. }
      21. // 数据已经存在
      22. let uniqueData = find(uniqueList, data);
      23. if (uniqueData) {
      24. parent[key] = uniqueData.target;
      25. continue;
      26. }
      27. // 数据不存在
      28. // 保存源数据,在拷贝数据中对应的引用
      29. uniqueList.push({
      30. source: data,
      31. target: res,
      32. });
      33. for(let k in data) {
      34. if (data.hasOwnProperty(k)) {
      35. if (typeof data[k] === 'object') {
      36. loopList.push({
      37. parent: res,
      38. key: k,
      39. data: data[k],
      40. });
      41. } else {
      42. res[k] = data[k];
      43. }
      44. }
      45. }
      46. }
      47. return root;
      48. }
      49. //find函数用来遍历uniqueList
      50. function find(arr, item) {
      51. for(let i = 0; i < arr.length; i++) {
      52. if (arr[i].source === item) {
      53. return arr[i];
      54. }
      55. }
      56. return null;
      57. }

      5. 总结

    • 浅拷贝:浅拷贝指的是将一个对象的属性值复制到另一个对象,如果有的属性的值为引用类型的话,那么会将这个引用的地址复制给对象,因此两个对象会有同一个引用类型的引用。浅拷贝可以使用 Object.assign 和展开运算符来实现。

    • 深拷贝:深拷贝相对浅拷贝而言,如果遇到属性值为引用类型的时候,它新建一个引用类型并将对应的值复制给它,因此对象获得的一个新的引用类型而不是一个原有类型的引用。深拷贝对于一些对象可以使用 JSON 的两个函数来实现,但是由于 JSON 的对象格式比 js 的对象格式更加严格,所以如果属性值里边出现函数或者 Symbol 类型的值时,会转换失败 ```javascript // 浅拷贝的实现;

    function shallowCopy(object) { // 只拷贝对象 if (!object || typeof object !== “object”) return;

    // 根据 object 的类型判断是新建一个数组还是对象 let newObject = Array.isArray(object) ? [] : {};

    // 遍历 object,并且判断是 object 的属性才拷贝 for (let key in object) { if (object.hasOwnProperty(key)) { newObject[key] = object[key]; } }

    return newObject; }

    // 深拷贝的实现;

    function deepCopy(object) { if (!object || typeof object !== “object”) return;

    let newObject = Array.isArray(object) ? [] : {};

    for (let key in object) { if (object.hasOwnProperty(key)) { newObject[key] = typeof object[key] === “object” ? deepCopy(object[key]) : object[key]; } }

    return newObject; } ```

    以太坊cppgolang区别 编程

    以太坊cppgolang区别

    以太坊是一种去中心化的开源平台,它采用智能合约技术,旨在构建和运行不受干扰的分布式应用程序。作为目前最受欢迎的区块链平台之一,以太坊提供了多种编程语言的支持,其
    progolang 编程

    progolang

    Go语言(Golang)是由Google开发的一门静态类型编程语言。作为一名专业的Golang开发者,我深知这门语言的优势和特点。在本文中,我将介绍Golang
    golangn个发送者 编程

    golangn个发送者

    Golang是一种开源的编程语言,由Google团队开发,旨在提高程序的并发性和简化软件开发过程。在Go语言中,有时需要向多个接收者发送信息。本文将介绍如何在G
    golang技能图谱 编程

    golang技能图谱

    从互联网行业的快速发展到人工智能技术的日益成熟,各种编程语言也应运而生。而在这众多的编程语言中,Golang(即Go)作为一门强大且高效的开发语言备受关注。Go
    评论:0   参与:  5