深入讲解JavaScript中遍历对象的Symbol键和不可枚举属性的完整方法与实用技巧,全面解析Reflect.ownKeys、Object.getOwnPropertyNames、Object.getOwnPropertySymbols的核心原理、对比差异与原型链扩展实践。
JavaScript怎么遍历对象的Symbol键和不可枚举属性
时间:2026-11-05
浏览:423
文章目录
在日常的 JavaScript 开发中,最常用的属性遍历工具莫过于 for...in 循环与 Object.keys()。但如果你曾经尝试用它们去读取包含 Symbol 类型的属性,或者由 Object.defineProperty() 定义的 enumerable: false 属性,就会发现遍历结果空空如也。直接的结论是:要遍历对象的自身全量键(同时包含可枚举/不可枚举字符串与 Symbol 键),最简洁且原生的现代 API 是 Reflect.ownKeys(target);若需单项提取,获取 Symbol 键使用 Object.getOwnPropertySymbols(),获取含不可枚举的字符串键使用 Object.getOwnPropertyNames(),若需穿透原型链则需要结合 Object.getPrototypeOf() 循环迭代。
一、常规遍历的“盲区”从何而来
理解为什么某些属性会被跳过,需要从 JavaScript 对象的底层属性存储模型谈起。在 ECMAScript 规范中,对象上的每一个属性并不仅仅是一个简单的“键值对”,其背后都绑定着一个属性描述符(Property Descriptor)。对于常规的数据属性,描述符包含四个元属性:value、writable、enumerable 以及 configurable。
当一个属性的 enumerable 标志被设置为 false 时,它就成为了“不可枚举属性”。JavaScript 设计此机制的初衷,是为了保护系统内置方法和敏感数据。例如,所有数组实例上的 length 属性、原型链上的 toString、hasOwnProperty 以及 ES6 类声明中的所有原型方法,默认全都是不可枚举的。这样做的目的是防止我们在普通循环中把这些基础方法当成业务数据遍历出来。
而 Symbol 则是 ES6 引入的第七种基本数据类型。由于 Symbol 可以生成全局唯一的标识符,它常被用作元编程、内部协议(如 Symbol.iterator)以及私有数据模拟。为了完全向后兼容旧版代码,JavaScript 制定了一条硬性规则:所有老牌的字符串遍历工具(如 for...in、Object.keys()、JSON.stringify())必须无条件忽略所有 Symbol 类型的属性键,无论该 Symbol 的 enumerable 是否为 true。
二、构建基准测试对象
为了直观验证各个 API 的实际表现,我们首先构建一个具备全部四个象限特征的典型对象。这个对象既包含日常可见的普通字符串,也包含由属性描述符定制的不可枚举属性和 Symbol 键:
// 1. 创建两个独特的 Symbol 标识
const symPublic = Symbol('publicToken');
const symPrivate = Symbol('internalState');
// 2. 初始化测试对象
const userProfile = {
username: 'chen_xiao', // 象限1:可枚举字符串属性
role: 'developer' // 象限1:可枚举字符串属性
};
// 3. 挂载普通不可枚举属性
Object.defineProperty(userProfile, 'accessSecret', {
value: 'SEC_8943_XDF',
enumerable: false, // 象限3:不可枚举字符串属性
writable: true,
configurable: true
});
// 4. 挂载可枚举 Symbol 属性
userProfile[symPublic] = 'PUB_TOKEN_ACTIVE'; // 象限2:可枚举 Symbol 属性
// 5. 挂载不可枚举 Symbol 属性
Object.defineProperty(userProfile, symPrivate, {
value: 'SYS_LOCKED_FLAG',
enumerable: false, // 象限4:不可枚举 Symbol 属性
writable: false,
configurable: false
});
如果我们直接运行常规的获取方法:
console.log(Object.keys(userProfile));
// 输出示例: ['username', 'role']
// 遗漏了:accessSecret、symPublic 以及 symPrivate
可以看到,原本存在的 5 个属性中,有 3 个在常规方法下完全“隐身”了。
三、核心解决方案:四大专业 API 实战
1. 全能方案:Reflect.ownKeys(target)
Reflect.ownKeys() 是 ES6 随反射机制引入的标准静态方法。它的核心规范正是直接调用对象内部的 [[OwnPropertyKeys]] 槽函数。该方法会返回一个由目标对象自身拥有的所有键组成的数组,无视是否可枚举,也无视是字符串还是 Symbol。
// 获取对象自身的全量属性键
const allKeys = Reflect.ownKeys(userProfile);
console.log(allKeys);
// 输出示例:
// [
// 'username',
// 'role',
// 'accessSecret',
// Symbol(publicToken),
// Symbol(internalState)
// ]
// 遍历并读取对应的值
allKeys.forEach(key => {
console.log(String(key), '=>', userProfile[key]);
});
Reflect.ownKeys() 是现代 JavaScript 库(如深拷贝、状态追踪、序列化增强器)中最推崇的 API。不仅代码极短,而且原生性能优于多次调用拼接。
2. 专精提取:Object.getOwnPropertySymbols(target)
如果你在架构设计中专门使用 Symbol 作为私有数据或插件扩展槽,且只需要取出所有的 Symbol 键,那么直接使用 Object.getOwnPropertySymbols() 是最精准的选择。
const symbolKeys = Object.getOwnPropertySymbols(userProfile);
console.log(symbolKeys);
// 输出示例: [ Symbol(publicToken), Symbol(internalState) ]
// 无论 enumerable 是 true 还是 false,只要是 Symbol 都会被捕获
symbolKeys.forEach(sym => {
const desc = Object.getOwnPropertyDescriptor(userProfile, sym);
console.log(sym.description, '可枚举性:', desc.enumerable, '对应值:', userProfile[sym]);
});
// 输出示例:
// publicToken 可枚举性: true 对应值: PUB_TOKEN_ACTIVE
// internalState 可枚举性: false 对应值: SYS_LOCKED_FLAG
3. 提取所有字符串键:Object.getOwnPropertyNames(target)
如果业务需求是获取所有的字符串键(包含可见与隐藏属性),但坚决不要 Symbol,那么应该使用 Object.getOwnPropertyNames()。
const stringKeys = Object.getOwnPropertyNames(userProfile);
console.log(stringKeys);
// 输出示例: ['username', 'role', 'accessSecret']
// 注意:所有 Symbol 均已被天然滤除
若你只需要单纯的不可枚举字符串键,只需配合 propertyIsEnumerable 进行一次反向过滤:
const nonEnumerableStringKeys = Object.getOwnPropertyNames(userProfile).filter(key => {
return !Object.prototype.propertyIsEnumerable.call(userProfile, key);
});
console.log(nonEnumerableStringKeys);
// 输出示例: ['accessSecret']
四、底层原理:[[OwnPropertyKeys]] 内部排序规则
在使用 Reflect.ownKeys() 获取属性时,返回数组中键的先后排列并非随机,而是严格遵循 ECMAScript 规范的内部算法顺序:
- 整数索引优先:所有能够转换为非负整数的属性键(例如
"0"、"1"、"42"),优先按照数值升序排列; - 字符串按插入顺序:其余所有普通字符串键,严格按照其被添加到对象上的时间先后顺序排列;
- Symbol 键最后排布:所有 Symbol 类型的键排在最后,同样严格保持被定义时的先后时序。
这一规范保证了跨浏览器运行环境(V8、JavaScriptCore、SpiderMonkey)在执行 Reflect.ownKeys() 时的一致确定性。
五、进阶实战:遍历原型链上的不可枚举与 Symbol 键
上述所有 getOwnProperty* 与 Reflect.ownKeys 都有一个共同的前提:仅遍历对象自身(Own Properties),不涉足原型链。
然而在实际开发中(例如分析继承体系、编写框架级别的反射与依赖注入工具时),父类或原型上的方法同样是不可枚举的(ES6 class 中定义的成员方法默认 enumerable: false)。若要完整获取整条原型链上的所有属性,需要通过迭代 Object.getPrototypeOf() 向上爬梳。
以下是具备防环机制与深度爬取能力的通用遍历函数:
/**
* 完整获取对象及其原型链上的所有属性键(含不可枚举与Symbol)
* @param {Object} target 目标对象
* @param {boolean} includeObjectPrototype 是否包含 Object.prototype 上的原生方法
* @returns {Array} 属性键集合
*/
function getDeepAllKeys(target, includeObjectPrototype = false) {
if (target === null || target === undefined) {
return [];
}
const collectedKeys = new Set();
const visitedPrototypes = new Set();
let currentObj = target;
while (currentObj) {
// 达到原型链顶端或防范原型成环
if (visitedPrototypes.has(currentObj)) break;
visitedPrototypes.add(currentObj);
// 默认不提取 Object.prototype 上的默认方法(如 hasOwnProperty、toString 等)
if (!includeObjectPrototype && currentObj === Object.prototype) {
break;
}
// 取出当前层级拥有的所有键(含不可枚举与Symbol)
const ownKeys = Reflect.ownKeys(currentObj);
for (const key of ownKeys) {
collectedKeys.add(key);
}
// 沿原型链向上爬升一级
currentObj = Object.getPrototypeOf(currentObj);
}
return Array.from(collectedKeys);
}
// 验证类继承场景
class BaseService {
constructor() {
this.serviceId = 'SRV_BASE_01';
}
// 类原型上的方法默认不可枚举!
logBase() {
console.log('BaseService invoked');
}
}
class AuthService extends BaseService {
constructor() {
super();
this[Symbol('authLevel')] = 'ADMIN';
}
validateUser() {
return true;
}
}
const instance = new AuthService();
const deepKeys = getDeepAllKeys(instance);
console.log(deepKeys);
// 输出示例包含:
// [
// 'serviceId',
// Symbol(authLevel),
// 'constructor',
// 'validateUser',
// 'logBase'
// ]
for...in 只能遍历原型链上的可枚举属性,因此 validateUser 和 logBase 在常规循环中根本无法被发现。而通过 getDeepAllKeys,原型链上的所有类方法与继承状态均被完整还原。
六、API 综合特性矩阵与选型总结
| 遍历方式 | 可枚举字符串 | 不可枚举字符串 | Symbol 键 | 原型链继承 | 典型适用场景 |
|---|---|---|---|---|---|
for...in |
支持 | 忽略 | 忽略 | 仅可枚举 | 遗留代码遍历纯字面量字典 |
Object.keys() |
支持 | 忽略 | 忽略 | 无 | 常规业务数据对象遍历、JSON 键收集 |
Object.getOwnPropertyNames() |
支持 | 支持 | 忽略 | 无 | 提取隐藏字符串键、检查系统内部字段 |
Object.getOwnPropertySymbols() |
忽略 | 忽略 | 全量支持 | 无 | 微前端/插件隔离中的 Symbol 状态探查 |
Reflect.ownKeys() |
支持 | 支持 | 全量支持 | 需沿链爬取 | 全量深拷贝、序列化、元编程与调试监控 |
七、注意事项与避坑建议
- JSON.stringify 的天然丢失:
JSON.stringify在设计上只处理对象自身的可枚举字符串键。任何 Symbol 键和不可枚举属性在被序列化时都会被静默丢弃。如果需要序列化这些属性,必须先借助Reflect.ownKeys()收集键名,并在自定义replacer或重写toJSON中进行手动映射。 - 原型链污染与死循环:在向上迭代原型链时,必须设置终止条件(如对比
null或Object.prototype),并建议维护一个Set防止由于某些恶意代码篡改原型链结构导致的无限循环。 - 属性访问器(Getter/Setter)副作用:在通过获取到的键读取属性值时,若该属性被定义为 Getter 函数,执行
obj[key]会实际触发该函数的运算逻辑。如果只希望分析属性结构而不触发 Getter,应改用Object.getOwnPropertyDescriptor(obj, key)查看其配置。
实用工具推荐
绿化、压缩、噪点和虚拟水印,自由调节做旧程度。
选择画质和输出格式,预览压缩前后的大小。