Setup

()
首页博客あさき命MomentLive Log关于我数据统计
首页
首页博客あさき命MomentLive Log关于我数据统计

应用

GITADORA Skill RecorderFog of World HelperDTX Player宝可梦个体/努力值倒推
© 2014-2026Sean白熱All Rights Reserved.

在 JavaScript 中实现拷贝

发布2023年7月25日
更新2026年8月15日
阅读时间4 min
分类Dev/开发
标签#浅拷贝#深拷贝#结构化克隆#JSON.stringify#JSON.parse#JavaScript#structuredClone

在 JavaScript 里谈“拷贝”,其实是在讨论“引用”。拷贝不是复制一个值,而是决定谁拥有那份数据的控制权。

按引用传递

在 JavaScript 中,数据总是通过引用传递:当把一个变量传给函数或赋值给另一个变量时,实际上传递的是内存地址,而不是数据本身。这个行为被称为“按引用传递”(pass-by-reference)。

问题也由此产生:当我们修改传入的数据时,原始数据会被改变。

javascript
function modifyArray(arr) {  arr.push(4)}const originalArray = [1, 2, 3]modifyArray(originalArray)console.log(originalArray) // 输出 [1, 2, 3, 4]

当然,对于字符串、数字、布尔值等基本类型而言,它们是不可变的,修改变量并不会影响原始数据。但对象和数组就不一样了。

所以我们才会需要“拷贝”,来让修改发生在新的副本上。

浅拷贝 Shallow copy

浅拷贝只复制一层数据,嵌套对象或数组仍然是引用。因此浅拷贝适用于“没有深层嵌套”的结构。

常见方式包括:

Object.assign

javascript
const sourceObject = { a: 1, b: { c: 2 } }const targetObject = Object.assign({}, sourceObject)console.log(sourceObject === targetObject) // 输出 false

Array.prototype.slice

javascript
const sourceArray = [1, 2, 3, { a: 4 }]const targetArray = sourceArray.slice()console.log(sourceArray === targetArray) // 输出 false

Array.prototype.concat

javascript
const sourceArray = [1, 2, 3, { a: 4 }]const targetArray = sourceArray.concat()console.log(sourceArray === targetArray) // 输出 false

Spread Operator

javascript
const sourceArray = [1, 2, 3, { a: 4 }]const targetArray = [...sourceArray]console.log(sourceArray === targetArray) // 输出 falseconst sourceObject = { a: 1, b: { c: 2 } }const targetObject = { ...sourceObject }console.log(sourceObject === targetObject) // 输出 false

如果数据存在多层嵌套,浅拷贝就会暴露问题,这也是我们需要深拷贝的原因。

JSON.stringify / JSON.parse

最常见的“深拷贝”方式,是通过 JSON.stringify 序列化,再 JSON.parse 反序列化,产生新对象副本:

javascript
const originalObject = { a: 1, b: { c: 2 } }const deepCopiedObject = JSON.parse(JSON.stringify(originalObject))console.log(originalObject === deepCopiedObject) // 输出 false

它确实简单,但有明显限制:

  1. 不支持特殊对象和属性: JSON.stringify 无法序列化函数、undefined、Symbol、Infinity、Map 等对象或属性,反序列化时会丢失或变成 null。
  2. 无法处理循环引用: 如果对象存在循环引用,JSON.stringify 会抛出 TypeError。
  3. 函数和原型链丢失: JSON 字符串只包含数据,不包含方法与原型信息。

对于大对象而言,性能也可能成为隐忧。不过在多数场景下,它已经足够使用。

深拷贝 Deep copy

深拷贝会递归复制所有嵌套对象与数组,期间可以对数据类型做判断和处理,确保副本与原对象完全独立。下面是一个简单示例:

