likes
comments
collection
share

JavaScript深拷贝与浅拷贝

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

在JavaScript中,拷贝对象有两种方式:浅拷贝和深拷贝。了解这两种类型的复制之间的差异在许多编程场景中非常重要,因为它们各自有自己的优点和缺点。

什么是浅拷贝和深拷贝

JavaScript深拷贝与浅拷贝
let shallow = ['list', {items: ['item-01', 'item-02', 'item-03']}]
let shallowCopy = Array.from(shallow)

shallowCopy[1].items = []
// shallow: ['list', {items: ['any']}]

shallow[1].items = ['any']
// shallowCopy: ['list', {items: ['any']}]

这就是浅拷贝:拷贝的新对象发生改变,源对象也会发生改变;源对象发生改变,新对象也会发生改变。引用MDN的描述:

对象的浅拷贝是其属性与拷贝源对象的属性共享相同引用(指向相同的底层值)的副本。因此,当你更改源或副本时,也可能导致其他对象也发生更改——也就是说,你可能会无意中对源或副本造成意料之外的更改。这种行为与深拷贝的行为形成对比,在深拷贝中,源和副本是完全独立的。

JavaScript深拷贝与浅拷贝
let deep = ['list', {items: ['item-01', 'item-02', 'item-03']}]
let deepCopy = JSON.parse(JSON.stringify(deep))

deepCopy[1].items = []
// deep: ['list', {items: ['any']}]

deep[1].items = ['any']
// deepCopy: ['list', {items: []}]

这就是深拷贝:拷贝的新对象发生改变,源对象不会发生改变;源对象发生改变,新对象也不会发生改变。引用MDN的描述:

对象的深拷贝是指其属性与其拷贝的源对象的属性不共享相同的引用(指向相同的底层值)的副本。因此,当你更改源或副本时,可以确保不会导致其他对象也发生更改;也就是说,你不会无意中对源或副本造成意料之外的更改。这种行为与浅拷贝的行为形成对比,在浅拷贝中,对源或副本的更改可能也会导致其他对象的更改(因为两个对象共享相同的引用)。

浅拷贝是对原始对象的引用,所有的副本都是指向同一块内存区域,修改副本对象也会同步到原始对象,拷贝速度快;深拷存储新的对象的副本,修改副本对象不会影响原始对象,拷贝速度慢。

数组的浅拷贝与深拷贝

// 数据
let primitives = [undefined, null, 'string', 1, true, BigInt(2), Symbol()]
let objects = [{[Symbol()]: 'object'}, ['array']]
  • 数组的内置方法

Array.from 方法:

// Array.from -> 浅拷贝
let arrayFromCopyPrimitve = Array.from(primitives)
let arrayFromCopyObject = Array.from(objects)

arrayFromCopyPrimitve[0] = 'void'
// primitives: [undefined, null, 'string', 1000, true, 2000n, Symbol()]

arrayFromCopyObject[1][0] = 'any'
// objects: [{key: 'unknown'}, ['any']]

objects[0].key = 'unknown'
// arrayFromCopyPrimitve: ['void', null, 'string', 1000, true, 2000n, Symbol()]
// arrayFromCopyObject: [{key: 'unknown'}, ['any']]

Array.prototype.concat 方法:

// Array.prototype.concat -> 浅拷贝
let concatArray = objects.concat(primitives)

concatArray[0] = 'void'
// primitives: [undefined, null, 'string', 1000, true, 2000n, Symbol()]

concatArray[1][0] = 'any'
// objects: [{key: 'unknown'}, ['any']]

objects[0].key = 'unknown'
// concatArray: ['void', ['any'], undefined, null, 'string', 1000, ...]

Array.prototype.slice 方法:

// Array.prototype.slice -> 浅拷贝
let sliceArray = objects.slice()

sliceArray[1][0] = 'any'
// objects: [{key: 'unknown'}, ['any']]

objects[0].key = 'unknown'
// sliceArray: [{key: 'unknown'}, ['any']]

Array.prototype.filter 方法:

// Array.prototype.filter -> 浅拷贝
let filterArray = objects.filter(() => true)

filterArray[1][0] = 'any'
// objects: [{key: 'unknown'}, ['any']]

objects[0].key = 'unknown'
// filterArray: [{key: 'unknown'}, ['any']]
  • 遍历

Array.prototype.map 方法:

// 浅拷贝
let shallowArray = objects.map(v => v)

shallowArray[1][0] = 'any'
// objects: [{key: 'unknown'}, ['any']]

objects[0].key = 'unknown'
// shallowArray: [{key: 'unknown'}, ['any']]

// 深拷贝
let deepArray = objects.map(v => Object.assign({}, v))

