浅拷贝与深拷贝的魔法与技巧

前言

  • 在编程世界中,对象和数据结构的复制是一个常见而重要的操作。
  • 然而,拷贝并不总是简单的,因为它可能涉及到引用和嵌套的对象。
  • 在这个背景下,我们将探讨两种主要的拷贝方式:浅拷贝和深拷贝。
  • 理解这两种拷贝方式的内涵以及它们在不同场景下的用途,将有助于编写出更加强大和可靠的代码。
  • 在下文中,我们将深入探讨如何实现浅拷贝和深拷贝,以及它们的实际应用。

浅拷贝

  • 浅拷贝通常用于复制对象、数组等复杂的数据结构,但只复制它们的外部结构和第一层元素。
  • 对于原始数据结构包含的基本数据类型,浅拷贝会直接复制出一份新数据, 新老数据之间不会存在任何关系。
  • 但是如果原始数据结构包含嵌套的对象或数组,浅拷贝会复制嵌套元素的引用,导致新的数据结构和原始数据结构之间共享该嵌套元素。
  • 下面提供了两种方式实现浅拷贝:
    *
    1. 使用 ES6 实现浅拷贝(附带图解)
      1. 使用 for...in + hasOwnProperty() 实现浅拷贝

使用 ES6 实现浅拷贝

代码实现

js 复制代码
function shallowClone(target) {
  // 说明是数组
  if (target instanceof Array) {
    // 方式1:
    return [...target];

    // 方式2:
    // return target.slice()

    // 方式3:
    // return [].concat(target)

    // 方式4:Array.from() 就是将一个类数组转为真正的数组
    // return Array.from(target)

    // 方式5:
    // return target.reduce((pre, item) => {
    //   pre.push(item);
    //   return pre;
    // }, []);

    // ......
  } else if (target !== null && typeof target === "object") {
    // 说明只能是个对象或者数组(也不可能是函数),但是对于数组,上一个分支已经走了,所以这里只可能是对象
    return {
      ...target,
    };
  } else {
    // 说明不是对象也不是数组
    return target;
  }
}

let target = {
  name: "zsy",
  address: {
    x: 100,
    y: 200,
  },
};
let result = shallowClone(target); // 最终拿到的result对象为这样:{name:"zsy", address: 'address地址值', }

console.log(result === target); // false

// 1. 修改的只是基本数据类型
// target.name = 'peiqi'
// console.log(target.name) // peiqi
// console.log(result.name) // zsy

// 2. 修改了引用数据类型的数据
target.address.x = 5000;
console.log(target.address.x); // 5000
console.log(result.address.x); // 5000

console.log(result.address === target.address); // true

图解

  • 下面我们通过图解的形式对上面的代码进行分析:

原始对象 target, 它包含了两个元素, 一个是基本数据类型 name, 另一个是对象 address:
当使用浅拷贝方法创建了新对象 result 时, 对于原始对象中基本数据类型, 则是复制一份。而对于引用数据类型则会复制其引用:

  • 如果通过拷贝后的新对象去修改其子元素 address 中的数据时, 会导致原始对象的 address 也会被修改。
  • 这就是浅拷贝的特点, 新对象和原始对象共享其子元素中的引用数据类型。如果需要避免这种共享, 就需要使用深拷贝。

使用 for...in + hasOwnProperty() 实现浅拷贝

补充 hasOwnProperty() 基本用法

  • 在实现浅拷贝之前, 我们先温习下 hasOwnProperty() 的基本用法。

  • 在 JavaScript 中,hasOwnProperty 是一个对象的方法,用于检查 对象自身 是否具有指定的属性(不包括从原型链继承的属性)。它返回一个布尔值,指示对象自身是否拥有该属性。

  • 使用 hasOwnProperty 方法可以帮助你确定一个属性是对象自身拥有的,还是继承自其原型链。这在编程中非常有用,特别是当你需要遍历对象的属性并只处理对象自身的属性时。

  • 以下是使用 hasOwnProperty 方法的示例:

js 复制代码
const person = {
  name: "peiqi",
  age: 22,
};

console.log(person.hasOwnProperty("name")); // true
console.log(person.hasOwnProperty("city")); // false
  • 在 JavaScript 中,数组是一种特殊的对象,因此数组实例也继承了对象的属性和方法。包括 hasOwnProperty 方法在内的大多数对象原型上的方法也可以在数组实例上使用。
  • 然而,需要注意的是,hasOwnProperty 方法只用于检查对象自身是否具有属性,而不会查找原型链。在数组实例上调用 hasOwnProperty 方法会检查数组实例自身是否具有指定的属性,但不会检查数组的元素。
  • 以下是一个示例,演示了在数组实例上使用 hasOwnProperty 方法:
js 复制代码
const arr = [1, 2, 3];

console.log(arr.hasOwnProperty(0)); // true,数组自身具有索引为 0 的属性
console.log(arr.hasOwnProperty(1)); // true,数组自身具有索引为 1 的属性
console.log(arr.hasOwnProperty(3)); // false,数组自身没有索引为 3 的属性

console.log(arr.hasOwnProperty("length")); // true,数组自身具有 length 属性
console.log(arr.hasOwnProperty("toString")); // false,toString 是从原型链继承的方法

代码实现

下面我们就使用 for...in 实现浅拷贝, 代码如下:

js 复制代码
function shallowClone(target) {
  // 说明对象或者是数组
  if (target !== null && typeof target === "object") {
    const cloneTarget = target instanceof Array ? [] : {};
    for (const key in target) {
      // for...in 会遍历【自身的】和【原型上】的所有属性, 如果是遍历对象,则 key 是属性,如果是数组,则 key 是索引
      if (target.hasOwnProperty(key)) {
        cloneTarget[key] = target[key];
      }
    }
    return cloneTarget;
  } else {
    return target;
  }
}