javascript
function deepCopy(obj) {  if (typeof obj !== 'object' || obj === null) {    return obj  }  let copy  if (Array.isArray(obj)) {    copy = []    for (let i = 0; i < obj.length; i++) {      copy[i] = deepCopy(obj[i])    }  } else {    copy = {}    for (const key in obj) {      if (Object.prototype.hasOwnProperty.call(obj, key)) {        copy[key] = deepCopy(obj[key])      }    }  }  return copy}

但循环引用依旧是难题:

javascript
const objA = {}objA.b = objA // 循环引用// 递归深拷贝会陷入无限递归,导致栈溢出deepCopy(objA)

要处理这些边界条件,需要更复杂的实现,这也是 lodash 等库要做的工作。

结构化克隆 Structured Clone

很多人不知道,其实浏览器早已内置了一种强大的拷贝方式:结构化克隆(structured clone)。History API 的 history.replaceState 就是它的典型应用。

History API

javascript
const objA = {}objA.b = objA // 循环引用history.replaceState(objA, document.title)const objB = history.stateconsole.log(objB === objA) // 输出 false

这是个“古老”的方法可以实现对循环引用的对象的深拷贝,但是部分浏览器(比如 Safari)中存在调用限制,它并不是一个完美的方案,我们再来看两个例子。

MessageChannel

javascript
const objA = {}objA.b = objA // 循环引用const { port1, port2 } = new MessageChannel()port2.onmessage = (event) => {  const objB = event.data  console.log(objB === objA) // 输出 false}port1.postMessage(objA)

MessageChannel 也可以实现对循环引用的对象的深拷贝,但拷贝的结果是异步返回的 🤔

Notification API

javascript
const objA = {}objA.b = objA // 循环引用const notification = new Notification('', { data: objA })const objB = notification.data

Structured Clone API

类似的“hack 手段”其实还有,不过他们底层都是基于前文所说的结构化克隆算法实现的,你可以简单地理解为浏览器曾经为了自身对深拷贝复杂对象的需要,而实现了一个内置的深拷贝方法。

现在 structuredClone 作为一个标准的 API,已经被列入了 HTML Standard 中,它的使用方式也很简单:

javascript
structuredClone(value)structuredClone(value, { transfer })

我们可以不费吹灰之力地获得一个对象副本:

javascript
const objA = { c: new Map() }objA.b = objA // 循环引用const objB = structuredClone(objA)console.log(objB === objA) // 输出 false

但它也并非万能,对 Symbol、函数对象、DOM 对象等仍然无能为力,强行调用会抛出 DOMException。

transfer 参数用于指定需要“转移”的可转移对象,这些对象不会被复制,而是被移动到新对象中:

javascript
// 创建一个8MB的“文件”并填充它. 8MB = 1024 * 1024 * 8 Bconst uInt8Array = new Uint8Array(1024 * 1024 * 8).map((v, i) => i)console.log(uInt8Array.byteLength) // 8388608const clonedArray = structuredClone(uInt8Array)console.log(uInt8Array.byteLength) // 0console.log(clonedArray.byteLength) // 8388608

总结

浅拷贝胜在轻便,深拷贝追求完整,结构化克隆则是现代浏览器的“硬实力”。选哪一种并没有标准答案,取决于你的数据结构与目标场景。

如果你想继续深入:

  • Deep-copying in JavaScript
  • The structured clone algorithm
  • HTML Standard 2.7 Safe passing of structured data

相关文章

Dev/开发

CSS Custom Highlight API 的机制与实践

不改写 DOM 结构,直接在渲染层实现文本高亮与标注。

2026年2月27日

Dev/开发

使用 Promise.try() 优雅地同时处理同步与异步错误

Just try it.

2026年2月25日

Dev/开发

同形异码:Unicode 正规化、编码与文件系统的隐形分岔

从 URL 编码到文件系统,解释为何“看起来相同”的字符串会走向不同字节路径。

2026年2月14日
上海草莓共振场 Day 3 半场 @回响之地 Music Park通过 wbr 标签实现更好的换行控制

评论…