探索 JavaScript 中的对象拷贝:浅拷贝与深拷贝的区别与应用

497 阅读6分钟

探索 JavaScript 中的对象拷贝:浅拷贝与深拷贝的区别与应用

前言

当涉及JavaScript中的拷贝时,理解浅拷贝和深拷贝的概念至关重要。JavaScript中的拷贝操作通常涉及复制对象的引用或值,这直接影响到如何处理数据和对象之间的关系。本文将探讨浅拷贝和深拷贝的区别,以及如何使用不同的方法来实现它们,帮助你更好地理解和应用JavaScript中的拷贝技术。

什么是拷贝?

拷贝是指将一个对象的值(或引用)复制到另一个对象中的操作。在 JavaScript 中,拷贝通常涉及基本类型和引用类型的复制。

浅拷贝

浅拷贝会复制对象的基本结构,但是只会复制对象的引用,而不是对象本身。这意味着新对象与原对象共享同一组内部对象。以下是几种常见的浅拷贝方法:

  1. Object.create(obj) :使用 Object.create() 方法基于现有对象创建一个新对象,新对象的原型链指向原对象。
// 原始对象
const obj = {
 name: 'libai',
 age: 30
};

// 浅拷贝对象
const shallowCopy = Object.create(obj);

// 修改浅拷贝的值
shallowCopy.age = 31;

// 输出结果
console.log('Original Object:', obj); // { name: 'libailibai', age: 30 }

  1. Object.assign({}, obj) :使用 Object.assign() 方法将所有可枚举属性的值从一个或多个源对象复制到目标对象,并返回目标对象。
// 原始对象
const obj = {
  name: 'libai',
  age: 30
};
// 浅拷贝对象
const shallowCopy = Object.assign({}, obj);

// 修改浅拷贝的值
shallowCopy.age = 31;

// 输出结果
console.log('Original Object:', obj); // { name: 'libai', age: 30 }
console.log('Shallow Copy:', shallowCopy); // { name: 'libai', age: 31 }
  1. [].concat(arr) :使用数组的 concat() 方法创建一个新数组,将原数组的元素添加到新数组中。
// 原始数组
const arr = [1, 2, 3];

// 浅拷贝数组
const shallowCopy = [].concat(arr);

// 修改浅拷贝的值
shallowCopy[0] = 10;

// 输出结果
console.log('Original Array:', arr); // [1, 2, 3]
console.log('Shallow Copy Array:', shallowCopy); // [10, 2, 3]
  1. 数组解构赋值 [...arr] :通过数组解构赋值创建一个新数组,复制原数组的元素到新数组中。
// 原始数组
const arr = [1, 2, 3];

// 浅拷贝数组
const shallowCopy = [...arr];

// 修改浅拷贝的值
shallowCopy[0] = 10;

// 输出结果
console.log('Original Array:', arr); // [1, 2, 3]
console.log('Shallow Copy Array:', shallowCopy); // [10, 2, 3]

  1. arr.slice() :使用数组的 slice() 方法创建一个新数组,包含原数组的所有元素。
// 原始数组
const arr = [1, 2, 3];

// 浅拷贝数组
const shallowCopy = arr.slice(0);

// 修改浅拷贝的值
shallowCopy[0] = 10;

// 输出结果
console.log('Original Array:', arr); // [1, 2, 3]
console.log('Shallow Copy Array:', shallowCopy); // [10, 2, 3]

  1. arr.reverse().reverse()
    const arr = [1, 2, 3, 4, 5];
    arr.reverse(); // 第一次反转,原数组变为 [5, 4, 3, 2, 1]
    arr.reverse(); // 第二次反转,原数组再次变为 [1, 2, 3, 4, 5]

深拷贝

深拷贝是创建一个新对象,复制原对象所有的属性值,包括嵌套的对象引用,而不是仅复制引用。这样新对象和原对象完全独立,修改新对象不会影响原对象。以下是几种深拷贝方法及其特点:

  1. JSON.parse(JSON.stringify(obj)) :利用 JSON.stringify() 将对象转换为 JSON 字符串,再用 JSON.parse() 将 JSON 字符串解析为新的对象。这种方法简单易行,但有几个限制,如无法处理循环引用、BigInt 类型、函数和 Symbol 属性。
// 原始对象
const obj = {
  name: 'John',
  age: 30,
  address: {
    city: 'New York',
    country: 'USA'
  }
};

// 深拷贝对象
const deepCopy = JSON.parse(JSON.stringify(obj));

// 修改深拷贝的值
deepCopy.age = 31;
deepCopy.address.city = 'San Francisco';

