前端面试 P006:对象遍历与属性键的选择
来源:方格前端讲面试 · 6.对象遍历方式对比(for in、Object.keys、Reflect),14 分 09 秒。
回看定位:01:35 属性的三个维度 · 06:17 Object.keys · 12:06 键顺序
先区分三个维度
对象属性要分别看:自有还是继承、可枚举还是不可枚举、键是字符串还是 Symbol。不可枚举只是部分枚举 API 不展示,不是保密机制。
| API | 自有属性 | 原型链属性 | 不可枚举 | Symbol 键 |
|---|---|---|---|---|
| for...in | 可枚举字符串键 | 可枚举字符串键 | 不包含 | 不包含 |
| Object.keys | 可枚举字符串键 | 不包含 | 不包含 | 不包含 |
| Object.getOwnPropertyNames | 所有自有字符串键 | 不包含 | 包含 | 不包含 |
| Object.getOwnPropertySymbols | 所有自有 Symbol 键 | 不包含 | 包含 | 包含 |
| Reflect.ownKeys | 所有自有属性键 | 不包含 | 包含 | 包含 |
Object.values、Object.entries 与 Object.keys 的属性范围相同,分别返回值或键值对。
const id = Symbol('id');
const obj = Object.create({ inherited: 1 });
obj.visible = 2;
obj[id] = 3;
Object.defineProperty(obj, 'hidden', { value: 4 });
console.log(Object.keys(obj)); // ['visible']
console.log(Object.getOwnPropertyNames(obj)); // ['visible', 'hidden']
console.log(Object.getOwnPropertySymbols(obj)[0] === id); // true
console.log(Reflect.ownKeys(obj).length); // 3
for...in 为什么常要过滤
只需要自身属性时,可以在循环中使用 Object.hasOwn(obj, key)。兼容旧环境可使用 Object.prototype.hasOwnProperty.call(obj, key),避免对象覆盖同名方法或没有 Object.prototype。
并不是所有 for...in 都必须过滤:如果业务本来就要读取继承的可枚举属性,那是主动选择。日常读取自有可枚举字符串字段时,Object.keys 或 Object.entries 的意图更直接。
键顺序与全面性的边界
对于普通对象,常用自有键顺序是:数组索引键按升序、其他字符串键按创建顺序、Symbol 键按创建顺序。不要把任何看起来像数字的字符串都当成数组索引,例如 '-1'、'01' 不属于这一类。
const obj = { '10': 'a', '2': 'b', name: 'c', '01': 'd' };
console.log(Object.keys(obj)); // ['2', '10', 'name', '01']
Reflect.ownKeys 的“全部”是全部自有属性键,不包括原型链、私有字段、WeakMap 里的关联状态,也不自动复制属性描述符。Proxy 还可能通过 ownKeys 等 trap 影响行为。拿到键只是复制对象的一个步骤,见 前端面试 P004:手写深拷贝、循环引用与实现边界。
视频表述的适用范围
视频的主要 API 对比正确。它将 Object.keys 说成性能最好的默认方案,这不能当作跨引擎、跨数据规模的绝对结论。按需要的属性范围选 API,实际性能问题再结合真实场景测量;也不必为了“最全面”在每处都用 Reflect.ownKeys。
Symbol() 每次创建不同 Symbol,但 Symbol.for 会复用注册表中同名 Symbol,不能把所有 Symbol 创建方式都理解成“永远不重复”。