关于JS对象的属性顺序

156 阅读4分钟

背景:
今天因为遇到json diff有多余diff问题,所以就来研究了一下js对象的key值排序,排序之后即可减少多余diff

一、JavaScript 对象属性的排序规则(ES2015+ 规范)

在 ES2015(ES6)之前,规范未定义对象属性的遍历顺序,完全由浏览器引擎实现决定(表现混乱)。ES2015 及后续版本明确了对象属性的排序逻辑,核心分为三类属性,按优先级依次排序:

1. 第一优先级:数值型属性(Numeric Properties)

  • 定义:属性名可被强制转换为 32 位无符号整数的属性(如 1230xFF,但 '123.5' 不属于此类)。

  • 排序规则:严格按照数值大小升序排列(与赋值顺序无关)。

    • 示例:

      javascript

      const obj = { 10: 'a', 2: 'b', 100: 'c' };
      console.log(Object.keys(obj)); // 输出 ["2", "10", "100"](按数值升序)
      

2. 第二优先级:字符串型属性(String Properties,非数值)

  • 定义:属性名是字符串且无法转换为 32 位无符号整数的属性(如 'name''age18''123.5')。

  • 排序规则:严格按照首次赋值的顺序排列(插入顺序)。

    • 示例:

      const obj = { name: 'Alice', age: 20, city: 'Beijing' };
      console.log(Object.keys(obj)); // 输出 ["name", "age", "city"](与赋值顺序一致)
      

3. 第三优先级:Symbol 型属性(Symbol Properties)

  • 定义:属性名是 Symbol 类型的属性(如 Symbol('id'))。

  • 排序规则:严格按照首次赋值的顺序排列(插入顺序),且排在所有字符串属性之后。

    • 示例:
      const obj = { 
        [Symbol('id')]: 1, 
        name: 'Bob', 
        [Symbol('age')]: 25 
      };
      console.log(Object.keys(obj)); // 输出 ["name"](Symbol 属性不被 keys() 遍历,但内部排序为 name → Symbol(id) → Symbol(age))
      

二、排序规则的设计原因(基于 ECMA 规范的核心诉求)

ECMA 规范对属性排序的设计,本质是平衡 “开发者直觉” 与 “引擎性能”“历史兼容性”  的结果,具体可拆解为两点:

1. 数值型属性按大小排序:适配 “类数组” 场景的直觉

早期 JavaScript 中,开发者常使用对象模拟数组(如 {0: 'a', 1: 'b'}),但 ES 规范未定义排序前,不同引擎对数值键的处理混乱(如按插入顺序、按哈希表顺序等)。ES2015 明确数值键按大小排序,核心是为了适配开发者对 “索引” 的直觉认知:数值键本质上类似数组的索引(index),而数组的索引天然是按数值升序排列的。这种设计让对象可以更可靠地模拟 “类数组结构”(如 arguments 对象、DOM 集合等),同时避免引擎对数值键的特殊处理逻辑过于复杂。

此外,数值键的排序逻辑基于 “32 位无符号整数”,是因为 JavaScript 数组的索引范围本身就是 0 到 2³²-2(超出则视为常规对象属性),这种对齐减少了引擎的适配成本。

2. 非数值属性按赋值顺序排序:满足 “可预测性” 需求

对于字符串键(如 'name''city'),开发者的核心诉求是 “赋值顺序可预测”—— 即先定义的属性先被遍历。ES2015 之前,由于对象底层多基于哈希表实现(哈希表本身无序),遍历顺序不可靠,导致依赖属性顺序的代码(如序列化、配置项遍历)在不同浏览器中表现不一致。

ES2015 强制非数值属性按插入顺序排序,本质是牺牲了部分哈希表的性能优势,换取了开发者对代码行为的可预测性。这一设计符合大多数业务场景的需求(开发者更关心 “顺序可控” 而非极致的属性访问性能),同时也与 Map 数据结构(天然按插入顺序排序)形成了逻辑对齐(Map 是 ES2015 同时引入的,专门用于强调顺序的键值对场景)。

三、特殊注意点

  • 兼容性:ES2015 之前的环境(如 IE8 及以下)仍遵循引擎自身的排序逻辑,需避免在旧环境中依赖属性顺序。
  • “数值字符串” 的判定:只有能被精确转换为 32 位无符号整数的字符串才视为数值键,例如 '0123'(转换为 83,因前缀 0 视为八进制)、'123.0'(转换为 123)属于数值键;而 '123.5''abc' 属于字符串键。
  • 与 Map 的区别:对象的排序是 “混合优先级”(数值键优先),而 Map 的所有键(包括数值、字符串、Symbol)均严格按插入顺序排序,且支持更灵活的键类型,适合强依赖顺序的场景。