什么!你居然还分不清深拷贝和浅拷贝

58 阅读5分钟

前言

在JavaScript开发中,我们经常会遇到这样的场景:你需要基于一个已有的对象创建一个新对象,但又不希望新对象的修改影响到原对象。这就好比制作一份文档的副本——你想在新副本上自由编辑,同时保留原始版本不变。

今天,我们就来深入探讨JavaScript中的拷贝技术,从简单的"影分身"(浅拷贝)到完美的"克隆人"(深拷贝),理解它们的原理、适用场景和实现方法。

废话不多说先看一段代码

const arr = ['a', 'b', 'c', 'd', {age: 18}]
const arr2 = arr.slice(0)
arr[4].age = 20
console.log(arr2);//输出[ 'a', 'b', 'c', 'd', { age: 20 } ]

仔细看你可能会有一个疑问,为什么arr的改变为什么会导致arr2也改变呢? 这时就得提到引用类型的地址引用了。

一、原始类型和引用类型

  • 原始类型(简单类型)
  1. string (字符串)
  2. number (数字)
  3. boolean (布尔)
  4. undefined
  5. null
  6. symbol 定义一个独一无二的值
  7. bigInt 大整型

原始类型的拷贝也叫值拷贝,可以直接将值存入调用栈中。原件的改变不会影响复印件

  • 引用类型(复杂类型)
  1. 数组 -- 线性存储结构,靠下标来操作里面的元素
  2. 对象 -- 键值对存储结构,通过访问属性来操作里面的值
  3. 函数

引用类型的拷贝是将引用地址拷贝到对象当中,在调用栈中引用类型无法直接存入栈中只能通过引用地址传递,当原件被修改时复印件也会被修改。

二、浅拷贝:对象的"影分身"

什么是浅拷贝?

浅拷贝就像创建对象的"影分身"——外表看起来一模一样,但内在的灵魂(嵌套对象)却是共享的。当你修改原对象的嵌套属性时,新对象也会随之改变。简单来说就是副本会受原件的改变而改变。

浅拷贝的常用方法

1. 数组的浅拷贝

1.1 slice
const arr = ['a', 'b', 'c', 'd', {age: 18}]
const arr2 = arr.slice(0)
arr[4].age = 20
console.log(arr2);//输出[ 'a', 'b', 'c', 'd', { age: 20 } ]

slice(0)将原数组全部赋值一份,连同嵌套对象的引用地址一并复制

1.2 [...arr]

const a = [1, 2, {age: 18}]
let c = [...a]
a[2].age = 19

console.log(c);//输出[1, 2, {age: 19}]

1.3 [].concat(arr)

const a = [1, 2, 3, {age: 18}]
const d = [].concat(a)
a[3].age = 20

console.log(d);//输出[ 1, 2, 3, { age: 20 } ]

[].concat(arr)是将前面[]中的数组与后面arr拼接到一起的方法

2. 对象的浅拷贝

Object.assign()

const obj = {
  name: '俊杰',
  like: ['泡脚']
}
const newObj = Object.assign({}, obj)
obj.like[0] = '台球'

console.log(newObj);//输出{ name: '俊杰', like: [ '台球' ] }

assign就是将两个对象合并到一个对象当中。

浅拷贝的局限性

浅拷贝只复制了对象的第一层属性,对于嵌套的对象,它复制的是引用(内存地址),而不是实际的值。这就像你复制了一个房子的蓝图,但房子里面的家具还是原来那套。

二、深拷贝:创建完美的"克隆人"

什么是深拷贝?

深拷贝是真正的"克隆人"——从外到内,完全独立的全新个体。无论你如何修改原对象,新对象都不会受到任何影响。

深拷贝的常用方法

方法1:)structuredClone(现代浏览器API)

const obj = {
  name: '俊杰',
  age: 18,
  like: {
    n: '洗脚',
    m: '台球'
  },
  a: 123n,
  say() {
    console.log('hello');
  }
}

const newObj = structuredClone(obj)
obj.like.m = '蓝球'

console.log(newObj);//structuredClone不能拷贝函数

image.png 使用structuredClone时我们得注意:structuredClone不能拷贝函数

方法2:JSON方法(最常用但有缺陷)

const obj = {
  name: '俊杰',
  age: 18,
  like: {
    n: '洗脚',
    m: '台球'
  },
  say() {
    console.log('hello');
  },
  a: undefined,
  b: null,
  c: NaN,
  d: Infinity
}

const oo = JSON.parse(JSON.stringify(obj))
obj.like.m = '篮球'

console.log(oo);
// 输出{
//   name: '俊杰',
//   age: 18,
//   like: { n: '洗脚', m: '台球' },
//   b: null,
//   c: null,
//   d: null
// }

缺点就是JSON会直接忽视函数,undefined,并且将null, NaN,Infinity转化为null

方法3:手写递归实现

在深拷贝时可能会遇到嵌套非常深的对象中的对象中的对象...,因此手写递归实现深拷贝将层层嵌套全部遍历是个面试时必须会的方法

const obj = {
  name: '俊杰',
  age: 18,
  like: {
    n: '洗脚',
    m: '台球'
  }
}

function deepClone(obj) {
  let o = {}
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      if (typeof(obj[key]) == 'object' && obj[key] !== null) {
        const childObj = deepClone(obj[key])
        o[key] = childObj
      } else {
        o[key] = obj[key]
      }
      
    }
  }
  return o
}

const newObj = deepClone(obj)
obj.like.m = '篮球'

console.log(newObj);

选择指南:何时使用哪种拷贝?

场景推荐方法原因
简单的对象/数组,没有嵌套浅拷贝 (...Object.assign())性能最好,代码简洁
需要序列化的数据,不含函数/特殊值JSON.parse(JSON.stringify())简单快速,兼容性好
现代浏览器,需要拷贝特殊对象structuredClone()原生API,支持更多类型
复杂对象,需要完整拷贝Lodash的_.cloneDeep()功能最全,最可靠
学习/自定义需求手写递归实现理解原理,灵活定制

三、总结

在JavaScript中,拷贝是一个看似简单实则复杂的话题。选择正确的拷贝策略需要综合考虑:

  1. 数据结构复杂度:简单的结构用浅拷贝,复杂的嵌套结构用深拷贝
  2. 数据类型要求:是否包含函数、Date、正则等特殊类型
  3. 性能要求:大数据量时需要考虑拷贝性能
  4. 环境限制:是否需要考虑浏览器兼容性
  5. 循环引用:对象是否包含循环引用

记住这个简单的决策流程:

  • 如果只需要第一层独立 → 选择浅拷贝
  • 如果需要完全独立且数据可序列化 → 选择JSON方法
  • 如果需要完整拷贝且环境支持 → 选择structuredClone
  • 如果需要最可靠的完整拷贝 → 选择Lodash等库
  • 如果追求极致性能或特殊需求 → 考虑手写实现

拷贝不仅是技术,更是一种编程哲学。理解数据的不变性和引用关系,能帮助你写出更健壮、更可预测的代码。希望这篇"影分身"到"克隆人"的旅程,能让你在JavaScript的数据处理中游刃有余!