深拷贝与浅拷贝:详解 JavaScript 中的数据拷贝机制

343 阅读7分钟

前言

相信小伙伴在开发过程中或多或少都曾遇到过类似这样的问题:将变量A赋值给变量B后,修改A(B)的值导致B(A)的值也随之发生了更改。

原因

上面问题的发生并不是必然的。JS中数据的拷贝主要关注两个方面:

  • 数据类型
  • 拷贝方式

我们来回顾一下JS中的数据类型JS基本数据类型和引用数据类型

在 JavaScript 中,数据类型可以分为基本数据类型引用数据类型两大类。

  1. 基本数据类型 基本类型包括:字符串、布尔值、数字、undefined、null、symbol。 基本类型的存储方式:基本类型以name-value的方式,直接存储在  中。 副作用:基本数据类型的存储是相互独立的,拷贝时栈内存会新开辟一个内存,不同数据之间没有直接的联系,修改不同互相拷贝的变量值也不会对彼此造成影响。
  2. 引用数据类型 引用数据类型包括:数组、对象、Date、RegExp、函数、特殊的基本包装类型(String、Number、Boolean)以及单体内置对象(Global、Math)。 引用数据类型的存储方式:以name-address-value的方式,变量名name)存在“”内存中,value)存在“”内存中,但是 “栈” 内存会提供一个 “引用地址(address)” 指向 “堆” 内存中的值。 副作用:当我们操作引用类型的变量时,实际上是操作它们的引用,而不是值本身。更改指向同一地址的变量值时,会导致相互影响。

这也就解释了最开始我们提到的现象,在A引用数据类型的情况下,进行B=A这样的直接赋值去进行拷贝时,其实复制的是 A引用地址,而并非堆里面的值。当修改B(A)的值时,由于AB指向的是同一个地址,所以自然A(B)也受了影响。

深拷贝与浅拷贝

通过上面的分析我们得知,基本数据类型的拷贝都是“深拷贝”,复制时会开辟新的独立变量内存存储,不会互相影响。因此我们这里的深拷贝浅拷贝只针对引用数据类型而言。

深拷贝与浅拷贝的区别

引用类型的深拷贝浅拷贝的区别如下:

  • 引用类型的浅拷贝:拷贝的仅仅是“引用地址”,两个对象的引用地址对应的还是同一个值,所以,无论改变哪个对象的值,另一个对象对应的值也会改变。
  • 引用类型的深拷贝:把引用地址和值一起拷贝过来,一个对象的值改变,另一个对象的值不受影响。

实现浅拷贝的几种方法(含ES6)

说明:这里将不完全能够深拷贝(部分深拷贝)的方法归类到浅拷贝方法中,如Object.assign() ,请注意甄别

1、直接赋值

直接赋值是最常见的拷贝方法之一,也是我们开头所举的例子。

const original = {
  name: '张三',
  age: 18
};

// 直接赋值
const copy = original;

// 修改拷贝对象中的嵌套属性
copy.name = '李四';

console.log(original.name); // 输出 '李四'
console.log(copy.name);     // 输出 '李四'

2、 使用 Object.assign() 进行浅拷贝

Object.assign() 是 JavaScript 提供的一个常用方法,它将一个或多个源对象的所有可枚举属性复制到目标对象中。

拷贝规则

  • 对于基本属性类型的属性,Object.assign()是深拷贝
  • 对于引用类型的属性,Object.assign() 只会复制其引用,为浅拷贝

简单来说,拷贝的目标数据如果只有一层对象,则为深拷贝;如果对象里面还有对象(多层嵌套),则里层对象仍为浅拷贝。

示例如下:

(1)一层无嵌套对象
// 一层无嵌套对象
const original = {
  name: '张三',
  age: 18
};

// 深拷贝
const copy = Object.assign({}, original);

// 修改拷贝对象中的属性
copy.name = '李四';

// 不影响原数据
console.log(original.name); // 输出 '张三'
console.log(copy.name);     // 输出 '李四'
(2)多层嵌套对象
const original = {
 name: '张三',
 age: 18,
 more: {
   hobby: '篮球',
   address: '翻斗花园'
 }
};

// 浅拷贝
const copy = Object.assign({}, original);

// 修改拷贝对象中的嵌套属性
copy.more.hobby = 'rap';

// 影响了原数据
console.log(original.more.hobby); // 输出 'rap'
console.log(copy.more.hobby);     // 输出 'rap'

解释:虽然 originalcopy 是两个不同的对象,但它们的 more 属性指向同一个对象。因此,修改 copy.more.hobby 会影响到 original.more.hobby解决办法:对里层引用数据类型再次使用相同方法拷贝。

const original = {
  name: '张三',
  age: 18,
  more: {
    hobby: '篮球',
    address: '翻斗花园'
  }
};

// 浅拷贝
const copy = Object.assign({}, original);

// 对嵌套对象进行深拷贝 
copy.more = Object.assign({}, original.more);

