JSON.parse深拷贝JS数组的局限性与替代方案

作者:小括号时间:2026-11-03 11:23:24浏览:443

深入剖析使用 JSON.parse(JSON.stringify()) 深拷贝 JavaScript 数组时面临的 5 大致命局限性,包括 Date、RegExp、undefined、NaN、循环引用等数据丢失与崩溃问题,并全面解读原生 structuredClone、Lodash 及手写递归等现代替代方案与工程选型依据。

在日常 JavaScript 开发中,使用 JSON.parse(JSON.stringify(array)) 进行数组深拷贝几乎成了条件反射式的简写方案。然而,这行看似便利的代码实际上是将数据临时压入 JSON 文本传输协议后再反序列化,并非真正的对象堆内存复制。它存在着特殊值静默转为 null、内置对象类型退化、集合丢失以及遭遇循环引用直接导致运行时报错崩溃等致命隐患。本文将详细拆解该方案的底层缺陷,并深入介绍现代规范推荐的 structuredClone()、生产级类库以及特定场景的手写递归替代方案。
JSON.parse深拷贝JS数组的局限性与替代方案
图 0:JSON 序列化深拷贝数组的核心矛盾与现代化演进概览

一、JSON.parse 深拷贝数组的运作原理与“便捷表象”

深拷贝的核心目标是让新生成的数组与原数组在内存堆中彻底切断引用关系。对多维数组或包含嵌套对象的数组,浅拷贝方法(如 [...arr]、Array.from(arr) 或 arr.slice())只能复制顶层元素指针,内层嵌套的对象依然指向同一块内存空间。

为了阻断内部引用的关联,许多开发者习惯调用:

const clonedArray = JSON.parse(JSON.stringify(originalArray));

该方法的工作流非常朴素:

  1. 序列化阶段:引擎递归遍历原数组,将其中的属性和值按照 JSON 规范拼接为符合标准的 UTF-16 文本字符串;
  2. 反序列化阶段:引擎重新词法分析该字符串,开辟全新的内存地址生成对应的数据结构。

如果数组内只包含严格意义上的 JSON 基础数据类型(即数字、普通字符串、布尔值、null 以及仅由这些基础键值组成的普通字面量对象),该方法确实能够输出一份崭新、互不干扰的独立副本。然而,真实工程中的 JavaScript 数据结构远比纯 JSON 丰富,一旦触及稍微复杂一些的数据类型,问题就会接踵而至。

二、深入剖析:JSON.parse 拷贝数组的 5 大致命局限性

由于 JSON 是一种跨语言的数据交换格式,它的类型系统只是 JavaScript 类型系统的极简子集。当 JavaScript 数组中存在 JSON 规范不支持的类型时,JSON.stringify() 不得不采取静默退化、剔除或抛错等策略。

JSON.parse深拷贝JS数组的局限性与替代方案
图 1:JSON.parse 序列化流程中的底层链路与数据畸变全景

1. 特殊数据类型静默转为 null(undefined、Function、Symbol)

在纯对象属性中,包含 undefined、函数或 Symbol 的键值对会在序列化时直接被省略忽略;但在数组中,为了维持数组索引下标的连续性与原本长度,JSON 规范强制将其全部转换为 null。

const rawList = [
  100,
  undefined,
  function calc() { return 42; },
  Symbol('id')
];

const copyList = JSON.parse(JSON.stringify(rawList));

// 打印比较结果
console.log('原始数组:', rawList);
// [100, undefined, [Function: calc], Symbol(id)]

console.log('克隆数组:', copyList);
// [100, null, null, null]  <-- 函数与 undefined 全部沦为 null,导致后续调用报错!

如果业务逻辑依赖 arr[2]() 执行回调,或者依靠 arr[1] === undefined 校验参数默认值,这种静默转换将直接导致 TypeError: arr[2] is not a function 隐蔽崩溃。

2. 内置对象类型严重退化(Date 沦为字符串,RegExp 沦为空对象)

JavaScript 中的核心内置对象在经过 JSON 管道后,原本的类原型方法会全部蒸发:

  • Date 实例:Date 原型链内置了 toJSON() 方法,序列化后会输出形如 "2026-10-31T04:22:00.000Z" 的 ISO 字符串。当使用 JSON.parse 读回时,它只会成为一个普通字符串,无法再调用 .getFullYear()、.getTime() 等日期方法;
  • RegExp 与 Error 实例:正则表达式对象与错误对象序列化后会变成空对象 {},原有的正则表达式规则、flags 或错误信息全部丢失。
const items = [
  new Date('2026-10-31'),
  /^[a-z]+$/gi,
  new Error('Network timeout')
];

const clonedItems = JSON.parse(JSON.stringify(items));

console.log(typeof clonedItems[0]); // "string" (不再是 Date 实例!)
console.log(clonedItems[1]);          // {} (正则表达式规则全部被抹除)
console.log(clonedItems[2]);          // {} (异常堆栈清空)

