在 JavaScript 开发中,对象属性的存储顺序和运算符的执行优先级常常是容易混淆的细节,也是面试中的高频考点。本文将通过一道具体的代码输出题,深入剖析 V8 引擎中对象属性的分类机制以及赋值表达式的求值顺序,帮助你在实际编码中避免类似的陷阱。
题目代码- const obj = {
- a: 0,
- };
- obj['1'] = 0;
- obj[++obj.a] = obj.a++;
- const values = Object.values(obj);
- obj[values[1]] = obj.a;
- console.log(obj);
复制代码 要求:写出最终 console.log(obj) 的输出结果。
前置知识一:对象属性的常规属性和排序属性
ECMAScript 规范规定了数字属性按索引大小升序排列,而字符串属性则按创建时的顺序排列。V8 引擎内部为此实现了两种不同的存储结构:数字属性称为 elements(排序属性),存放方式类似数组;字符串属性称为 properties(常规属性),按添加顺序存放。
例如以下代码的 for...in 遍历结果会先输出数字键(升序),再输出字符串键(按添加顺序):- const obj = {};
- obj['b'] = 'b';
- obj[100] = '100';
- obj[1] = '1';
- obj['a'] = 'a';
- obj[50] = '50';
- obj['c'] = 'c';
- for (const key in obj) {
- console.log(`key: ${key} value: ${obj[key]}`);
- }
- // 输出顺序:
- // key: 1 value: 1
- // key: 50 value: 50
- // key: 100 value: 100
- // key: b value: b
- // key: a value: a
- // key: c value: c
复制代码 这个特性直接影响 Object.keys、Object.values 和 for...in 的遍历顺序,理解它是正确解答本题的基础。
前置知识二:运算符执行顺序与赋值表达式求值规则
运算符的优先级从高到低简化如下(仅列本题相关部分):
- 属性访问:. 或 []
- 递增/递减:后置 ++/--、前置 ++/--
- 算术:+、- 等
- 比较、相等、逻辑、三元、赋值(优先级最低)
对于赋值表达式 LHS = RHS,ECMAScript 规范(AssignmentExpression evaluation)规定:
1. 先计算左侧表达式(LHS),得到要写入的“引用”(Reference),即确定写到哪里。
2. 再计算右侧表达式(RHS),得到要写入的值。
3. 最后将右侧的值写入左侧引用。
注意:计算左侧引用时,如果包含操作(如 ++obj.a),会先执行该操作以获取最终的属性名。
看一个辅助理解的例子:- const obj = {
- get a() {
- console.log('获取 a 的值');
- return 1;
- },
- get b() {
- console.log('获取 b 的值');
- return 2;
- }
- };
- obj[++obj.a] = obj.b++;
- console.log('obj: ', obj);
复制代码 执行顺序:
- 左侧求引用:先访问 obj.a(获得 getter 返回值 1),再执行前置递增 ++obj.a,结果变为 2,因此最终写入的属性名为 '2'。
- 右侧求值:访问 obj.b 获得 2,后置递增 obj.b++ 后 obj.b 变为 3,但右侧表达式返回的值仍是 2。
- 赋值:将 2 写入 obj['2']。
逐行分析原题代码
初始对象:执行 obj['1'] = 0; 后,对象包含两个属性:数字属性 '1' 和字符串属性 'a',按 elements 规则,数字键 '1' 在前,字符串键 'a' 在后。当前状态:接着执行 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。
对象状态变成:执行 const values = Object.values(obj); 由于数字属性 '1' 在前,values 数组为 [1, 2]。
执行 obj[values[1]] = obj.a; values[1] 是 2,obj.a 当前值为 2,所以 obj[2] = 2。
最终对象:- {
- "1": 1,
- "2": 2,
- "a": 2
- }
复制代码 console.log(obj) 在 Chrome 等 V8 控制台中输出:{1: 1, 2: 2, a: 2}。
小结与启示
本题综合了两个易错点:
1. 对象中数字属性(elements)永远优先于字符串属性(properties)被遍历,且按升序排列。
2. 赋值表达式先确定左侧引用(包含其中的操作),再计算右侧值,这对后置递增和前置递增的时机有决定性影响。
理解这些底层机制能帮助开发者更准确地预测代码行为,避免在复杂操作中产生预期之外的 Bug。在日常开发中,尽量避免在同一表达式中对同一属性进行多次读写操作,以保持代码的清晰可维护。 |