ATRULE 技术博客ATRULE 技术博客
首页
博客
文档
关于
首页
博客
文档
关于
  • 技术文章

    • VuePress 2.x 完全指南
    • Gemini3 & GPT5 & DeepSeek3:AI时代程序员的身份转变
    • Agent 架构选型指南:单智能体 vs 多智能体
    • Agent 工程师转型学习路线图 (Full-Stack to Agent Engineer)
    • Agent 工程师学习笔记
    • 📖 Agent 系统架构与工程实践面试速记卡片
    • Agent 流式对话前端踩坑记录
    • RAG 检索工程问题记录
    • Agent 后端问题记录(Koa / MongoDB)
    • LLM 调用与 Agent 编排问题记录
    • Electron 桌面端问题排查记录
    • AI Agent 全栈开发问题排查记录
    • ES2026 与 TypeScript 进阶问题记录
    • 浏览器渲染与 V8 内存问题记录
    • React 19 与 Vue 4 新内核问题记录
    • 前端工程化与微前端问题记录
    • 性能、安全与可观测性问题记录
    • 前端架构实战问题记录
  • 项目实战

    • Node.js + Koa 抖音直播弹幕Agent 五大核心模块(面试项目完整版,TS技术栈)★★★★★
    • Poiclaw 项目蓝图:自主编程实体
    • AI旅行助手

Vue3 实战:解决深拷贝导致的“Maximum call stack size”与“函数丢失”问题

在 Vue3 项目开发中,我们经常会遇到需要深度克隆(Deep Clone)响应式对象的情况。传统的 lodash.cloneDeep 或 JSON.parse(JSON.stringify()) 在面对复杂业务场景时,经常会触发以下两个痛点:

  1. RangeError: Maximum call stack size exceeded:由于对象中存在循环引用(Circular Reference)或嵌套过深。
  2. TypeError: xxx is not a function:克隆后的对象丢失了原对象原型链上的方法(例如第三方插件实例的方法)。

本文将分享一个高度自定义的 deepSafeClone 工具函数,支持手动脱壳(toRaw)、防循环引用、以及原型方法自动补全。

核心代码实现

你可以将以下代码直接复制到你的工具类文件(如 utils/clone.js)中:

JavaScript

/**
 * 深度安全克隆 (支持 Vue3 toRaw、防循环引用、保留原型方法)
 * @param {Object} obj - 要克隆的目标对象
 * @param {Function} myRaw - 外部传入的脱壳函数 (如 Vue3 的 toRaw)
 * @param {WeakMap} hash - 用于处理循环引用,内部递归使用
 */
export function deepSafeClone(obj, myRaw = (v) => v, hash = new WeakMap()) {
  // 1. 处理基本类型、null 以及 undefined
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }

  // 2. 防御循环引用:如果该对象已经处理过,直接返回对应的副本
  // 解决 "Maximum call stack size exceeded" 的核心逻辑
  if (hash.has(obj)) {
    return hash.get(obj);
  }

  // 3. 使用传入的 myRaw 函数脱掉响应式外壳 (防止 Proxy 递归导致的溢出)
  const rawObj = myRaw(obj);

  // 4. 特殊内置对象处理
  if (rawObj instanceof Date) return new Date(rawObj);
  if (rawObj instanceof RegExp) return new RegExp(rawObj.source, rawObj.flags);

  // 5. 函数处理:保持引用以防止 "is not a function" 报错
  if (typeof rawObj === 'function') {
    return rawObj;
  }

  // 6. 安全过滤:跳过 DOM 节点和 Window 等全局对象
  if (rawObj instanceof Node || rawObj === window) {
    return undefined;
  }

  // 7. 初始化容器 (区分数组和对象)
  const clone = Array.isArray(rawObj) ? [] : {};
  
  // 记录引用映射
  hash.set(obj, clone);

  // 8. 深度递归遍历自身属性
  const keys = Object.keys(rawObj);
  for (const key of keys) {
    try {
      const value = rawObj[key];
      // 递归调用,持续透传 myRaw 和 hash
      clone[key] = deepSafeClone(value, myRaw, hash);
    } catch (err) {
      console.warn(`[DeepClone] 属性 ${key} 读取失败,已跳过:`, err);
    }
  }

  // 9. 原型链补全:解决某些实例方法(如 getCurrentState)丢失的问题
  if (!Array.isArray(rawObj)) {
    const proto = Object.getPrototypeOf(rawObj);
    // 如果原型上存在业务方法,且当前克隆对象没有,则进行挂载
    if (proto && proto !== Object.prototype) {
      // 针对特定方法的补齐逻辑
      if (typeof proto.getCurrentState === 'function' && !clone.getCurrentState) {
        // 使用 bind 确保调用时 this 指向正确
        clone.getCurrentState = proto.getCurrentState.bind(rawObj);
      }
      
      /* // 如果需要更激进的原型恢复,可以开启下方代码:
      Object.setPrototypeOf(clone, proto); 
      */
    }
  }

  return clone;
}

在 Vue3 中的使用方式

JavaScript

import { toRaw } from 'vue';
import { deepSafeClone } from '@/utils/clone';

const originalData = { /* 响应式数据或复杂实例 */ };

// 传入 toRaw 作为第二个参数,确保彻底切断响应式追踪
const newCopy = deepSafeClone(originalData, toRaw);

// 此时 newCopy.getCurrentState() 可以正常调用

为什么这个版本更强大?

1. 彻底解决递归溢出

通过 WeakMap 记录每一个处理过的对象地址。当递归遇到循环引用时,直接从 Map 中取出已有的副本,不会陷入无限循环。

2. 解决 Vue3 Proxy 干扰

Vue3 的响应式对象是 Proxy。在深拷贝时,如果直接操作 Proxy,可能会触发多余的 getter。通过参数化传入 toRaw,我们在每一层递归都优先获取原始数据,既提高了性能又增加了稳定性。

3. 原型方法“精准降级”

很多第三方库的对象(如编辑器、地图、图表配置)将方法定义在原型上。普通的克隆会只拷贝数据而丢失方法。我们在函数末尾加入了原型检查,发现 getCurrentState 等关键函数缺失时,会自动通过 .bind() 重新挂载,确保克隆后的对象“功能完备”。


希望这篇文章能帮你解决深拷贝的各种坑!如果有更复杂的实例克隆需求,欢迎在评论区讨论。


💡 小提示:如果你的业务场景中,getCurrentState 内部需要修改克隆后的数据,建议将 .bind(rawObj) 改为 .bind(clone)。

最后更新: 2026/3/23 20:48