Vue3 实战:解决深拷贝导致的“Maximum call stack size”与“函数丢失”问题
在 Vue3 项目开发中,我们经常会遇到需要深度克隆(Deep Clone)响应式对象的情况。传统的 lodash.cloneDeep 或 JSON.parse(JSON.stringify()) 在面对复杂业务场景时,经常会触发以下两个痛点:
- RangeError: Maximum call stack size exceeded:由于对象中存在循环引用(Circular Reference)或嵌套过深。
- 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)。
