必须用 Reflect.ownKeys,因为它能同时返回字符串和 Symbol 类型的自有属性键并保持插入顺序;Object.keys 和 getOwnPropertyNames 忽略 Symbol,for...in 遍历原型链且不保证 Symbol 出现。

要实现支持 Symbol 键名的深度优先对象遍历引擎,核心在于用 Reflect.ownKeys 替代 Object.keys 或 for...in —— 因为它能同时返回字符串和 Symbol 类型的自有属性键,且保持插入顺序(包括 Symbol)。
为什么必须用 Reflect.ownKeys?
Object.keys 和 Object.getOwnPropertyNames 都会忽略 Symbol 键;for...in 还会遍历原型链上的可枚举属性,且不保证 Symbol 出现。而 Reflect.ownKeys(obj) 返回一个包含所有自有属性键(字符串 + Symbol)的数组,且遵循规范定义的枚举顺序:先按创建顺序列出所有整数索引字符串键,再按插入顺序列出其余字符串键,最后按插入顺序列出 Symbol 键。这对确定性遍历至关重要。
基础深度优先遍历结构
采用递归栈或显式栈均可。推荐显式栈(避免调用栈溢出),每项存目标对象及其父路径(便于调试/定位)。关键点:
- 对每个对象,先调用
Reflect.ownKeys(obj)获取完整键列表 - 逐个处理键:用
Object.getOwnPropertyDescriptor(obj, key)可选判断是否为 accessor,但非必需 - 对值为普通对象(
obj !== null && typeof obj === 'object' && !Array.isArray(obj))且未被循环引用时,压入栈继续遍历 - Symbol 键直接参与遍历,无需特殊转换——只要键存在,就能用
obj[key]访问对应值
处理循环引用与边界情况
深度遍历中对象重复出现会导致无限循环。需记录已访问对象的引用(WeakMap 最佳):
- 初始化
const visited = new WeakMap() - 每次准备遍历新对象前,检查
visited.has(obj);若存在,跳过该分支 - 进入遍历前执行
visited.set(obj, true) - 注意:WeakMap 只接受对象为键,天然排除原始值,也避免内存泄漏
- 其他边界:null、undefined、Date、RegExp、Map、Set 等应视为叶子节点,不再展开(除非业务明确需要)
可扩展的遍历回调设计
让引擎支持用户自定义行为,例如收集 Symbol 键、修改值、中断遍历等。建议提供统一的 entry 回调函数,参数为:{ object, key, value, path, depth },其中 path 是从根开始的键路径数组(如 ['a', Symbol('id'), 0])。
- 回调返回
false表示跳过该键的子遍历(但当前项仍算访问) - 返回
undefined或真值则继续深入 - Symbol 键在
path中保持原样(即Symbol('x')),不转成字符串,便于精准识别 - 可额外暴露
isSymbolKey: typeof key === 'symbol'字段,降低用户判断成本

















