likes
comments
collection
share

从影子到实体:浅拷贝与深拷贝的解析

作者站长头像
站长
· 阅读数 54

前言

在 JavaScript 中,存在多种复制对象的方法,那么什么样的复制方式为浅拷贝,什么样为深拷贝呢?下面我们就来了解一下。

先了解数据存储

在js中,变量的值可以分为两种类型:原始类型和引用类型。原始类型包括数字、字符串、布尔值、null 和 undefined,它们都是按值存储的,即每个变量都有一个独立的内存空间用于存储值。而引用类型则是指对象、数组、函数等复杂数据类型,它们在内存中以引用的方式存储,即变量存储的是对象的地址,而非对象本身。

原始类型的存储结构

对于原始类型的值,JavaScript 引擎会将它们直接存储在栈中。栈是一种线性结构,采用后进先出(LIFO)的方式存储数据。每个栈帧包含了当前函数的局部变量及其参数,当函数执行完毕后,该栈帧被弹出,其中的数据也随之消失。因此,原始类型的值具有不可变性,即一旦赋值就无法更改。

例如,当我们定义一个数字变量 a 时,JavaScript 引擎会在栈中分配一段内存空间,并将其值(假设为 10)存储在其中:

let a = 10;
// 会在栈中分配一段内存空间,存储值为 10 的数字变量 a

当我们将 a 的值赋给另一个变量 b 时,JavaScript 引擎会在栈中为 b 分配一段新的内存空间,并将 a 的值(即 10)复制到该空间中:

let b = a;
// 会在栈中分配一段新的内存空间,存储值为 10 的数字变量 b
// 并将 a 的值(即 10)复制到该空间中

由于原始类型的值具有不可变性,因此对 ab 的修改都只会影响到对应的变量,而不会影响到另一个变量:

a = 20;
console.log(b);  // 10

引用类型的存储结构

对于引用类型的值,JavaScript 引擎会在堆中为其分配一段内存空间,并将其地址存储在栈中。堆是一种非线性结构,以链表的形式存储数据,每个节点包含了对象的属性和方法等信息。当我们创建一个对象时,JavaScript 引擎会在堆中为其分配一段连续的内存空间,并返回该空间的地址作为对象的引用。

例如,当我们定义一个对象变量 obj 时,JavaScript 引擎会在堆中为其分配一段内存空间,并在栈中存储该空间的地址:

let obj = { name: 'John', age: 30 };
// 会在堆中分配一段内存空间,存储 { name: 'John', age: 30 } 对象
// 并在栈中存储该空间的地址,即 obj 变量的值

当我们将 obj 的值赋给另一个变量 obj2 时,JavaScript 引擎会将 obj 的地址复制给 obj2,即 obj2obj 指向了同一个对象:

let obj2 = obj;
// 会将 obj 的地址复制给 obj2,它们指向同一个对象

由于 objobj2 指向同一个对象,在修改对象的属性时,两者都会受到影响:

obj.age = 40;
console.log(obj2.age);  // 40

这就是浅拷贝的实现机制,即复制了对象的引用,而不是对象本身。如果我们想要实现深拷贝,则需要递归地遍历对象的所有属性和嵌套对象,并为它们分配新的内存空间。这样可以确保复制的对象与原始对象完全独立,互不影响。

所以,接下来我们详细介绍一下浅拷贝和深拷贝!

浅拷贝

浅拷贝是指创建一个新对象或数据结构,并将原始对象的引用复制给新对象。换句话说,浅拷贝只复制了对象的引用而不是对象本身。这意味着新旧对象仍然共享相同的内存地址,对其中一个对象的修改会影响到另一个对象。

常见浅拷贝方式

  1. Object.create(x): 这种方式通过创建一个新对象,并将原始对象的属性复制到新对象中来进行浅拷贝。

    const x = { a: 1, b: 2 };
    const clone = Object.create(x);
    console.log(clone); // {}
    console.log(clone.a); // 1
    console.log(clone.b); // 2
    
  2. Object.assign({}, a): 使用Object.assign()方法可以将一个或多个源对象的属性复制到目标对象中。在这种方式中,我们创建了一个空对象作为目标对象,并将原始对象的属性复制到新对象中。

    const a = { x: 1, y: 2 };
    const clone = Object.assign({}, a);
    console.log(clone); // { x: 1, y: 2 }
    
  3. [].concat(x): 使用数组的concat()方法可以将一个或多个数组连接起来,并返回一个新的数组。

    const x = [1, 2, 3];
    const clone = [].concat(x);
    console.log(clone); // [1, 2, 3]
    
  4. [...arr]: 使用扩展运算符(spread operator)可以将一个可迭代对象(如数组)展开为独立的元素,并用于创建一个新的数组。

    const arr = [1, 2, 3];
    const clone = [...arr];
    console.log(clone); // [1, 2, 3]
    
  5. slice(): 数组的slice()方法可以返回一个新数组,包含原始数组的指定部分。

    const arr = [1, 2, 3];
    const clone = arr.slice();
    console.log(clone); // [1, 2, 3]
    

浅拷贝方式只能复制一层对象或数组的属性,如果原始对象或数组中包含引用类型的属性,则复制的结果仍然是引用,修改其中一个对象或数组会影响到其他对象或数组。

手写实现浅拷贝

