首页 > web前端 > js教程 > 正文

JS 深拷贝实现方案对比 - 处理循环引用的结构化克隆算法解析

夢幻星辰
发布: 2025-09-19 23:55:01
原创
861人浏览过

js 深拷贝实现方案对比 - 处理循环引用的结构化克隆算法解析

在JavaScript中实现深拷贝,尤其当数据结构中存在循环引用时,这可不是个小问题。说实话,刚接触深拷贝的时候,

JSON.parse(JSON.stringify(obj))
登录后复制
简直是我的救星,简单粗暴。但现实往往不那么理想,一旦遇到函数、
undefined
登录后复制
,或者最麻烦的——循环引用,它就直接罢工了。这时,真正的解决方案浮出水面,那就是基于结构化克隆算法(Structured Clone Algorithm)的实现,它能优雅地处理这些复杂场景,尤其是循环引用,而不会陷入无限递归的泥潭。

解决方案

处理带有循环引用的JavaScript对象深拷贝,最可靠且现代的方案是利用浏览器或Node.js环境内置的结构化克隆算法。这个算法是HTML标准的一部分,它被用于多种场景,比如通过

postMessage
登录后复制
传递数据,或者在
IndexedDB
登录后复制
中存储数据。现在,我们可以直接通过
structuredClone()
登录后复制
API 来调用它。

它的核心机制在于,在克隆过程中维护一个内部映射(通常是原对象到克隆对象的映射),当算法遍历到某个对象时,它会先检查这个对象是否已经被访问过并正在克隆中。如果发现已经存在于映射中,它就不会再次克隆,而是直接返回之前克隆好的对应实例,从而有效切断循环引用,避免溢出。

这个算法不仅能处理循环引用,还能正确克隆许多内置类型,比如

Date
登录后复制
RegExp
登录后复制
Map
登录后复制
Set
登录后复制
ArrayBuffer
登录后复制
TypedArray
登录后复制
Blob
登录后复制
File
登录后复制
ImageData
登录后复制
等。它不会克隆函数、
Symbol
登录后复制
值、DOM节点或错误对象,这些在克隆时会被忽略或抛出错误。对于大多数数据结构深拷贝的需求,
structuredClone()
登录后复制
提供了一个高效、安全且标准化的途径。

为什么
JSON.parse(JSON.stringify(obj))
登录后复制
不足以应对复杂的深拷贝场景?

其实,很多开发者,包括我自己在内,初期都非常依赖

JSON.parse(JSON.stringify(obj))
登录后复制
来进行深拷贝。它确实简单,对于只包含基本数据类型(字符串、数字、布尔值、
null
登录后复制
)和纯粹的数组、对象的数据结构,效果拔群。但它有几个致命的缺陷,让它在面对真实世界的复杂数据时显得力不从心。

首先,它无法处理函数

undefined
登录后复制
Symbol
登录后复制
类型。当
JSON.stringify
登录后复制
遇到这些类型时,函数和
undefined
登录后复制
会直接被忽略(如果它们是对象属性值),
Symbol
登录后复制
值则会使整个属性被跳过。这意味着这些数据会悄无声息地丢失。

其次,日期对象会被转换成ISO格式的字符串,而不是保持为一个

Date
登录后复制
对象。这通常需要手动解析和转换,增加了额外的工作量。
RegExp
登录后复制
对象则会变成一个空对象
{}
登录后复制

最关键的一点,也是标题中强调的,它无法处理循环引用。如果你的对象结构中存在 A 引用 B,B 又引用 A 的情况,

JSON.stringify
登录后复制
会直接抛出一个
TypeError: Converting circular structure to JSON
登录后复制
的错误,程序直接崩溃。这在构建复杂的数据模型,比如图结构、链表或者一些相互关联的配置对象时,是极其常见的。所以,虽然它方便,但其适用范围非常有限,不适合作为通用的深拷贝方案。

如果
structuredClone
登录后复制
不适用,如何手动实现一个能处理循环引用的深拷贝函数?

尽管

structuredClone
登录后复制
API 已经非常强大且推荐使用,但在一些特定场景,比如旧版浏览器环境、需要对某些特定类型进行特殊处理(比如深拷贝函数或特定DOM节点),或者仅仅是为了理解其内部机制,我们可能需要手动实现一个深拷贝函数。实现一个能处理循环引用的深拷贝,关键在于维护一个映射表,记录已克隆的对象。

以下是一个简化版的实现思路,核心是利用

WeakMap
登录后复制
来存储原对象和其对应的克隆对象,以此来检测并处理循环引用:

FineVoice语音克隆
FineVoice语音克隆

免费在线语音克隆,1 分钟克隆你的声音,保留口音和所有细微差别。