// 输出结果
console.log('Original Object:', obj); // { name: 'John', age: 30, address: { city: 'New York', country: 'USA' } }
console.log('Deep Copy:', deepCopy); // { name: 'John', age: 31, address: { city: 'San Francisco', country: 'USA' } }

  1. 自定义递归方法:可以编写递归函数,深度遍历对象的所有属性,判断属性值是否为对象,如果是则递归调用自身,直到复制完整个对象。这种方法需要小心处理循环引用问题,通常通过记录已经复制过的对象来避免。
// 自定义深拷贝函数
function deepCopy(obj, visited = new WeakMap()) {
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }

  if (visited.has(obj)) {
    return visited.get(obj);
  }

  let newObj = Array.isArray(obj) ? [] : {};
  visited.set(obj, newObj);

  for (let key in obj) {
    if (Object.prototype.hasOwnProperty.call(obj, key)) {
      newObj[key] = deepCopy(obj[key], visited);
    }
  }

  return newObj;
}

// 原始对象
const obj = {
  name: 'John',
  age: 30,
  address: {
    city: 'New York',
    country: 'USA'
  }
};

// 深拷贝对象
const deepCopyObj = deepCopy(obj);

// 修改深拷贝的值
deepCopyObj.age = 31;
deepCopyObj.address.city = 'San Francisco';

// 输出结果
console.log('Original Object:', obj); // { name: 'John', age: 30, address: { city: 'New York', country: 'USA' } }
console.log('Deep Copied Object:', deepCopyObj); // { name: 'John', age: 31, address: { city: 'San Francisco', country: 'USA' } }

  1. 结构化克隆算法 structuredClone(obj) :这是浏览器 API 提供的一种深拷贝算法,用于复制对象及其所有属性。它与 JSON.parse(JSON.stringify(obj)) 类似,但可以处理更多类型的对象,例如循环引用和特殊的 DOM 对象。
// 创建一个对象,包括循环引用和 DOM 对象
const obj = {
  message: 'Hello',
  innerObj: {
    value: 123
  }
};

// 创建循环引用
obj.self = obj;

// 创建一个 DOM 对象
const div = document.createElement('div');
div.textContent = 'DOM Element';

// 添加 DOM 对象到对象中
obj.element = div;

// 使用 structuredClone 复制对象
const clonedObj = structuredClone(obj);

// 修改克隆对象的属性
clonedObj.message = 'Modified';
clonedObj.innerObj.value = 456;

// 输出结果
console.log('Original Object:', obj);
console.log('Cloned Object:', clonedObj);

浅拷贝与深拷贝的区别

  1. 浅拷贝(Shallow Copy)

    • 定义:浅拷贝会创建一个新对象,然后将原始对象的基本数据类型的属性复制到新对象中。对于引用数据类型(如对象或数组),浅拷贝仅复制引用,而不复制引用的对象本身。
    • 影响:新对象与原对象共享相同的引用对象。如果修改新对象的引用对象,原对象也会受到影响,因为它们引用同一个内存地址。

    常见浅拷贝方法

    • Object.assign({}, obj)
    • Object.create(obj)
    • Array.prototype.concat()
    • Array.prototype.slice()
    • 扩展运算符 [...obj] 或 [...arr]
  2. 深拷贝(Deep Copy)

    • 定义:深拷贝创建一个完全独立的新对象,同时递归复制所有的引用类型的属性,使得新对象和原对象的所有嵌套对象也完全独立。
    • 影响:新对象的修改不会影响原对象,因为它们引用的是不同的内存地址。

    常见深拷贝方法

    • JSON.parse(JSON.stringify(obj))(注意该方法有一些限制,如无法处理循环引用、BigInt 类型、函数和 Symbol 属性)
    • 自定义递归函数实现深度复制
    • 结构化克隆算法 structuredClone(obj)(主要用于复制特殊对象,如 DOM 对象)

小结

在 JavaScript 中,拷贝操作涉及到基本类型和引用类型的处理。浅拷贝复制对象的基本结构,但共享内部对象引用,而深拷贝则创建一个完全独立的新对象,包括所有嵌套对象的复制。常见的浅拷贝方法包括 Object.assign、Object.create、数组的 concat 和 slice 方法,以及使用解构赋值。而深拷贝则可以通过 JSON.parse(JSON.stringify(obj))、自定义递归方法或结构化克隆算法实现,每种方法有其适用场景和限制。深浅拷贝的选择取决于需求,理解它们有助于避免意外的对象引用问题和数据共享风险。