先记住这个答案
两个都遍历不到。属性的 enumerable 为 false 时,for...in 和 Object.keys 都会跳过它。它们真正的区别是查找范围:for...in 先列自身的可枚举属性,再沿原型链往上列继承来的可枚举属性;Object.keys 只返回对象自身的可枚举字符串键。不可枚举属性并没有被藏起来,直接读 obj.hidden 照样能拿到值。
- 不可枚举键不会进入常规键遍历
for...in会带上原型链上的可枚举属性defineProperty新增属性默认不可枚举- 可用
getOwnPropertyNames查自有键
把几种 API 放在一起对比
造一个对象:原型上有一个属性,自身有一个普通属性和一个用 defineProperty 定义的属性,然后用不同方式去列它的键。
const proto = { inherited: 3 }
const obj = Object.create(proto)
obj.visible = 2 // 普通赋值,enumerable 默认 true
Object.defineProperty(obj, 'hidden', { value: 1 }) // 没写 enumerable,默认 false
console.log(Object.keys(obj).join(','))
const forIn = []
for (const k in obj) forIn.push(k)
console.log(forIn.join(','))
console.log(Object.getOwnPropertyNames(obj).join(','))
console.log(JSON.stringify(obj))
console.log(obj.hidden, 'hidden' in obj) // 读得到,in 也能判断出来
console.log(obj.propertyIsEnumerable('hidden'))查看输出与解释
visible
visible,inherited
visible,hidden
{"visible":2}
1 true
false逐行看:Object.keys 只有 visible;for...in 多了原型上的 inherited,但 hidden 依然没有;Object.getOwnPropertyNames 不管可不可枚举,把自身的字符串键全列出来;JSON.stringify 和 Object.keys 一样只认自身可枚举属性。最后两行说明,不可枚举只影响「列出来」,不影响「读得到」。
容易答错的地方
- 以为
for...in能遍历到不可枚举属性 for...in范围更大是因为它走原型链,不是因为它能看到不可枚举属性。它和Object.keys一样只认enumerable: true的。- 以为不可枚举就是私有
- 它只影响遍历类 API。
obj.hidden能读,writable为true时也能改,in和Object.hasOwn都返回true。真正的私有要用 class 的#field。 - 忘了
defineProperty的默认值 Object.defineProperty(obj, 'x', { value: 1 })没写enumerable,它就是false,同时writable、configurable也都是false。很多「属性怎么拷贝不过去」的问题就出在这里。
面试官还会怎么问?
怎么拿到对象所有的自有键,包括不可枚举和 Symbol?
用 Reflect.ownKeys(obj)。它返回全部自有键,包括不可枚举的字符串键和 Symbol 键。只要字符串键用 Object.getOwnPropertyNames,只要 Symbol 键用 Object.getOwnPropertySymbols。
for...in 怎么只遍历自身属性?
在循环里加一句 if (!Object.hasOwn(obj, k)) continue,把继承来的键过滤掉。更简单的办法是直接改用 Object.keys(obj).forEach(...) 或 for...of Object.entries(obj)。
想完整复制一个对象,连不可枚举属性和 getter 都保留,怎么写?
Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj))。它把每个属性的描述符原样复制过去,enumerable、writable、getter 和 setter 都不会丢,原型也保持一致。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。