先记住这个答案
enumerable 决定属性能否参加按可枚举性筛选的操作。Object.keys() 返回自身的可枚举字符串键;for...in 还考虑原型链上的字符串键,但同名键会受到遮蔽规则影响。两者都不枚举 Symbol 键。用 Object.defineProperty() 新增属性而省略 enumerable,默认不可枚举;对普通对象通过赋值新增的普通数据属性,通常可枚举。
- enumerable:false 对两个 API 都隐藏
- for...in 会包含原型链可枚举属性
- Object.keys 只取自身可枚举键
两个枚举 API 各自的过滤规则
Object.keys() 的筛选条件有三项:对象自身、字符串键、可枚举。for...in 沿原型链考察字符串键,跳过不可枚举项,并避免重复输出同名键。一个自身的不可枚举同名属性,也可能遮蔽原型上的可枚举属性,因此不能把它理解为简单拼接每一层的 Object.keys()。
默认值的差异也很关键:Object.defineProperty(obj, 'key', { value: 1 }) 新增属性且不写 enumerable 时会得到 false;而普通赋值 obj.key = 1 会得到 true。另一个区别是,for...in 会输出原型链上可枚举的继承属性,Object.keys() 不会。可通过 Object.getOwnPropertyDescriptor(obj, 'key').enumerable 查看实际状态。
const obj = {};
Object.defineProperty(obj, 'hidden', {
value: 1,
enumerable: false,
configurable: true,
writable: true
});
obj.visible = 2;
const proto = { inherited: 3 };
Object.setPrototypeOf(obj, proto);
console.log('keys:', Object.keys(obj).sort().join(','));
const forInKeys = [];
for (const key in obj) forInKeys.push(key);
console.log('for-in:', forInKeys.sort().join(','));
console.log('names:', Object.getOwnPropertyNames(obj).sort().join(','));
console.log('hidden enumerable:', Object.getOwnPropertyDescriptor(obj, 'hidden').enumerable);查看输出与解释
keys: visible
for-in: inherited,visible
names: hidden,visible
hidden enumerable: falsehidden 不可枚举,两个遍历都不输出它;for...in 多出继承的 inherited。Object.getOwnPropertyNames() 列出全部自身字符串键,因此能看到 hidden;它不返回 Symbol 键,也不返回继承键。
遍历表单字段时内部标记被意外忽略
假设渲染一个订单对象:业务金额、币种用普通赋值产生,另用 Object.defineProperty 写入一个 enumerable:false 的 _debugTag 供内部排查。若函数用 for...in 收集字段,对象原型上某个全局可枚举辅助方法也会进入列表;而 _debugTag 无论 for...in 还是 Object.keys 都不出现,因此真正的风险不是隐藏字段被输出,而是原型字段被当成自身字段。
改用 Object.keys(order) 可以避免误把继承字段当作自身字段,但它本身不能防止原型污染,也不能充当业务字段白名单。对外只允许输出金额和币种时,应明确选取这些字段。排查隐藏属性可使用描述符或 Object.prototype.propertyIsEnumerable.call(order, "_debugTag"),避免对象覆盖同名方法。
结论在哪些条件下会失效或需要修正
普通赋值新加的属性默认 enumerable:true,想隐藏必须显式用 Object.defineProperty 设置 enumerable:false。对已存在的属性,新描述符没有写的特性会保留旧值,所以只传 value 不会把原本可枚举的属性藏起来;只有从未定义过的新属性才落入不可枚举默认。
如果对象已被冻结或密封,隐藏状态不会在运行时被外部描述符改写,但新增属性也会因不可扩展而失败,因此隐藏属性必须在冻结前定义好。对原型方法,ES6 class 中的方法默认不可枚举,不会进入 for...in;但若用户用赋值方式扩展原型,对应方法就会变成可枚举,这时 Object.keys 仍不受影响。
容易答错的地方
- 把 for...in 看成只遍历对象自身
for...in会沿原型链读取可枚举属性,所以原型上的新增属性也会被输出。若只观察对象自身的普通字段,很容易误判来源。判断是否为自身属性应使用Object.prototype.hasOwnProperty.call(obj, key)。- 把新增属性当成默认可枚举
Object.defineProperty不传enumerable时,新属性的默认值是false,不是普通赋值那样的true。因此刚添加的属性不会出现在Object.keys()或for...in中,需要显式设置enumerable:true才会出现。
面试官还会怎么问?
enumerable:false 的属性还能用 `in` 运算符检测吗?
能。in 检查的是属性是否存在,不关心是否可枚举。要查值可用直接访问或 Reflect.get;要查描述符再用 Object.getOwnPropertyDescriptor。枚举性与存在性是两件独立的事。
Object.assign 和展开运算符会复制不可枚举属性吗?
都不会复制不可枚举属性,但它们会复制自身的可枚举字符串键和 Symbol 键,这一点与只返回字符串键的 Object.keys() 不同。复制普通对象的全部自有属性描述符可使用 Object.getOwnPropertyDescriptors() 配合 Object.defineProperties();这不是通用深拷贝,也不会复制所有内置对象的内部状态。
for...in 遍历到原型属性后应如何过滤?
在循环体内用 Object.prototype.hasOwnProperty.call(obj, key) 检查。若明确只处理自身属性,更简洁的方案是直接用 Object.keys,它不会包含原型链上的任何键。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。