3. 特殊数值与非有限值失真(NaN、Infinity 全部变成 null)

在标准 JSON 规范中,数值只支持有限十进制数。因此,JavaScript 语言中的 NaN、Infinity 以及 -Infinity 在序列化数组时均会被当作非数值,统一编码为 null。

const metrics = [NaN, Infinity, -Infinity, 0];
const clonedMetrics = JSON.parse(JSON.stringify(metrics));

console.log(clonedMetrics);
// 输出示例: [null, null, null, 0]
// 原本表达“非数值”或“无穷大”的计算状态被篡改为“空指针”,后续运算完全失真

4. ES6+ 现代集合数据结构彻底失效(Map 与 Set)

现代前端开发大量采用 Map 和 Set 处理高频键值检索与去重集合。然而,JSON.stringify 并不能理解迭代器协议与集合数据,遇到 Map 或 Set 时仅将其作为无自身可枚举属性的普通对象处理,最终输出 {}。

const complexList = [
  new Set([1, 2, 3]),
  new Map([['admin', true]])
];

const parsedList = JSON.parse(JSON.stringify(complexList));

console.log(parsedList[0]); // {} (Set 内部数据全部蒸发)
console.log(parsedList[1]); // {} (Map 键值全失)

5. 遭遇循环引用直接抛错中断,无法容错

当数组内包含循环引用(例如某个子对象持有了数组自身的引用,或者两个元素之间相互引用)时,JSON.stringify 会陷入无限递归。为了保护调用栈,引擎会直接抛出不可恢复的运行时异常:

const circularArr = ['task-alpha'];
circularArr.push(circularArr); // 数组自身形成了环

try {
  JSON.parse(JSON.stringify(circularArr));
} catch (err) {
  console.error(err.message);
  // 示例输出: TypeError: Converting circular structure to JSON
}
危险告警:循环引用引发的 TypeError 是无条件的致命错误。如果你的数据来自复杂业务树状图、UI 节点树或包含反向指针的父子数据,使用 JSON 深拷贝会导致整个服务进程或前端渲染流程瞬间崩溃。

三、现代原生标准解决方案:structuredClone()

为了彻底根治深拷贝的历史难题,WHATWG HTML 标准联合 ECMAScript 推出了官方内置的 structuredClone() 接口。目前该 API 已在所有现代运行环境中原生支持(Node.js 17+、Deno、Chrome 98+、Firefox 94+、Safari 15.4+)。

JSON.parse深拷贝JS数组的局限性与替代方案
图 2:structuredClone 结构化克隆算法底层对象图与映射表机制

1. structuredClone 的实战表现

structuredClone() 底层直接采用浏览器与 Node 引擎内置的 C++ 级别“结构化克隆算法”,无需在文本与对象之间反复转译,具备如下核心优势:

  • 完美保持类型实例:Date 复制后仍是可调用的 Date 实例;RegExp 仍保留修饰符与规则;
  • 原生支持复杂集合:Map、Set、ArrayBuffer、TypedArray 等均能无损克隆;
  • 原生支持循环引用:内部通过 Identity Map 记录已克隆对象内存指针,遇环时自动绑定正确的新内存节点,安全不抛错;
  • 精确保留特殊原语:undefined 与 NaN 严格保留,绝不会扭曲为 null。
// 验证 structuredClone 对复杂数组的无损拷贝能力
const richList = [
  new Date('2026-10-31'),
  /^[a-z]+$/gi,
  new Set(['apple', 'banana']),
  new Map([['status', 200]]),
  undefined,
  NaN
];
// 构造循环引用
richList.push(richList);

// 执行现代原生深拷贝
const clonedRichList = structuredClone(richList);

console.log(clonedRichList[0] instanceof Date);   // true (保留Date方法)
console.log(clonedRichList[1] instanceof RegExp); // true (保留正则规则)
console.log(clonedRichList[2].has('apple'));       // true (Set集合完好)
console.log(clonedRichList[4] === undefined);     // true (未变null)
console.log(clonedRichList[6] === clonedRichList); // true (环形拓扑完美维持,内存完全解耦)
使用限制:structuredClone 虽强大,但规范明确规定它不能克隆 Function(函数)、DOM 节点以及原型的 getter/setter。如果遇到函数,它会主动抛出 DataCloneError。

四、生产级备选方案:Lodash cloneDeep 与手写带 WeakMap 递归

在需要兼容旧版本浏览器、Node.js 16 以下旧服务,或者需要特殊策略处理函数拷贝时,通常有两种可靠方案。

方案 A:成熟工业级类库 Lodash 的 cloneDeep

对于已有大型工程,Lodash 的 _.cloneDeep 经过了多年工业界检验。它不仅能够处理环形引用与内置对象,当遇到包含函数的对象或自定义类实例时,它会保留函数的引用绑定而不会直接崩溃。

