logo
Published on

JavaScript中深拷贝对象的最有效方法

Authors
  • Name
    Twitter

在JavaScript编程中,经常需要对对象进行深拷贝操作。深拷贝指的是完整复制对象及其嵌套对象,确保无副作用地修改克隆对象。本文将介绍几种在JavaScript中实现深拷贝的方法,并特别强调structuredClone函数的使用及其在不同环境中的支持情况。

使用结构化克隆(Structured Cloning)

2022年更新: structuredClone全局函数已经在Firefox 94、Node 17和Deno 1.14中可用。

HTML标准中包含了内部结构化克隆/序列化算法,可以创建对象的深度克隆。这种方法扩展了JSON的支持范围,不仅支持JSON支持的类型,还支持DateRegExpMapSetBlobFileListImageData、稀疏数组、TypedArray等类型,并且未来可能支持更多类型。而且,它能在克隆数据中保留引用,从而支持循环和递归结构,这在使用JSON时会出错。

在Node.js中的支持:

structuredClone全局函数已经在Node 17.0中可以使用:

const clone = structuredClone(original)

对于之前的版本,Node.js中的v8模块(从Node 11开始)直接暴露了结构化序列化API,但此功能仍被标记为“实验性”并可能会在未来版本中更改或移除。如果您正在使用兼容版本,克隆对象的方法如下:

const v8 = require('v8')

const structuredClone = (obj) => {
  return v8.deserialize(v8.serialize(obj))
}

浏览器中的直接支持:已在Firefox 94中实现

structuredClone全局函数将会在所有主流浏览器中提供支持(在whatwg/html#793 on GitHub中进行了讨论)。其用法如下:

const clone = structuredClone(original)

在这一功能发布之前,浏览器的结构化克隆实现只能间接使用。

异步解决方案:可用解决方案

利用已有API创建结构化克隆的低开销方法是通过一个MessageChannel的两端进行数据传递。另一端会发出一个message事件,其中包含克隆数据。然而,监听这些事件必须是异步的,同步替代方案的实用性较差。

class StructuredCloner {
  constructor() {
    this.pendingClones_ = new Map()
    this.nextKey_ = 0

    const channel = new MessageChannel()
    this.inPort_ = channel.port1
    this.outPort_ = channel.port2

    this.outPort_.onmessage = ({ data: { key, value } }) => {
      const resolve = this.pendingClones_.get(key)
      resolve(value)
      this.pendingClones_.delete(key)
    }
    this.outPort_.start()
  }

  cloneAsync(value) {
    return new Promise((resolve) => {
      const key = this.nextKey_++
      this.pendingClones_.set(key, resolve)
      this.inPort_.postMessage({ key, value })
    })
  }
}

const structuredCloneAsync = (window.structuredCloneAsync =
  StructuredCloner.prototype.cloneAsync.bind(new StructuredCloner()))

示例用法:

const main = async () => {
  const original = { date: new Date(), number: Math.random() }
  original.self = original

  const clone = await structuredCloneAsync(original)

  // 它们是不同的对象:
  console.assert(original !== clone)
  console.assert(original.date !== clone.date)

  // 它们是循环引用的:
  console.assert(original.self === original)
  console.assert(clone.self === clone)

  // 它们包含等效的值:
  console.assert(original.number === clone.number)
  console.assert(Number(original.date) === Number(clone.date))

  console.log('断言完成。')
}

main()

同步解决方案:糟糕的选择

没有好的方法可以同步创建结构化克隆。这儿有几种不实用的黑客技巧。

history.pushState()history.replaceState()都可以创建其第一个参数的结构化克隆,并将该值赋给history.state。可以这样创建一个对象的结构化克隆:

const structuredClone = (obj) => {
  const oldState = history.state
  history.replaceState(obj, null)
  const clonedObj = history.state
  history.replaceState(oldState, null)
  return clonedObj
}

示例用法:

尽管这是同步的,但此方法可能非常慢。它会产生与操作浏览器历史记录相关的所有开销。重复调用此方法可能导致Chrome暂时无响应。

Notification构造函数可以创建其关联数据的结构化克隆。它也会尝试向用户展示浏览器通知,但如果您没有请求通知权限,则该尝试会静默失败。假如您出于其他目的已经具有权限,我们会立即关闭创建的通知。

const structuredClone = (obj) => {
  const n = new Notification('', { data: obj, silent: true })
  n.onshow = n.close.bind(n)
  return n.data
}

以上就是几种在JavaScript中进行深拷贝的方法。structuredClone全局函数无疑是最便捷和高效的选择,其在未来的广泛支持将使开发者更轻松地实现对象的深度克隆。