JavaScript 面试题:Object.keys() 的返回顺序是无序的吗?

一、核心结论

Object.keys() 不是无序的。现代 JavaScript 已经规定了明确的属性遍历顺序:

text 复制代码
整数索引键 → 按数值升序
普通字符串键 → 按属性创建/插入顺序
Symbol 键 → 按属性创建/插入顺序

但是:

text 复制代码
Object.keys()
Object.values()
Object.entries()

都不会返回 Symbol 键。 如果要完整获取对象的所有自有属性键,应使用:

javascript 复制代码
Reflect.ownKeys(obj)

二、核心思路(一句话)

现代 JavaScript 对象属性不是"无序",而是按照"整数索引优先升序 → 其他字符串按创建顺序 → Symbol 按创建顺序"的规则排列;Object.keys() 只返回其中的可枚举字符串键。


三、主要矛盾与次要矛盾

主要矛盾

面试真正考察的是:

你是否知道 JavaScript 对象属性的规范化排序规则。

次要矛盾

需要进一步区分:

  1. 什么叫"整数索引键"
  2. 字符串键和 Symbol 键的顺序
  3. Object.keys() 到底返回哪些属性
  4. Object.values()、Object.entries() 是否遵循相同顺序
  5. Reflect.ownKeys() 与 Object.keys() 的区别
  6. 旧 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 创建顺序"。

这套答案基本可以做到 "能背下来 → 能展开讲 → 能应对追问"。

相关推荐
朝朝辞暮i7 小时前
C++ 第 23 课:class —— 开始真正进入面向对象
开发语言·c++·算法
小师兄吃牛肉8 小时前
C语言篇:宏
c语言·开发语言
郑同学的笔记8 小时前
【c++随笔28】strcmp 和 memcmp对比
开发语言·c++
言乐69 小时前
Python根据无法识别搜索词找出可能输入内容模型
开发语言·python·django·virtualenv·pygame
weixin_307779139 小时前
有限产能智能排产与动态重排智能体:从需求解构到技术实现
开发语言·人工智能·算法·架构
kakakahahahaha9 小时前
Windows C盘临时文件清理:%temp%、Windows Temp、Prefetch、Windows.old与hiberfil.sys处理指南
c语言·开发语言·windows
朝朝辞暮i9 小时前
C++ 第 27 课:智能指针 shared_ptr
开发语言·c++·算法
朝朝辞暮i10 小时前
C++ 第 29 课:回调函数 Callback
开发语言·c++·算法
笨笨饿10 小时前
140_AI新手村MCP与Skills是干嘛的
开发语言·人工智能·python·stm32·单片机·嵌入式硬件·物联网