Skip to content

引用类型的拷贝

提示

本文回顾了引用类型的特点,并详细介绍了浅拷贝与深拷贝的多种实现方式及其优缺点。

引用类型回顾

在 JavaScript 中,引用类型(如 Object, Array)与基本类型的主要区别在于存储方式:

  • 栈内存:存储引用类型的内存地址(指针)
  • 堆内存:存储引用类型的实际数据(属性和方法)

当我们操作引用类型变量时,实际上是在操作对该对象的引用。因此,直接赋值只会复制内存指针,两个变量将指向同一个堆内存对象,修改其中一个会影响另一个。

为了在不影响原数据的情况下修改数据,我们需要进行拷贝。根据拷贝的层级不同,分为浅拷贝深拷贝

浅拷贝 (Shallow Copy)

浅拷贝创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。

  • 如果属性是基本类型,拷贝的就是基本类型的值。
  • 如果属性是引用类型,拷贝的就是内存地址
  • 结论:修改新对象中的引用类型属性影响源对象。

1. Object.assign()

Object.assign() 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。

js
const obj1 = {
  name: 'fengfeng',
  props: { a: 1 },
}

const obj2 = Object.assign({}, obj1)
obj2.name = '风风'
obj2.props.a++

console.log(obj1) // { name: 'fengfeng', props: { a: 2 } }
console.log(obj2) // { name: '风风', props: { a: 2 } }

2. Array.prototype.concat()

concat() 方法用于合并两个或多个数组,返回一个新数组。

js
const arr1 = [1, 2, 3, [4, 5]]
const arr2 = arr1.concat()

arr2[0] = 'arr2'
arr2[3][0] = 'arr2'

console.log(arr1) // [1, 2, 3, ['arr2', 5]]
console.log(arr2) // ['arr2', 2, 3, ['arr2', 5]]

3. Array.prototype.slice()

slice() 方法返回一个新的数组对象,这一对象是一个由 beginend 决定的原数组的浅拷贝。

js
const arr1 = [1, 2, 3, [4, 5]]
const arr2 = arr1.slice()

arr2[0] = 'arr2'
arr2[3][0] = 'arr2'

console.log(arr1) // [1, 2, 3, ['arr2', 5]]
console.log(arr2) // ['arr2', 2, 3, ['arr2', 5]]

4. 扩展运算符 (Spread Operator)

ES6 提供的扩展运算符 ... 语法简洁,是目前最常用的浅拷贝方式。

对象拷贝

js
const obj1 = {
  name: 'fengfeng',
  props: { a: 1 },
}

const obj2 = { ...obj1 }
obj2.name = '风风'
obj2.props.a++

console.log(obj1) // { name: 'fengfeng', props: { a: 2 } }
console.log(obj2) // { name: '风风', props: { a: 2 } }

数组拷贝

js
const arr1 = [1, 2, 3, [4, 5]]
const arr2 = [...arr1]

arr2[0] = 'arr2'
arr2[3][0] = 'arr2'

console.log(arr1) // [1, 2, 3, ['arr2', 5]]
console.log(arr2) // ['arr2', 2, 3, ['arr2', 5]]

深拷贝 (Deep Copy)

深拷贝是将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象。

  • 结论:修改新对象不会影响原对象。

1. JSON.parse(JSON.stringify())

这是最简单粗暴的深拷贝方法,适用于纯数据对象(Plain Object)。

js
const obj1 = {
  name: 'fengfeng',
  props: { a: 1 },
}

const obj2 = JSON.parse(JSON.stringify(obj1))
obj2.name = '风风'
obj2.props.a++

console.log(obj1) // { name: 'fengfeng', props: { a: 1 } }
console.log(obj2) // { name: '风风', props: { a: 2 } }

局限性

  1. 无法序列化函数undefinedSymbol,这些键值对会被忽略。
  2. 特殊对象丢失细节RegExpErrorSetMap 等仅会序列化可枚举属性(通常变为空对象)。
  3. 日期类型变字符串Date 对象转换后会变成 ISO 格式字符串。
  4. 特殊数值转换NaNInfinity-Infinity 会被转为 null
  5. 不支持循环引用:如果对象引用了自身,会抛出错误。
js
const map = new Map()
map.set(1, 2)

const obj1 = {
  a: undefined,
  b: null,
  c: Symbol(),
  d: NaN,
  e: Infinity,
  f: -Infinity,
  g: map,
  h: new Date(),
  i: () => {},
}

const obj2 = JSON.parse(JSON.stringify(obj1))

console.log(obj2)
/**
 * 输出结果:
 * {
 *   b: null,
 *   d: null,
 *   e: null,
 *   f: null,
 *   g: {},
 *   h: "2023-12-17T02:00:00.000Z"
 * }
 * 注意:a, c, i 丢失了
 */

2. structuredClone (Web API)

HTML 规范标准的 Web API,使用结构化克隆算法进行深拷贝。

js
const original = { name: 'MDN' }
original.itself = original // 循环引用

const clone = structuredClone(original)

console.assert(clone !== original) // true
console.assert(clone.name === 'MDN') // true
console.assert(clone.itself === clone) // true

优点

  • 支持循环引用。
  • 支持 DateSetMapRegExp 等多种内置类型。

局限性

  • 兼容性:虽然现代浏览器支持较好,但在旧环境中需注意。
  • 无法拷贝原型链
  • 无法拷贝函数
  • 不支持 Error 对象。

3. MessageChannel (Hack)

利用 MessageChannel 通信机制实现深拷贝(异步)。

js
function cloneUsingChannel(obj) {
  return new Promise((resolve) => {
    const channel = new MessageChannel()
    channel.port1.onmessage = (e) => resolve(e.data)
    channel.port2.postMessage(obj)
  })
}

// 使用
const obj = { a: 1, b: { c: 2 } }
cloneUsingChannel(obj).then(console.log)

局限性

  • 同样不支持函数,会抛出 DOMException
  • 是异步操作,使用不便。

4. 第三方库 (推荐)

在生产环境中,推荐使用成熟的工具库,它们处理了各种边界情况和兼容性问题。

lodash.cloneDeep

js
import { cloneDeep } from 'lodash-es'

const obj1 = { a: 1, b: { c: 2 } }
const obj2 = cloneDeep(obj1)

jQuery.extend

js
import $ from 'jquery'

const obj2 = $.extend(true, {}, obj1)

5. 手写简易深拷贝 (递归)

面试常考题,实现一个支持数组和对象的简易深拷贝。

js
function deepClone(obj, hash = new WeakMap()) {
  // 1. 处理 null 和非对象
  if (obj === null || typeof obj !== 'object') return obj

  // 2. 处理 Date 和 RegExp
  if (obj instanceof Date) return new Date(obj)
  if (obj instanceof RegExp) return new RegExp(obj)

  // 3. 处理循环引用
  if (hash.has(obj)) return hash.get(obj)

  // 4. 创建新对象/数组
  const cloneObj = new obj.constructor()
  hash.set(obj, cloneObj)

  // 5. 递归拷贝属性
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      cloneObj[key] = deepClone(obj[key], hash)
    }
  }

  return cloneObj
}

如有转载或 CV 的请标注本站原文地址