Reflect.ownKeys() 获取对象所有自有属性键名含Symbol

作者:技术拾光时间:2026-06-29 08:27:02浏览:225

先说个结论:在获取对象所有自有属性键这一件事上,Reflect.ownKeys() 确实是最完整的手段之一。它返回的是字符串和 Symbol 的混合数组,按创建顺序排列,既不管属性到底是可枚举还是不可枚举,实质上就是 Object.getOwnPropertyNames() 和 Object.get

先说个结论:在获取对象所有自有属性键这一件事上,Reflect.ownKeys() 确实是最完整的手段之一。它返回的是字符串和 Symbol 的混合数组,按创建顺序排列,既不管属性到底是可枚举还是不可枚举,实质上就是 Object.getOwnPropertyNames() 和 Object.getOwnPropertySymbols() 两个方法的并集。

Reflect.ownKeys() 获取对象所有自有属性键名含Symbol

Reflect.ownKeys() 确实能拿到 Symbol 键,但需确保 Symbol 是“自有”的

该方法的返回结果能否包含 Symbol 类型的键?答案是肯定的,但需要满足一个前提:这个 Symbol 键必须直接定义在目标对象上,而不是从原型链上继承来的。例如,通过 obj[Symbol('a')] = 1 这种方式定义的键一定会出现在结果中;而像 Object.prototype[Symbol.toStringTag] 这种原型链上的属性,则不属于自有属性,不会出现在结果里。

一个常见的误解是:有人以为 Reflect.ownKeys() 会一股脑返回所有 Symbol,包括全局注册的,甚至内置的。实际上,它只关心“自有属性中的字符串和 Symbol”,无论可枚举与否都会收录。换句话说,它完全等价于 Object.getOwnPropertyNames() + Object.getOwnPropertySymbols() 的组合。

  • 返回值类型为 Array,顺序清晰:先列出所有字符串键(按创建顺序),再列出所有 Symbol 键(同样按创建顺序)。
  • 如果同一个对象上既有 Symbol.for('x') 又有 Symbol('x'),这两个不同的 Symbol 都会被列出,互不干扰。
  • 注意:Reflect.ownKeys() 不能用于 WeakMap 或 WeakSet,对这些对象调用会抛出 TypeError,因为其内部键不可遍历。

对比 Object.keys() / Object.getOwnPropertyNames() 容易踩的坑

这里需要澄清几个容易混淆的地方。Object.keys() 只返回可枚举的字符串键;Object.getOwnPropertyNames() 范围稍宽,返回所有自有的字符串键(包括不可枚举),但完全不涉及 Symbol;真正能覆盖全量键的,要么是 Reflect.ownKeys(),要么是手动组合 Object.getOwnPropertyNames() + Object.getOwnPropertySymbols()。

一个实际中容易翻车的场景:使用 for...in 或 Object.keys() 遍历某个配置对象,结果漏掉了 obj[Symbol.metadata] = {...} 这种用 Symbol 存储的元数据键。后续的序列化或深拷贝会因为没拿到这个键而导致信息丢失。

  • 不建议使用 Object.keys(obj).concat(Object.getOwnPropertySymbols(obj)) 这种方式。原因是前者的类型是 string[],后者的类型是 symbol[],合并后数组元素类型虽然变成了 (string | symbol)[],但 TypeScript 编译器会给出类型错误。虽然运行时没问题,但从代码整洁度来说并不推荐。
  • 直接使用 Reflect.ownKeys(obj) 是最干净的写法,语义明确,返回值类型为 (string | symbol)[]。
  • 如果想从结果中过滤出纯 Symbol 键,可以这样写:Reflect.ownKeys(obj).filter(k => typeof k === 'symbol')。

Symbol 键不可枚举,但 Reflect.ownKeys() 不关心枚举性

另一个关键点:Reflect.ownKeys() 不在乎某个属性是否可枚举,它只认“这个属性是否属于这个对象”。所以即便通过 Object.defineProperty(obj, 'hidden', { value: 1, enumerable: false }) 定义了一个不可枚举的字符串键,或者给对象挂了一个不可枚举的 Symbol 键(例如 obj[Symbol.iterator] = function*(){}),它都会照单全收。

这反过来也意味着:不能指望 Reflect.ownKeys() 的返回值来判断某个键在 JSON.stringify() 时是否会被序列化——因为 JSON.stringify() 只处理可枚举的字符串键,对 Symbol 和不可枚举键一律无视。

  • Reflect.ownKeys() 本质上是一种反射层级的“物理存在性探测”,它告诉你某个属性在内存中是否真实存在,但不关心它是否在运行时行为中被用到。
  • 如果想模拟 JSON.stringify() 的行为,需要手动过滤:Reflect.ownKeys(obj).filter(k => typeof k === 'string' && obj.propertyIsEnumerable(k))。
  • 另外,给 Symbol 键设置 enumerable: false 实际上没有意义——因为 Symbol 键天生就是不可枚举的,propertyIsEnumerable() 对任何 Symbol 键都会返回 false。

实际用法示例:安全地遍历并克隆含 Symbol 的对象

许多知名库(例如 Vue 的响应式标记、class 装饰器中的元数据)都依赖 Symbol 属性来存储额外信息。如果使用 Object.assign({}, obj) 或对象展开运算符进行克隆,这些 Symbol 属性会丢失。

const source = {
  a: 1,
  b: 2
};
source[Symbol.for('meta')] = { version: 2 };
source[Symbol('id')] = 'xyz';

const clone = {};
Reflect.ownKeys(source).forEach(key => {
  clone[key] = source[key];
});
// clone 现在包含 a、b、Symbol.for('meta')、Symbol('id') 四个键

此示例展示的是浅拷贝。如果 Symbol 键对应的值是一个复杂对象,并且需要进行深拷贝,则需要额外判断 typeof key === 'symbol' 并对值做递归处理——这一步虽然很常见,但很多人会忽略。

更复杂的情况是,当遇到 Proxy 这类带有 [[DefineOwnProperty]] 陷阱的对象时,Reflect.ownKeys() 返回的键可能与实际可访问的键不一致。如果遇到这种特殊状况,仅靠 Reflect.ownKeys() 还不够,需要结合 Reflect.getOwnPropertyDescriptor() 做进一步的验证。