一、核心结论
Object.keys() 不是无序的。现代 JavaScript 已经规定了明确的属性遍历顺序:
text
整数索引键 → 按数值升序
普通字符串键 → 按属性创建/插入顺序
Symbol 键 → 按属性创建/插入顺序
但是:
text
Object.keys()
Object.values()
Object.entries()
都不会返回 Symbol 键。 如果要完整获取对象的所有自有属性键,应使用:
javascript
Reflect.ownKeys(obj)
二、核心思路(一句话)
现代 JavaScript 对象属性不是"无序",而是按照"整数索引优先升序 → 其他字符串按创建顺序 → Symbol 按创建顺序"的规则排列;
Object.keys()只返回其中的可枚举字符串键。
三、主要矛盾与次要矛盾
主要矛盾
面试真正考察的是:
你是否知道 JavaScript 对象属性的规范化排序规则。
次要矛盾
需要进一步区分:
- 什么叫"整数索引键"
- 字符串键和 Symbol 键的顺序
Object.keys()到底返回哪些属性Object.values()、Object.entries()是否遵循相同顺序Reflect.ownKeys()与Object.keys()的区别- 旧 JavaScript 与现代 ECMAScript 规范的区别
四、解决方案流程图
text
对象属性遍历
│
▼
是否调用 Object.keys?
│
▼
获取对象自身的可枚举字符串键
│
▼
┌──────────┴──────────┐
│ │
整数索引键 普通字符串键
│ │
▼ ▼
数值升序 创建顺序
│ │
└──────────┬──────────┘
│
▼
Object.keys()结果
注意:
Symbol 键不会出现在 Object.keys() 中
│
▼
如果需要 Symbol / 全部自身键
│
▼
Reflect.ownKeys()
│
▼
整数索引 → 普通字符串 → Symbol
五、底层实现原理:JavaScript 到底按照什么顺序?
这里是这道题最重要的部分。
JavaScript 规范实际上通过内部抽象操作 [[OwnPropertyKeys]] 确定对象自身属性键的顺序。可以把它理解成:
text
对象
│
└── [[OwnPropertyKeys]]
│
├── ① 数组索引属性
│ ↓
│ 数值升序
│
├── ② 其他字符串属性
│ ↓
│ 创建顺序
│
└── ③ Symbol 属性
↓
创建顺序
例如:
javascript
const obj = {
b: 'B',
2: 'two',
1: 'one',
a: 'A'
};
console.log(Object.keys(obj));
结果:
javascript
['1', '2', 'b', 'a']
虽然代码中:
text
b
2
1
a
是这个创建顺序,但遍历时:
text
1
2
b
a
六、为什么数字键会排在前面?
这里有一个非常容易答错的点。不能简单说:
"能转换成数字的字符串就是数字键。"
这个说法不准确。 规范意义上需要区分数组索引(array index)形式的属性键。
例如:
javascript
const obj = {
10: 'a',
2: 'b',
1: 'c'
};
console.log(Object.keys(obj));
结果:
javascript
['1', '2', '10']
因为这些属性键实际上都是字符串:
javascript
'1'
'2'
'10'
但它们符合数组索引属性的规则,所以按照数值升序排列。
七、哪些看起来像数字,但实际上不是这一类?
这是面试追问的重点。
javascript
const obj = {
'2': 'a',
'10': 'b',
'01': 'c',
'1.5': 'd',
'-1': 'e',
'1e2': 'f',
'4294967295': 'g'
};
console.log(Object.keys(obj));
不能简单认为:
text
所有数字形式的字符串
↓
都按照数字大小排序
实际上:
text
'2' → 数组索引形式
'10' → 数组索引形式
'01' → 普通字符串
'1.5' → 普通字符串
'-1' → 普通字符串
'1e2' → 普通字符串
所以判断规则时,面试中最好不要说:
"数字字符串按照数字排序。"
而应该说:
"符合数组索引定义的属性键按照数值升序,其他字符串属性按照创建顺序。"
这个表述更加准确。
八、普通字符串属性按照什么顺序?
普通字符串属性按照属性创建顺序排列。
javascript
const obj = {};
obj.name = '张三';
obj.age = 18;
obj.address = '深圳';
console.log(Object.keys(obj));
结果:
javascript
['name', 'age', 'address']
如果删除之后重新添加:
javascript
const obj = {
a: 1,
b: 2,
c: 3
};
delete obj.b;
obj.b = 4;
console.log(Object.keys(obj));
结果:
javascript
['a', 'c', 'b']
因为:
text
a 创建
b 创建
c 创建
删除 b
重新创建 b
最终:
a → c → b
所以这里更准确的概念是:
属性创建顺序,而不是简单理解成源码书写顺序。
九、Symbol 属性是什么顺序?
Symbol 属性同样具有顺序:
javascript
const s1 = Symbol('s1');
const s2 = Symbol('s2');
const obj = {};
obj[s1] = 'A';
obj[s2] = 'B';
console.log(Reflect.ownKeys(obj));
结果中 Symbol 部分保持:
text
s1
s2
也就是:
text
Symbol 属性之间
↓
按照创建顺序
但是:
javascript
Object.keys(obj)
不会返回 Symbol。
十、为什么 Object.keys() 看不到 Symbol?
因为 Object.keys() 的语义本身就是:
返回
对象自身的、可枚举的、字符串属性键。
因此它实际上过滤了两类东西:
text
Object.keys(obj)
│
├── 继承属性 ❌
├── 不可枚举属性 ❌
├── Symbol 属性 ❌
│
└── 自身 + 可枚举 + 字符串属性 ✅
例如:
javascript
const symbolKey = Symbol('id');
const obj = {
name: 'Tom',
[symbolKey]: 100
};
console.log(Object.keys(obj));
结果:
javascript
['name']
而:
javascript
console.log(Reflect.ownKeys(obj));
可以得到:
text
['name', Symbol(id)]
十一、Object.keys()、Object.values()、Object.entries() 顺序是否一样?
是的。 它们针对的都是对象自身的可枚举字符串属性,只是返回的数据形式不同。
javascript
const obj = {
b: 2,
10: 10,
2: 2,
a: 1
};
console.log(Object.keys(obj));
console.log(Object.values(obj));
console.log(Object.entries(obj));
结果:
javascript
Object.keys(obj)
// ['2', '10', 'b', 'a']
Object.values(obj)
// [2, 10, 2, 1]
Object.entries(obj)
// [
// ['2', 2],
// ['10', 10],
// ['b', 2],
// ['a', 1]
// ]
所以可以记成:
text
Object.keys()
Object.values()
Object.entries()
│
▼
遵循相同的属性遍历顺序
十二、如果要求"对象所有属性",应该用什么?
这是非常典型的面试追问。
Object.keys()
text
自身
+
可枚举
+
字符串键
Object.getOwnPropertyNames()
text
自身
+
字符串键
包括不可枚举属性
不包括 Symbol
Object.getOwnPropertySymbols()
text
自身
+
Symbol 键
Reflect.ownKeys()
text
自身所有属性键
+
字符串
+
Symbol
+
可枚举/不可枚举
因此:
javascript
Reflect.ownKeys(obj)
是最完整的。
十三、完整示例:一次看懂所有规则
javascript
const symbolA = Symbol('symbolA');
const symbolB = Symbol('symbolB');
const obj = {};
// ① 普通字符串属性
obj.name = 'Tom';
// ② 数组索引形式的属性
obj[10] = 'ten';
obj[2] = 'two';
obj[1] = 'one';
// ③ 普通字符串属性
obj.age = 18;
// ④ Symbol 属性
obj[symbolA] = 'A';
obj[symbolB] = 'B';
// ⑤ Object.keys:
// 只获取自身、可枚举、字符串属性
console.log(Object.keys(obj));
// [
// '1',
// '2',
// '10',
// 'name',
// 'age'
// ]
//
// 注意:
// 数组索引属性不会按照创建顺序,
// 而是按照数值升序排列。
// ⑥ Object.values:
// 顺序与 Object.keys 一致,只返回值
console.log(Object.values(obj));
// [
// 'one',
// 'two',
// 'ten',
// 'Tom',
// 18
// ]
// ⑦ Object.entries:
// 顺序与 Object.keys 一致
console.log(Object.entries(obj));
// [
// ['1', 'one'],
// ['2', 'two'],
// ['10', 'ten'],
// ['name', 'Tom'],
// ['age', 18]
// ]
// ⑧ Reflect.ownKeys:
// 获取对象自身的所有属性键,
// 包括 Symbol 和不可枚举属性。
console.log(Reflect.ownKeys(obj));
// [
// '1',
// '2',
// '10',
// 'name',
// 'age',
// Symbol(symbolA),
// Symbol(symbolB)
// ]
十四、使用场景
场景一:普通对象遍历
javascript
const user = {
name: 'Tom',
age: 18,
city: 'Shenzhen'
};
for (const key of Object.keys(user)) {
console.log(key, user[key]);
}
适合:
text
表单数据
配置对象
接口数据
普通键值映射
场景二:需要保证顺序的数据
如果业务本身要求严格顺序,例如:
text
排行榜
任务队列
步骤列表
菜单顺序
不要优先使用普通 Object 表达"有序集合"。
更好的选择通常是:
javascript
const list = [
{ id: 1, name: '第一步' },
{ id: 2, name: '第二步' },
{ id: 3, name: '第三步' }
];
或者:
javascript
const map = new Map();
map.set('b', 2);
map.set('a', 1);
map.set('c', 3);
Map 明确提供按插入顺序迭代的语义。
十五、边界场景:对象真的能代替 Map 吗?
不能简单认为:
text
Object = Map
虽然现代 Object 的属性遍历顺序已经规范化,但两者设计目标不同。
| 特性 | Object | Map |
|---|---|---|
| 键类型 | 字符串 / Symbol | 任意值 |
| 插入顺序 | 有特殊排序规则 | 按插入顺序 |
| 原型 | 默认存在 | 没有普通对象原型语义 |
| 获取大小 | Object.keys().length |
map.size |
| 适合配置 | ✅ | 一般 |
| 适合动态键值集合 | 可以 | 更推荐 |
例如:
javascript
const map = new Map();
map.set(10, 'a');
map.set(2, 'b');
map.set(1, 'c');
console.log([...map.keys()]);
结果:
javascript
[10, 2, 1]
这里不会像 Object 一样变成:
javascript
[1, 2, 10]
十六、ES6 之后到底发生了什么?
text
早期 ECMAScript
↓
对很多对象属性枚举顺序缺少统一、明确的规范保证
↓
不同 JavaScript 引擎可能表现不同
↓
现代 ECMAScript
↓
逐步明确并规范化属性键顺序
↓
整数索引 → 普通字符串 → Symbol
因此今天面试时,不要再直接回答"Object.keys 无序"。应该回答:
现代 JavaScript 中,Object.keys 的返回顺序是有规范依据的,但它不是简单的"全部按照插入顺序"。整数索引属性会优先按照数值升序,普通字符串属性按照创建顺序,Symbol 属性按照创建顺序但不会被 Object.keys 返回。
这才是比较完整的答案。
十七、结构化逻辑:面试如何快速回答?
可以用四层结构:
text
第一层:结论
Object.keys 不是无序。
↓
第二层:排序规则
整数索引 → 数值升序
普通字符串 → 创建顺序
Symbol → 创建顺序
↓
第三层:返回范围
Object.keys:
自身 + 可枚举 + 字符串
↓
第四层:追问
想拿全部属性:
Reflect.ownKeys
需要纯插入顺序键值集合:
Map
这样基本可以覆盖大多数追问。
十八、面试高频陷阱
陷阱 1:说"Object.keys 无序"
❌ 不准确。
应该说:
现代 ECMAScript 已经规定了属性键的顺序。
陷阱 2:说"数字键按照插入顺序"
❌ 错误。
javascript
const obj = {};
obj[3] = 'c';
obj[1] = 'a';
obj[2] = 'b';
console.log(Object.keys(obj));
结果:
javascript
['1', '2', '3']
陷阱 3:说"所有数字字符串都会排序"
❌ 不准确。
text
'1'
'2'
'10'
属于数组索引形式。
但:
text
'01'
'1.5'
'-1'
'1e2'
不能简单按照数字键处理。
陷阱 4:说"Object.keys 能拿到 Symbol"
❌ 错误。
javascript
Object.keys(obj)
不会返回 Symbol。
应该使用:
javascript
Object.getOwnPropertySymbols(obj)
或者:
javascript
Reflect.ownKeys(obj)
十九、满分答案
面试题:Object.keys() 返回的属性顺序是无序的吗?
核心思路(一句话)
不是。现代 JavaScript 已经规范化了对象属性键的遍历顺序:数组索引属性按数值升序,普通字符串属性按创建顺序,Symbol 属性按创建顺序;而
Object.keys()只返回自身可枚举的字符串属性。
流程图
text
Object.keys(obj)
│
▼
对象自身属性
│
├── 数组索引属性
│ ↓
│ 数值升序
│
├── 普通字符串属性
│ ↓
│ 创建顺序
│
└── Symbol 属性
↓
Object.keys 不返回
底层原理
JavaScript 规范通过对象的内部属性键获取机制规定属性键顺序,核心可以理解为:
text
[[OwnPropertyKeys]]
│
├── 数组索引 → 升序
├── 字符串 → 创建顺序
└── Symbol → 创建顺序
例如:
javascript
const obj = {
b: 1,
10: 10,
2: 2,
a: 3
};
console.log(Object.keys(obj));
// ['2', '10', 'b', 'a']
原因是:
text
2、10
↓
数组索引属性
↓
数值升序
b、a
↓
普通字符串属性
↓
创建顺序
进一步追问
Object.keys() 只返回:
text
自身
+
可枚举
+
字符串键
所以 Symbol 不会返回。
如果需要获取对象的全部自身属性键:
javascript
Reflect.ownKeys(obj);
如果业务真正要求的是严格按照插入顺序管理任意类型的键值集合,应该优先考虑:
javascript
Map
最终一句话
所以,"Object.keys 是无序的"是过时且不准确的说法;现代 JavaScript 中它是有确定规则的,只是这个规则不是单纯的插入顺序,而是"数组索引升序 + 普通字符串创建顺序 + Symbol 创建顺序"。
这套答案基本可以做到 "能背下来 → 能展开讲 → 能应对追问"。