查看: 88|回复: 1

JS对象属性排序与运算符优先级:一道经典面试题解析

[复制链接]
发表于 2 小时前 | 显示全部楼层 |阅读模式
在 JavaScript 开发中,对象属性的存储顺序和运算符的执行优先级常常是容易混淆的细节,也是面试中的高频考点。本文将通过一道具体的代码输出题,深入剖析 V8 引擎中对象属性的分类机制以及赋值表达式的求值顺序,帮助你在实际编码中避免类似的陷阱。

题目代码
  1. const obj = {
  2.   a: 0,
  3. };
  4. obj['1'] = 0;
  5. obj[++obj.a] = obj.a++;
  6. const values = Object.values(obj);
  7. obj[values[1]] = obj.a;
  8. console.log(obj);
复制代码
要求:写出最终 console.log(obj) 的输出结果。

前置知识一:对象属性的常规属性和排序属性
ECMAScript 规范规定了数字属性按索引大小升序排列,而字符串属性则按创建时的顺序排列。V8 引擎内部为此实现了两种不同的存储结构:数字属性称为 elements(排序属性),存放方式类似数组;字符串属性称为 properties(常规属性),按添加顺序存放。

例如以下代码的 for...in 遍历结果会先输出数字键(升序),再输出字符串键(按添加顺序):
  1. const obj = {};
  2. obj['b'] = 'b';
  3. obj[100] = '100';
  4. obj[1] = '1';
  5. obj['a'] = 'a';
  6. obj[50] = '50';
  7. obj['c'] = 'c';
  8. for (const key in obj) {
  9.   console.log(`key: ${key} value: ${obj[key]}`);
  10. }
  11. // 输出顺序:
  12. // key: 1 value: 1
  13. // key: 50 value: 50
  14. // key: 100 value: 100
  15. // key: b value: b
  16. // key: a value: a
  17. // key: c value: c
复制代码
这个特性直接影响 Object.keys、Object.values 和 for...in 的遍历顺序,理解它是正确解答本题的基础。

前置知识二:运算符执行顺序与赋值表达式求值规则
运算符的优先级从高到低简化如下(仅列本题相关部分):
- 属性访问:. 或 []
- 递增/递减:后置 ++/--、前置 ++/--
- 算术:+、- 等
- 比较、相等、逻辑、三元、赋值(优先级最低)

对于赋值表达式 LHS = RHS,ECMAScript 规范(AssignmentExpression evaluation)规定:
1. 先计算左侧表达式(LHS),得到要写入的“引用”(Reference),即确定写到哪里。
2. 再计算右侧表达式(RHS),得到要写入的值。
3. 最后将右侧的值写入左侧引用。

注意:计算左侧引用时,如果包含操作(如 ++obj.a),会先执行该操作以获取最终的属性名。

看一个辅助理解的例子:
  1. const obj = {
  2.   get a() {
  3.     console.log('获取 a 的值');
  4.     return 1;
  5.   },
  6.   get b() {
  7.     console.log('获取 b 的值');
  8.     return 2;
  9.   }
  10. };
  11. obj[++obj.a] = obj.b++;
  12. console.log('obj: ', obj);
复制代码
执行顺序:
- 左侧求引用:先访问 obj.a(获得 getter 返回值 1),再执行前置递增 ++obj.a,结果变为 2,因此最终写入的属性名为 '2'。
- 右侧求值:访问 obj.b 获得 2,后置递增 obj.b++ 后 obj.b 变为 3,但右侧表达式返回的值仍是 2。
- 赋值:将 2 写入 obj['2']。

逐行分析原题代码
初始对象:
  1. const obj = { a: 0 };
复制代码
执行 obj['1'] = 0; 后,对象包含两个属性:数字属性 '1' 和字符串属性 'a',按 elements 规则,数字键 '1' 在前,字符串键 'a' 在后。当前状态:
  1. {
  2.   "1": 0,
  3.   "a": 0
  4. }
复制代码
接着执行 obj[++obj.a] = obj.a++; 这是本题核心。
- 左侧:++obj.a 先读取 obj.a(当前值为 0),然后自增为 1,并返回自增后的值 1。所以左侧引用为 obj['1']。
- 右侧:obj.a++ 是后置递增,先读取 obj.a 的当前值(此时 obj.a 已经因为上一步的 ++obj.a 变成了 1),所以右侧读取到的值是 1,然后 obj.a 再自增为 2。因此右侧表达式的结果是 1。
- 赋值:将 1 写入 obj['1'],obj['1'] 从 0 变成 1。此时 obj.a 已变为 2。

对象状态变成:
  1. {
  2.   "1": 1,
  3.   "a": 2
  4. }
复制代码
执行 const values = Object.values(obj); 由于数字属性 '1' 在前,values 数组为 [1, 2]。

执行 obj[values[1]] = obj.a; values[1] 是 2,obj.a 当前值为 2,所以 obj[2] = 2。

最终对象:
  1. {
  2.   "1": 1,
  3.   "2": 2,
  4.   "a": 2
  5. }
复制代码
console.log(obj) 在 Chrome 等 V8 控制台中输出:{1: 1, 2: 2, a: 2}。

小结与启示
本题综合了两个易错点:
1. 对象中数字属性(elements)永远优先于字符串属性(properties)被遍历,且按升序排列。
2. 赋值表达式先确定左侧引用(包含其中的操作),再计算右侧值,这对后置递增和前置递增的时机有决定性影响。

理解这些底层机制能帮助开发者更准确地预测代码行为,避免在复杂操作中产生预期之外的 Bug。在日常开发中,尽量避免在同一表达式中对同一属性进行多次读写操作,以保持代码的清晰可维护。
回复

使用道具 举报

发表于 2 小时前 | 显示全部楼层

Re: JS对象属性排序与运算符优先级:一道经典面试题解析

感谢楼主的详细解析!这道题确实把对象属性的存储顺序和运算符优先级这两个容易混淆的点结合得很好。你一步步拆解执行过程,尤其是对赋值表达式“先左后右”的规则解释得非常清晰,辅助例子也很有帮助。读完对 V8 的 elements 和 properties 机制有了更直观的理解。日常编码中确实应该避免在同一表达式里对同一属性多次操作,感谢分享实用技巧!
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-6-11 17:59 , Processed in 0.025213 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部