先记住这个答案
三个方法都只返回对象自身的属性,原型链上的一个都不要。区别在两个维度上:键是字符串还是 Symbol,属性是否可枚举。Object.keys 只要可枚举的字符串键,Object.getOwnPropertyNames 要全部字符串键(包括不可枚举的),Object.getOwnPropertySymbols 只要 Symbol 键(不管是否可枚举)。想一次拿到自身全部的键,用 Reflect.ownKeys,它等于后两者合起来。
- 三个方法都只看自身属性,原型链上的一律不管
Object.keys只要可枚举的字符串键getOwnPropertyNames包括隐藏字符串键- 要一次拿全所有键用
Reflect.ownKeys
一个对象上看三个方法的差别
构造一个对象,同时带普通属性、不可枚举属性、Symbol 属性,再给它挂个原型。
const id = Symbol('id')
const user = { name: 'Tom', [id]: 42 }
Object.defineProperty(user, 'secret', { value: 'x', enumerable: false })
const admin = Object.create(user) // 原型上的属性三个方法都不管
admin.role = 'admin'
console.log(Object.keys(user).join())
console.log(Object.getOwnPropertyNames(user).join())
console.log(Object.getOwnPropertySymbols(user).map(String).join())
console.log(Reflect.ownKeys(user).map(String).join()) // 三类合在一起
console.log(Object.keys(admin).join())查看输出与解释
name
name,secret
Symbol(id)
name,secret,Symbol(id)
rolesecret 用 defineProperty 定义时 enumerable 为 false,所以 Object.keys 看不到它,getOwnPropertyNames 能看到。Symbol 键 id 两个字符串方法都不返回,只在 getOwnPropertySymbols 里出现。admin 继承了 user 的全部属性,但 Object.keys(admin) 只有它自己的 role。
不可枚举属性从哪来
平时写 obj.x = 1 或者字面量定义的属性都是可枚举的,所以不可枚举属性主要来自两个地方。
一个是语言内置的属性。数组的 length、函数的 name 和 length、class 里定义的方法都是不可枚举的。比如 Object.getOwnPropertyNames([1]) 返回 ['0', 'length'],而 Object.keys([1]) 只有 ['0']。class 方法不可枚举这一点很重要,它意味着用 Object.keys 或 Object.assign 处理 SomeClass.prototype 什么也拿不到。
另一个是用 Object.defineProperty 主动隐藏。库作者经常这样挂内部状态,比如 Vue 2 在响应式对象上挂的 __ob__ 就是不可枚举的,这样用户遍历、JSON.stringify 时都不会看到它。
- Symbol 键为什么单独一个方法
- Symbol 是 ES2015 才加的。当时
Object.keys和getOwnPropertyNames已经被大量代码使用,如果让它们突然返回 Symbol,老代码里key.toUpperCase()这类字符串操作就会报错。所以标准选择新开一个方法,旧方法的行为保持不变。这也是 Symbol 适合做「不会和别人冲突的隐藏键」的原因:普通遍历根本看不到它。
容易答错的地方
- 以为
Object.keys和for...in一样 for...in会遍历原型链上可枚举的字符串键,Object.keys只看自身。上面的admin用for...in会得到role和name两个键。项目里想只遍历自身属性,直接用Object.keys或Object.entries,不用在for...in里再加hasOwn过滤。- 以为
getOwnPropertyNames能拿到所有键 - 它名字里有 Names,只管字符串键。Symbol 键要单独用
getOwnPropertySymbols,或者直接用Reflect.ownKeys一次拿全。 - 以为三个方法的顺序是随意的
- 返回顺序有规则:先是整数索引键按数值升序,然后是其他字符串键按添加顺序,Symbol 键也按添加顺序,
Reflect.ownKeys里 Symbol 排在所有字符串之后。所以{ b: 1, 2: 1, a: 1, 1: 1 }的Object.keys结果是['1', '2', 'b', 'a']。
面试官还会怎么问?
JSON.stringify 和展开运算符会带上哪些属性?
JSON.stringify 只处理自身可枚举的字符串键,和 Object.keys 范围一致,Symbol 键直接忽略。对象展开 { ...obj } 和 Object.assign 复制的是自身可枚举属性,字符串键和 Symbol 键都会复制。
深拷贝时要用哪个方法取键?
要保留 Symbol 键和不可枚举属性,就用 Reflect.ownKeys 取键,再用 Object.getOwnPropertyDescriptor 拿描述符,用 Object.defineProperty 写到新对象上,这样 enumerable、getter 都能原样保留。只拷贝普通数据的话 structuredClone 更省事,但它会丢掉 Symbol 键和函数。
Object.keys 传一个字符串会怎样?
ES2015 起会先把参数转成对象,Object.keys('ab') 返回 ['0', '1']。传 null 或 undefined 会抛 TypeError,因为它们没法转成对象。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。