// 修改拷贝对象中的嵌套属性
copy.more.hobby = 'rap';

// 影响了原数据
console.log(original.more.hobby); // 输出 '篮球'
console.log(copy.more.hobby);     // 输出 'rap'

3、 使用展开运算符(...)进行浅拷贝:

ES6 引入的展开运算符(...)也可以用来实现引用数据类型的浅拷贝。 拷贝规则同上述提到的Object.assign(),这里不再赘述。

示例如下:

(1)一层无嵌套对象
// 一层无嵌套对象
const original = {
 name: '张三',
 age: 18
};

// 深拷贝
const copy = { ...original };

// 修改拷贝对象中的属性
copy.name = '李四';

// 不影响原数据
console.log(original.name); // 输出 '张三'
console.log(copy.name);     // 输出 '李四'
(2)多层嵌套对象
const original = {
 name: '张三',
 age: 18,
 more: {
   hobby: '篮球',
   address: '翻斗花园'
 }
};

// 浅拷贝
const copy = { ...original };

// 修改拷贝对象中的嵌套属性
copy.more.hobby = 'rap';

// 影响了原数据
console.log(original.more.hobby); // 输出 'rap'
console.log(copy.more.hobby);     // 输出 'rap'

解释:虽然 originalcopy 是两个不同的对象,但它们的 more 属性指向同一个对象。因此,修改 copy.more.hobby 会影响到 original.more.hobby解决办法:对里层引用数据类型再次使用相同方法拷贝。

const original = {
  name: '张三',
  age: 18,
  more: {
    hobby: '篮球',
    address: '翻斗花园'
  }
};

// 对嵌套对象进行深拷贝 
const copy = { ...original , more: { ...original.more } };

// 修改拷贝对象中的嵌套属性
copy.more.hobby = 'rap';

// 影响了原数据
console.log(original.more.hobby); // 输出 '篮球'
console.log(copy.more.hobby);     // 输出 'rap'

4、数组使用数组方法进行深拷贝(concat、slice)

这里同上面对象的两种方法类似,一层为深拷贝,多层为浅拷贝,这里不再赘述,使用方法请自行查询。

实现深拷贝的几种方法

1、使用 JSON.parse() 和 JSON.stringify() 实现深拷贝

一种常见的简便方式是使用 JSON.stringify() 将对象序列化为 JSON 字符串,然后使用 JSON.parse()解析为一个新的对象。

局限性
  • 无法处理非JSON结构的引用数据类型,如函数、数组、日期对象等。
  • 当对象中的属性值为 undefined 时,执行 JSON.stringify(obj) ,这些属性会被忽略,因此最终的 JSON 字符串中不会包含这些属性。
示例
const original = {
  name: '张三',
  age: 18,
  more: {
    hobby: '篮球',
    address: '翻斗花园'
  }
};

// 深拷贝 
const copy = JSON.parse(JSON.stringify(original));

// 修改拷贝对象中的嵌套属性
copy.more.hobby = 'rap';

// 影响了原数据
console.log(original.more.hobby); // 输出 '篮球'
console.log(copy.more.hobby);     // 输出 'rap'

2、使用递归实现深拷贝

比较全面的解决方案,使用递归的方式来复制对象,手动实现更灵活、不受限制的深拷贝。

缺点是稍微繁琐,手动造轮子

示例
function deepCopy(obj) {
  if (obj === null || typeof obj !== 'object') {
    return obj; // 基本数据类型直接返回
  }

  // 创建一个新的对象或数组
  const copy = Array.isArray(obj) ? [] : {};

  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      copy[key] = deepCopy(obj[key]); // 递归拷贝
    }
  }

  return copy;
}

const original = {
  name: '张三',
  more: {
    address: '翻斗花园',
    school: '大肥羊学校'
  },
  hobbies: ['篮球', 'rap']
};

const copy = deepCopy(original);

// 修改拷贝对象中的嵌套属性
copy.more.address = '花果山';
copy.hobbies[0] = '唱跳';

console.log(original.more.address); // 输出 '翻斗花园'
console.log(copy.more.address);     // 输出 '花果山'

console.log(original.hobbies[0]); // 输出 '篮球'
console.log(copy.hobbies[0]);     // 输出 '唱跳'

3、使用第三方库lodash实现深拷贝

使用lodashcloneDeep函数进行深拷贝。

示例如下:

import cloneDeep from 'lodash/cloneDeep';

const obj = { a: 1, b: { c: 2 } };
const newObj = cloneDeep(obj);

缺点:

  • 需要引入第三方库,在某些不需要额外其他功能模块的情况下,可能手动实现是更好的选择。

最后

  1. 了解内部的数据拷贝机制有助于更好的在不同场景下应用不同的解决方案
  2. 没有百分百完美的解决方法,根据需求进行相应选择,避免踩坑是第一位
  3. 本文梳理了JS中数据拷贝的基础原理与常用的解决方法,希望对你有帮助