如何在TypeScript序列化对象时用$ref处理循环引用
TypeScript实现循环引用$ref序列化(重复非循环引用保留原值)
需求回顾
需要实现一个序列化函数,满足:
- 循环引用的对象用
$ref标记,指向其首次出现的JSON Pointer路径 - 非循环的重复引用对象,保留完整的序列化值(不使用
$ref)
输入示例
const obj2 = { circle: {} }; obj2.circle = obj2; const obj3 = { foo: 'bar' }; const obj = { obj3A: obj3, obj3B: obj3, paths: { '/circle': { obj2: {} }, }, }; obj.paths['/circle'].obj2 = obj2;
预期输出
{ "obj3A": { "foo": "bar" }, "obj3B": { "foo": "bar" }, "paths": { "/circle": { "obj2": { "circle": { "$ref": "#/paths/~1circle/obj2" } } } } }
初始实现问题分析
你写的decycle函数存在几个关键问题:
- 语法错误:
weakmap应为TypeScript内置构造函数WeakMap(首字母大写) - 缺少核心的
pathToPointer函数:无法将遍历路径转换为符合规范的JSON Pointer格式 - 逻辑偏差:用单一
WeakMap记录所有已处理对象,导致非循环的重复引用(如obj3)也被替换为$ref,不符合需求
修正后的完整实现
export function decycle(obj: unknown, replacer?: (value: any) => any) { // 记录对象首次出现的JSON Pointer路径 const objPaths = new WeakMap<object, string>(); // 跟踪当前递归栈中的对象,用于识别循环引用 const traversalStack: object[] = []; // 将路径数组转换为符合JSON Pointer规范的字符串 function pathToPointer(path: (string | number)[]): string { if (path.length === 0) return '#'; return '#' + path.map(segment => { return String(segment) .replace(/~/g, '~0') .replace(/\//g, '~1'); }).join('/'); } function derez(value: any, currentPath: (string | number)[]): any { // 应用自定义替换函数(如果有) if (replacer) { value = replacer(value); } // 非对象类型直接返回原始值 if (typeof value !== 'object' || value === null) { return value; } // 检查是否是当前遍历栈中的对象(循环引用) const stackIndex = traversalStack.indexOf(value); if (stackIndex !== -1) { return { $ref: objPaths.get(value)! }; } // 检查是否已处理过该对象(非循环重复引用) const existingPath = objPaths.get(value); if (existingPath) { // 重新序列化对象完整值,不使用$ref if (Array.isArray(value)) { return value.map((item, index) => derez(item, [...currentPath, index])); } const newObj: Record<string, any> = {}; for (const key in value) { if (Object.prototype.hasOwnProperty.call(value, key)) { newObj[key] = derez(value[key], [...currentPath, key]); } } return newObj; } // 首次处理该对象,记录路径并加入遍历栈 const pointer = pathToPointer(currentPath); objPaths.set(value, pointer); traversalStack.push(value); // 递归处理数组或对象的属性 const result: any = Array.isArray(value) ? value.map((item, index) => derez(item, [...currentPath, index])) : {}; if (!Array.isArray(value)) { for (const key in value) { if (Object.prototype.hasOwnProperty.call(value, key)) { result[key] = derez(value[key], [...currentPath, key]); } } } // 处理完成后从栈中移除,避免影响后续遍历 traversalStack.pop(); return result; } return derez(obj, []); }
关键改进点
- 区分循环与非循环引用:
- 用
traversalStack跟踪当前递归链中的对象,仅当对象在栈中时判定为循环引用,返回$ref - 已处理但不在栈中的对象(非循环重复引用),重新序列化完整值
- 用
- 规范JSON Pointer生成:实现
pathToPointer函数,正确转义路径中的/和~字符 - 修复语法错误:修正
WeakMap的大小写问题,符合TypeScript语法规范 - 完善递归栈管理:处理完对象后从栈中弹出,确保后续遍历的正确性
测试验证
用你的输入对象测试:
const obj2 = { circle: {} }; obj2.circle = obj2; const obj3 = { foo: 'bar' }; const obj = { obj3A: obj3, obj3B: obj3, paths: { '/circle': { obj2: {} }, }, }; obj.paths['/circle'].obj2 = obj2; const serialized = decycle(obj); console.log(JSON.stringify(serialized, null, 2));
输出结果与预期完全一致。
内容的提问来源于stack exchange,提问作者Rohit Desai
相关产品推荐
相关产品推荐