实现原理

  1. 借助for in 遍历原对象,将原对象的属性值增加到新对象中

  2. 因为for in 会遍历到对象隐式具有的属性,通常要使用obj.hasOwnProperty(key)来判断要拷贝的属性是否是自身显示具有的属性

实现代码

function shallow(obj) {
  let newObj = {}
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) { //判断是否是自身显示属性
      newObj[key] = obj[key];
    }
  }
  return newObj;
}

深拷贝

深拷贝是指创建一个全新的对象或数据结构,并递归地复制原始对象的所有值和嵌套对象,而不是仅仅复制引用。这样,新对象在内存中有一个独立的副本,对任何对象的修改都不会影响到其他对象。

常见深拷贝方式

JSON.parse(JSON.stringify(obj))

执行 JSON.stringify(obj) 会将对象序列化为 JSON 格式的字符串。然后执行 JSON.parse(jsonStr) 方法将 JSON 字符串解析为新的对象。由于 JSON 格式的字符串是一个新的字符串,因此解析出来的对象与原始对象没有任何关联,即完成了深拷贝操作。

示例:

const obj = {
  name: 'John',
  age: 25,
  func: function() {
    console.log('Hello');
  }
};

const copy = JSON.parse(JSON.stringify(obj));
console.log(copy);

缺点和限制:

  1. 不能识别 BigInt:BigInt 是 JavaScript 中的一种数据类型,表示任意精度的整数。但是在使用 JSON.stringify() 时,BigInt 类型的值会被转换为字符串,导致丢失精度。在进行拷贝时,被转换为字符串的 BigInt 值会变成普通的字符串类型。

  2. 不能拷贝函数、Symbol、undefined 类型的值:JSON.stringify() 会忽略函数、Symbol 和 undefined 类型的属性值,因此在进行拷贝时这些值会被丢失。

  3. 不能处理循环引用:如果原始对象中存在循环引用(即某个属性的值引用了对象自身),则在进行拷贝时会导致无限递归,最终抛出异常。

structuredClone()

structuredClone() 是 HTML5 中新增的 API,可以实现深拷贝操作,支持拷贝不同类型的数据,包括函数、Symbol 和循环引用等。

示例:

// 创建一个对象
const obj = {
  name: "John",
  age: 30,
  hobbies: ["reading", "coding"],
  greet: function() {
    console.log("Hello!");
  }
};

// 使用 structuredClone() 方法进行深拷贝
const clonedObj = structuredClone(obj);

// 输出原始对象和深拷贝后的对象
console.log(obj);
console.log(clonedObj);

// 修改原始对象的属性
obj.name = "Jane";
obj.hobbies.push("swimming");

// 输出修改后的原始对象和深拷贝后的对象
console.log(obj);
console.log(clonedObj);

缺点和限制:

  1. 在同一个页面中执行 structuredClone() 操作,会对内存使用造成较大影响。

  2. 该方法只能在浏览器(目前只有谷歌浏览器)环境中使用,无法在 Node.js 等非浏览器环境下使用。

手写实现深拷贝

实现原理

  1. 借助for in 遍历原对象,将原对象的属性值增加到新对象中

  2. 因为for in 会遍历到对象隐式具有的属性,通常要使用obj.hasOwnProperty(key)来判断要拷贝的属性是否是自身显示具有的属性

  3. 如果遍历到的属性值是原始值类型,直接往新对象中赋值,如果是引用类型,递归创建新的子对象

实现代码

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

  const clone = Array.isArray(obj) ? [] : {};

  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      if (typeof obj[key] === "function") {
        clone[key] = Function(`return ${obj[key].toString()}`)();
      } else {
        clone[key] = deepClone(obj[key]);
      }
    }
  }
  return clone;
}

只是简易版,对于不同类型数据,还需进行详细划分!

赋值、浅拷贝与深拷贝的区别

总结如下表:

特点赋值浅拷贝深拷贝
基本概念将一个变量的值(或引用)赋给另一个变量创建一个新对象,并将原始对象的属性值复制到新对象,但不复制对象引用的对象本身创建一个新对象,并将原始对象的所有属性值及其引用的对象都进行复制,形成完全独立的新对象
指针关系赋值后,两个变量指向同一个内存地址(对于引用类型)浅拷贝后,新对象与原始对象具有相同的属性值(对于引用类型,指向相同的内存地址)深拷贝后,新对象与原始对象在内存中是独立的,互不干扰
内存占用无额外内存开销(对于基本类型),引用类型仅增加一个引用可能有额外的内存开销,具体取决于对象的复杂度会有额外的内存开销,因为创建了完全独立的新对象及其引用的对象
引用类型处理引用类型变量赋值后,两个变量共享同一个对象引用类型属性浅拷贝后,两个对象共享同一个子对象(如果属性是引用类型)引用类型属性深拷贝后,两个对象的子对象也是完全独立的
修改影响修改原始对象会影响所有指向它的变量修改浅拷贝后的对象,可能会影响原始对象(如果修改了引用类型的属性)修改深拷贝后的对象,不会影响原始对象

最后

掌握深拷贝和浅拷贝,不要轻易掉进复制陷阱里!

从影子到实体:浅拷贝与深拷贝的解析

转载自:https://juejin.cn/post/7371358964547682319
评论
请登录