You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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函数存在几个关键问题:

  1. 语法错误:weakmap应为TypeScript内置构造函数WeakMap(首字母大写)
  2. 缺少核心的pathToPointer函数:无法将遍历路径转换为符合规范的JSON Pointer格式
  3. 逻辑偏差:用单一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, []);
}

关键改进点

  1. 区分循环与非循环引用:
    • 用traversalStack跟踪当前递归链中的对象,仅当对象在栈中时判定为循环引用,返回$ref
    • 已处理但不在栈中的对象(非循环重复引用),重新序列化完整值
  2. 规范JSON Pointer生成:实现pathToPointer函数,正确转义路径中的/和~字符
  3. 修复语法错误:修正WeakMap的大小写问题,符合TypeScript语法规范
  4. 完善递归栈管理:处理完对象后从栈中弹出,确保后续遍历的正确性

测试验证

用你的输入对象测试:

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 12:25:00