shallowArray[1][0] = 'any'
// objects: [{key: 'unknown'}, ['array']]

objects[0].key = 'unknown'
// shallowArray: [{key: 'object'}, ['any']]

for 循环:

// 浅拷贝
let shallowArray = []
for (let i = 0; i < objects.length; i++) {
  shallowArray[i] = objects[i]
}

shallowArray[1][0] = 'any'
// objects: [{key: 'unknown'}, ['any']]

objects[0].key = 'unknown'
// shallowArray: [{key: 'unknown'}, ['any']]

// 深拷贝
let deepArray = []
for (let i = 0; i < objects.length; i++) {
  deepArray[i] = Object.assign({}, objects[i])
}

deepArray[1][0] = 'any'
// objects: [{key: 'unknown'}, ['array']]

objects[0].key = 'unknown'
// deepArray: [{key: 'object'}, ['any']]
  • JSON转换
const jsonArray = JSON.parse(JSON.stringify(objects))

jsonArray[1][0] = 'any'
// objects: [{key: 'unknown'}, ['array']]

objects[0].key = 'unknown'
// jsonArray: [{key: 'object'}, ['any']]
  • 扩展运算符
// [...] -> 浅拷贝
const spreadArray = [...objects]

spreadArray[1][0] = 'any'
// objects: [{key: 'unknown'}, ['any']]

objects[0].key = 'unknown'
// spreadArray: [{key: 'unknown'}, ['any']]

从上面可以得出,数组的内置方法(包括扩展运算符)都是浅拷贝,而遍历和JSON转换使用新的内存区域深拷贝才得以实现。

对象的浅拷贝与深拷贝

// 数据
let first = {a: {b: {c: 'd'}}, e: 'f'}
let second = {h: {i: 'j'}, k: 'l'}
  • 对象的内置方法
// Object.assign -> 浅拷贝
let assignObj = Object.assign({}, first, second)

assignObj.h.i = 'x'
// first: { a: { b: { c: 'd' } }, e: 'f' }
// second: { h: { i: 'x' }, k: 'l' }

second.k = {}
// assignObj: { a: { b: { c: 'd' } }, e: 'f', h: { i: 'x' }, k: 'l' }
  • 遍历
function type(data) {
  return Reflect.toString.call(data).replace(/.+[ ](.+)]$/g, '$1').toLowerCase()
}

function copy(target, source = {}) {
  let iterator = []

  if (Array.isArray(target)) {
    iterator = target.entries()
  } else {
    if (typeof (target) === 'object') {
      iterator = Object.entries(target)
    }
  }

  for (const [k, v] of iterator) {
    if (v && typeof v === 'object') {
      source[k] = copy(v, source[k])
    } else {
      source[k] = v
    }
  }

  return source
}

const traversalObj = copy(second)

traversalObj.h.i = 'x'
// second: { h: { i: 'j' }, k: {} }

second.k = {}
// traversalObj: { h: { i: 'x' }, k: 'l' }
  • JSON转换
const jsonObj = JSON.parse(JSON.stringify(second))

jsonObj.h.i = 'x'
// second: { h: { i: 'j' }, k: {} }

second.k = {}
// jsonObj: { h: { i: 'x' }, k: 'l' }
  • 扩展运算符(...
// {...} -> 浅拷贝
const spreadObj = { ...second }

spreadObj.h.i = 'x'
// second: { h: { i: 'x' }, k: {} }

second.k = {}
// spreadObj: { h: { i: 'x' }, k: 'l' }

从上面可以得出,对象的内置方法(包括扩展运算符)也都是浅拷贝,而遍历和JSON转换使用新的内存区域深拷贝才得以实现。

总结

JavaScript深拷贝与浅拷贝
  1. 对象(数组)的内置方法都是浅拷贝,引用MDN的描述:

在 JavaScript 中,所有标准的内置对象复制操作(展开语法、Array.prototype.concat()Array.prototype.slice()Array.from()Object.assign()Object.create())创建的是浅拷贝而不是深拷贝。

  1. 通过测试可以看出,基本类型的数据不会相互影响。换句话说,如果对象(数组)的第一层全是基本类型,那么使用其中任何一种方法都是深拷贝。
  2. 如果需要实现深拷贝就需要使用新的变量去接收数据,得到新的内存区域分配。
  3. JSON转换是将对象转为字符串然后再解析出来,也就是说它存在局限性,比如对象中存在函数。另外,大量的数据转换在性能上也存在一定隐患。
  4. 什么时候深拷贝取决于要不要影响对应的数据,使用哪种手段进行深拷贝取决于对象的复杂度。

注:文章中的深拷贝方法并不完善,仅为此处测试用。

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