// 环境依赖: npm install lodash
const cloneDeep = require('lodash/cloneDeep');

const enterpriseData = [
  { id: 1, action: () => console.log('ok') },
  new Date()
];

const safeCopy = cloneDeep(enterpriseData);
// 既保留了数组内对象的深层独立性,又允许函数安全存在

方案 B:轻量级手写深拷贝实现(带 WeakMap 防循环引用)

如果你在严控打包体积的小程序、微前端或独立工具库中工作,不希望为了一个方法引入额外的 npm 依赖,可以使用以下完整的递归深拷贝模板。该模板核心采用 WeakMap 作为访问缓存,有效防止循环引用造成的栈溢出:

/**
 * 生产级轻量深拷贝函数 (支持数组、对象、Date、RegExp、Set、Map 及循环引用)
 * @param {*} target - 需要拷贝的目标
 * @param {WeakMap} hash - 引用记忆缓存
 * @returns {*} 深拷贝后的新对象
 */
function deepClone(target, hash = new WeakMap()) {
  // 1. 处理 null 或基础标量类型
  if (target === null || typeof target !== 'object') {
    return target;
  }

  // 2. 处理特殊内置对象类型
  if (target instanceof Date) return new Date(target.getTime());
  if (target instanceof RegExp) return new RegExp(target.source, target.flags);

  // 3. 处理循环引用,查表直接返回
  if (hash.has(target)) {
    return hash.get(target);
  }

  // 4. 处理 Set 集合
  if (target instanceof Set) {
    const newSet = new Set();
    hash.set(target, newSet);
    target.forEach(val => newSet.add(deepClone(val, hash)));
    return newSet;
  }

  // 5. 处理 Map 集合
  if (target instanceof Map) {
    const newMap = new Map();
    hash.set(target, newMap);
    target.forEach((val, key) => newMap.set(key, deepClone(val, hash)));
    return newMap;
  }

  // 6. 处理数组或普通对象
  const cloneTarget = Array.isArray(target) ? [] : Object.create(Object.getPrototypeOf(target));
  hash.set(target, cloneTarget);

  // 遍历所有自身属性 (包括 Symbol 属性键)
  const keys = [...Object.keys(target), ...Object.getOwnPropertySymbols(target)];
  for (const key of keys) {
    cloneTarget[key] = deepClone(target[key], hash);
  }

  return cloneTarget;
}

五、四大深拷贝方案能力矩阵横向对比

综合上述解析,我们将常见方案在核心指标上的支持能力整理成直观矩阵,便于在架构评审与代码重构时核对:

JSON.parse深拷贝JS数组的局限性与替代方案
图 3:四大主流 JavaScript 深拷贝技术能力矩阵全景对比
对比维度 JSON.parse(JSON.stringify) structuredClone() Lodash cloneDeep 手写递归 (带 WeakMap)
纯基础类型数组 完全支持 完全支持 完全支持 完全支持
Date / RegExp 变异为字串 / 空对象 原生保留完整实例 完整实例克隆 支持(需显式分支)
undefined / NaN 全部扭曲为 null 无损保留原始值 无损保留原始值 无损保留原始值
Map / Set 结构 变为空对象 {} 原生完整深拷贝 完整递归深拷贝 支持(需递归遍历)
循环引用容错 抛出 TypeError 崩溃 原生解构拓扑结构 内部栈缓存安全还原 WeakMap 查表安全还原
函数 / 原型方法 转为 null 抛 DataCloneError 共享函数指针引用 按需保留或拷贝
外部依赖与体积 零依赖(极轻) 零依赖(现代标准内置) 第三方库(~24KB) 零依赖(~30行代码)

六、工程实践中的技术选型指南

在实际项目研发中,不应盲目一刀切,应根据执行环境与具体数据形态选择最具性价比的方案。

JSON.parse深拷贝JS数组的局限性与替代方案
图 4:现代 JavaScript 数组深拷贝决策流与选型路径

选型落地三法则:

  1. 现代工程首选 structuredClone(): 在以 Node.js 18+ 或现代浏览器为主的现代前端/全栈项目中,只要数据不含函数,均应将原生 structuredClone() 作为深拷贝的第一首选。它运行在引擎底层,无打包体积开销,执行速度与安全性俱佳。
  2. 含函数或需兼容旧环境时使用 Lodash cloneDeep: 如果工程运行在老旧浏览器或容器(如 Node 14/16、老版 WebView),或者数组的子对象中挂载了事件处理器、工具方法,选择工业级的 Lodash 能够提供最稳健的兼容底线。
  3. 纯 JSON 数据的简易脚本方可使用 JSON.parse: 仅在一次性爬虫脚本、纯配置文件读写,且能 100% 确保数据均为基础标量的前提下,才可出于极简目的使用 JSON.parse(JSON.stringify())。在严肃的核心业务代码中,应全面淘汰这一存在隐患的写法。
作者最新文章
相关文章
更多