深拷贝

  • 深拷贝会递归地复制原始数据结构中的所有元素,包括嵌套的对象、数组等。
  • 深拷贝会复制元素的内容,而不是仅复制元素的引用,因此新的数据结构和原始数据结构是完全独立的,彼此之间不会共享任何元素。
  • 由于深拷贝会递归地复制所有的嵌套元素,因此在处理大型数据结构时可能会导致性能问题。
  • 下面提供了两种方式实现深拷贝:
    *
    1. 使用 JSON.stringify() 实现深拷贝
      1. 使用递归实现深拷贝(附带优化版)

使用 JSON.stringify() 实现深拷贝

正常使用

我们先来看下基本的实现

js 复制代码
let target = {
  name: "zsy",
  address: {
    x: 100,
    y: 200,
  },
};

// 调用 JSON.stringify 方法将对象转为 JSON 字符串
let str = JSON.stringify(target);
console.log(str); // str为: "{"name":"peiqi","address":{"x":100,"y":200}}"

// 调用 JSON.parse 方法就是将 JSON 字符串还原为对象
let result = JSON.parse(str);

target.address.x = 300;
console.log(target); // { name: 'zsy', address: { x: 300, y: 200 } }
console.log(result); // { name: 'zsy', address: { x: 100, y: 200 } }

无法解析特殊的值

但是注意:JSON.stringify 无法解析特殊的值,比如 undefined,函数等,拷贝后会发生丢失

js 复制代码
let target = {
  name: "zsy",
  address: {
    x: 100,
    y: 200,
  },
  fn: function () {},
  un: undefined,
};

// 调用JSON.stringify方法将对象转为字符串
let str = JSON.stringify(target);
let result = JSON.parse(str);

target.address.x = 300;
console.log(target); // { name: 'zsy', address: { x: 300, y: 200 }, fn: [Function: fn], un: undefined }
console.log(result); // { name: 'zsy', address: { x: 100, y: 200 } }

从上面的代码中, 可以看出拷贝后的 result 对象丢失了 fn 属性和 un 属性。

无法处理循环引用

除此之外, JSON.stringify 还无法处理循环引用问题。

js 复制代码
let target = {
  name: "peiqi",
  address: {
    x: 100,
    y: 200,
  },
};
target.address.z = target.address;

let result = JSON.stringify(target);

执行上面的代码会报错, 因为 JSON.stringify 无法处理存在处理循环引用问题

使用递归实现深拷贝

  • 使用递归实现深拷贝就可以 解析特殊的值处理循环引用!

代码实现

js 复制代码
function deepClone(target) {
  // 说明是对象或者是数组
  if (target != null && typeof target === "object") {
    let cloneTarget = target instanceof Array ? [] : {};
    // 或者使用下面的方式
    // let cloneTarget = Array.isArray(target) ? [] : {}

    for (const key in target) {
      if (target.hasOwnProperty(key)) {
        // 此处使用递归
        cloneTarget[key] = deepClone(target[key]);
      }
    }
    return cloneTarget;
  } else {
    return target;
  }
}

使用 Map 优化递归深拷贝

  • 使用递归来实现深拷贝时,当在处理大型对象或深度嵌套结构时可能会导致性能问题。
  • 为了优化递归深拷贝,可以使用 Map 数据结构可以在一定程度上优化深拷贝过程,特别是对于大型对象或深度嵌套结构。
  • Map 允许我们将已经拷贝过的对象作为键,将其对应的拷贝作为值存储起来,从而避免重复拷贝相同的对象。
  • 代码如下:
js 复制代码
function deepCopyWithMap(obj, map = new Map()) {
  if (map.has(obj)) {
    return map.get(obj);
  }

  if (obj === null || typeof obj !== "object") {
    return obj;
  }

  if (obj instanceof Array) {
    const copyArr = [];
    map.set(obj, copyArr); // 存入 Map,以便后续引用
    for (let i = 0; i < obj.length; i++) {
      copyArr[i] = deepCopyWithMap(obj[i], map);
    }
    return copyArr;
  }

  if (obj instanceof Object) {
    const copyObj = {};
    map.set(obj, copyObj); // 存入 Map,以便后续引用
    for (const key in obj) {
      if (obj.hasOwnProperty(key)) {
        copyObj[key] = deepCopyWithMap(obj[key], map);
      }
    }
    return copyObj;
  }
}
相关推荐
理想不理想v4 分钟前
vue经典前端面试题
前端·javascript·vue.js
不收藏找不到我5 分钟前
浏览器交互事件汇总
前端·交互
小阮的学习笔记18 分钟前
Vue3中使用LogicFlow实现简单流程图
javascript·vue.js·流程图
YBN娜19 分钟前
Vue实现登录功能
前端·javascript·vue.js
阳光开朗大男孩 = ̄ω ̄=19 分钟前
CSS——选择器、PxCook软件、盒子模型
前端·javascript·css
minDuck23 分钟前
ruoyi-vue集成tianai-captcha验证码
java·前端·vue.js
小政爱学习!44 分钟前
封装axios、环境变量、api解耦、解决跨域、全局组件注入
开发语言·前端·javascript
魏大帅。1 小时前
Axios 的 responseType 属性详解及 Blob 与 ArrayBuffer 解析
前端·javascript·ajax
花花鱼1 小时前
vue3 基于element-plus进行的一个可拖动改变导航与内容区域大小的简单方法
前端·javascript·elementui
k09331 小时前
sourceTree回滚版本到某次提交
开发语言·前端·javascript