javascript对象拷贝后键序由规范与引擎共同决定:object.assign和展开运算符按数字键升序、字符串键插入序、symbol键插入序;json.parse会转键为字符串并重排数字键;for...in在旧环境顺序不可靠;现代引擎自有属性枚举顺序已统一,但原型链或不可枚举属性仍有差异。

JavaScript 中对象属性的枚举顺序,直接影响“拷贝后对象”的键名排列表现——尤其在跨浏览器或跨引擎环境中。这种影响不是偶然的,而是由规范演进、引擎实现和拷贝方式三者共同决定的。
关键点在于:对象本身不保证顺序,但现代引擎(V8、SpiderMonkey、JavaScriptCore)对自有属性的枚举顺序已高度统一;而“拷贝”行为是否保留该顺序,取决于你用的是哪种拷贝方法。
一、拷贝操作如何继承/破坏原始枚举顺序
不同拷贝方式对属性顺序的处理逻辑不同:
Object.assign({}, obj)
遵循 ES2015 枚举规则:先数字键(数值升序),再字符串键(插入序),最后 Symbol 键(插入序)。
它会按Object.keys(obj)的顺序逐个赋值,因此结果对象的键序与Object.keys(obj)一致。展开运算符
{...obj}
行为等价于Object.assign,同样遵循标准枚举顺序,不保留原始定义顺序中的“非数字键乱序”。JSON.parse(JSON.stringify(obj))
会丢失函数、Symbol、undefined、NaN、Infinity 等值;
数字键仍按数值升序排列,但所有键都会被转为字符串,且不可枚举属性、原型链属性全部丢失;
实际上是“重建一个新对象”,其键序由 JSON 解析器内部枚举逻辑决定(现代环境也遵循 ES2015 规则)。浅拷贝循环(如
for (let k in obj))
若未加hasOwnProperty判断,可能混入原型链属性,且for...in在旧引擎中顺序不可靠;
即使加了判断,for...in在部分早期规范兼容模式下仍可能不按插入序(如 IE8 及更早),导致拷贝后顺序错乱。
二、浏览器引擎差异在拷贝后是否还存在
答案是:对自有属性而言,基本不存在差异;但对原型链、不可枚举属性、或使用 for...in 的场景,仍有风险。
-
所有主流现代引擎(Chrome/V8、Firefox/SpiderMonkey、Safari/JavaScriptCore)自 ES2015 起,对
Object.keys()、Object.getOwnPropertyNames()、Object.assign()的自有属性枚举顺序完全一致:-
"10"、"2"、"1"→ 排列为"1"→"2"→"10"(数值升序) -
"name"、"id"、"01"→ 按原始插入顺序("01"不是整数键,归入字符串类) -
Symbol('a')、Symbol('b')→ 总是在最后,且按插入顺序
-
-
差异主要残留在:
- 使用
for...in且未过滤原型属性时(它会包含原型上的可枚举属性,而各引擎对原型属性的枚举顺序无强制约定); - 依赖
Object.getOwnPropertyNames()获取不可枚举属性时,虽然顺序规范,但 IE11 及更早版本存在 bug; - 服务端 Node.js(V8)与老版浏览器(如 Android 4.4 WebView)混合部署时,若未统一规范版本,可能遇到极少数边界 case。
- 使用
三、为什么“看起来顺序变了”?常见误判场景
很多开发者以为“拷贝后顺序乱了”,其实是以下原因:
- 原始对象用了纯数字字符串键(如
"2023"、"2024"),拷贝后自动升序 → 显示为"2023"在"2024"前,符合预期,但不符合人眼“时间倒序”习惯; - 后端返回 JSON 字符串,前端解析成对象,数字键被重排 —— 这不是拷贝问题,是 JS 解析器的标准行为;
- 使用了
Map或数组做中间结构再转对象,却忘了 Map 的.keys()是插入序,而转成普通对象后又触发了数字键重排。
例如:
const data = { "2025": "new", "2023": "old", "name": "car" };
console.log(Object.keys({...data})); // ["2023", "2025", "name"]
这不是引擎 bug,是规范要求。
四、确保拷贝后键序可控的实用建议
- ✅ 需要严格插入顺序 → 改用
Map,它不参与任何自动排序,.keys()、.entries()全部保持插入序; - ✅ 必须用对象,且含数字键但不想被重排 → 统一加前缀,如
"year-2023"、"year-2025"; - ✅ 序列化/传输需确定性顺序 → 不依赖对象遍历,改用
Object.entries(obj).sort(...)显式排序后再处理; - ✅ 调试或展示用 → 用
JSON.stringify(obj, null, 2),它遵循枚举顺序,且可读性强(注意 Symbol 和函数会被忽略); - ❌ 避免依赖
for...in做顺序敏感操作,尤其在需兼容老环境时。
不复杂但容易忽略