FineVoice语音克隆 61
查看详情 FineVoice语音克隆
function deepCloneManual(obj, map = 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 (map.has(obj)) {
        return map.get(obj);
    }

    // 4. 根据对象类型创建新的容器(数组或普通对象)
    const clone = Array.isArray(obj) ? [] : {};

    // 5. 存储映射:在递归克隆属性之前,将原对象和新克隆对象存入 WeakMap
    //    这一步至关重要,它确保了在处理循环引用时能返回正确的克隆实例
    map.set(obj, clone);

    // 6. 递归克隆对象的属性或数组的元素
    for (const key in obj) {
        // 确保只处理对象自身的属性,而不是原型链上的属性
        if (Object.prototype.hasOwnProperty.call(obj, key)) {
            clone[key] = deepCloneManual(obj[key], map);
        }
    }

    return clone;
}

// 示例用法:
// const obj = {};
// const a = { name: 'A' };
// const b = { name: 'B' };
// a.b = b;
// b.a = a; // 循环引用
// obj.a = a;
//
// const clonedObj = deepCloneManual(obj);
// console.log(clonedObj);
// console.log(clonedObj.a.b.a === clonedObj.a); // true,表示循环引用被正确处理
登录后复制

这个

deepCloneManual
登录后复制
函数的思路是:当遇到一个对象时,首先检查它是否已经被克隆过(通过
map.has(obj)
登录后复制
)。如果是,就直接返回之前克隆好的版本,避免无限递归。如果还没有克隆,就创建一个新的空对象或数组作为克隆结果,并立即将原对象和这个空克隆结果存入
Map
登录后复制
中。这样,在后续递归克隆其属性时,如果再次遇到这个原对象(即循环引用),就能从
Map
登录后复制
中取出已经创建的克隆结果,而不是重新开始克隆,从而打破循环。

需要注意的是,这个手动实现比

structuredClone
登录后复制
简单得多,它没有处理
Map
登录后复制
Set
登录后复制
ArrayBuffer
登录后复制
等复杂类型,也没有处理属性描述符、原型链等更高级的细节。它更多是展示处理循环引用的核心逻辑。

在实际开发中,何时选择
structuredClone
登录后复制
,何时考虑自定义实现?

选择深拷贝方案时,我们总要权衡便利性、性能、兼容性和功能覆盖。

优先选择

structuredClone()
登录后复制
API:

在绝大多数现代Web应用和Node.js环境中,

structuredClone()
登录后复制
应该是你的首选。

  • 性能优异: 它是浏览器或Node.js环境的原生实现,通常用C++等底层语言编写,性能远超JavaScript层面的手动实现。
  • 功能全面: 除了处理循环引用,它还能正确克隆许多复杂的内置对象类型(如
    Map
    登录后复制
    ,
    Set
    登录后复制
    ,
    ArrayBuffer
    登录后复制
    ,
    ImageData
    登录后复制
    等),这些是手动实现起来非常麻烦的。
  • 简洁易用: 单行代码即可完成深拷贝,无需引入第三方库或编写复杂逻辑。
  • 标准规范: 作为Web标准的一部分,其行为是可预测且一致的。

考虑自定义实现或第三方库:

尽管

structuredClone()
登录后复制
功能强大,但它也有其局限性,这时你可能需要考虑自定义实现或引入像
lodash.clonedeep
登录后复制
这样的第三方库:

  • 不支持克隆函数、
    Symbol
    登录后复制
    、DOM节点或错误对象:
    如果你的深拷贝需求包含这些类型,
    structuredClone()
    登录后复制
    会忽略它们或抛出错误。例如,你可能需要一个能深拷贝包含回调函数的配置对象的方案。
  • 兼容性要求: 如果你的项目需要支持非常老旧的浏览器环境(例如IE),
    structuredClone()
    登录后复制
    可能不可用。虽然可以引入Polyfill,但如果只是一小部分功能需要深拷贝,自定义实现可能更轻量。
  • 特定场景的性能优化: 在极其注重性能的场景下,并且你对数据结构有非常深入的了解,一个高度定制化的深拷贝函数可能能通过避免不必要的检查和克隆来达到更高的效率。但这通常意味着更高的开发和维护成本。
  • 控制原型链或属性描述符:
    structuredClone()
    登录后复制
    不会保留原型链,也不会复制属性描述符(如
    writable
    登录后复制
    ,
    enumerable
    登录后复制
    )。如果你需要深拷贝对象并保留这些元数据,则必须自定义实现。

总的来说,默认情况下,请使用

structuredClone()
登录后复制
。只有当你明确知道
structuredClone()
登录后复制
的局限性与你的需求冲突,并且这些冲突无法通过改变数据结构来规避时,才考虑编写自定义的深拷贝函数或引入成熟的第三方库。这种决策应基于对项目需求、目标环境和性能要求的全面评估。

以上就是JS 深拷贝实现方案对比 - 处理循环引用的结构化克隆算法解析的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号