在JavaScript里实现整数到罗马数字的转换,最常见思路是准备一张数值与符号的对应表,然后不断用当前数字减去能匹配的最大面值。不少写法为了省事,直接把对应表写成一个普通对象,键用数值,值用罗马符号,接着用循环遍历这个对象。这种做法在表面上看似直观,却隐藏着一个和语言规范紧密相关的属性遍历顺序问题,稍不注意就会让转换结果完全错误。

对象属性遍历顺序的底层规范
JavaScript对象属性的遍历顺序并不是完全随意的,而是由ECMAScript规范严格定义的。对于普通对象而言,如果属性的键是字符串,但能够被转换成整数索引(也就是像"1000"、"500"这种纯数字字符串,或者干脆用数字当键名),那么这些属性会被视为数组索引属性,在遍历时严格按照数值从小到大排列。剩下的字符串键名则按照它们被添加到对象中的先后顺序出现,而Symbol类型的键永远排在最后。
这意味着当你写出类似{1000:'M',900:'CM',500:'D'}这样的字面量时,虽然代码里是先写1000再写900,但用for...in、Object.keys或者Object.entries拿到的顺序却是1000、500、900,甚至在某些实现里完全按数字升序重排。如果罗马数字转换函数依赖从大到小依次尝试减去面值,这种顺序反转会直接导致函数优先用掉小面值,最终拼出错误甚至无限循环的字符串。
我们可以通过一段简单的代码观察这个现象。下面这个例子定义了一个数值键对象并打印遍历结果,能清楚看到顺序和源码书写不一致:
const map = {
1000: 'M',
900: 'CM',
500: 'D',
400: 'CD',
100: 'C'
};
for (const key of Object.keys(map)) {
console.log(key, map[key]);
}
// 输出顺序可能是 100,1000,400,500,900 等升序排列
罗马数字转换中的错误写法与后果
一个典型的错误转换函数会直接遍历上述对象,并假设遍历顺序就是从大到小。比如下面这段代码,本意是从最大面值开始匹配,但由于属性遍历顺序被规范重排,实际匹配时可能先遇到100,导致传入的数字被拆成一堆小符号,或者因为无法正确递减而输出异常。
错误示例如下,注意这里用数字作键,且直接在循环里做贪心减法:
function badIntToRoman(num) {
const table = {
1000: 'M',
900: 'CM',
500: 'D',
400: 'CD',
100: 'C',
90: 'XC',
50: 'L',
40: 'XL',
10: 'X',
9: 'IX',
5: 'V',
4: 'IV',
1: 'I'
};
let result = '';
for (const k of Object.keys(table)) {
const val = Number(k);
while (num >= val) {
result += table[k];
num -= val;
}
}
return result;
}
console.log(badIntToRoman(1994)); // 可能得到错误结果而非 MCMXCIV
这种写法的危害不仅是结果错,更在于它在不同引擎或不同对象构造方式下表现可能略有差异,难以调试。开发者往往以为自己写的是从大到小,其实运行时完全相反。尤其在单元测试只覆盖了小数字时,问题会被掩盖,直到处理接近千位的数字才暴露。
稳定可靠的转换方案与最佳实践
要避免这个陷阱,最直白的办法是放弃用数字键对象承载顺序,改用数组保存从大到小排列的条目。数组的遍历顺序就是下标顺序,不受对象属性规则影响,逻辑清晰且性能更好。下面给出一种正确的实现方式:
function intToRoman(num) {
const list = [
[1000, 'M'],
[900, 'CM'],
[500, 'D'],
[400, 'CD'],
[100, 'C'],
[90, 'XC'],
[50, 'L'],
[40, 'XL'],
[10, 'X'],
[9, 'IX'],
[5, 'V'],
[4, 'IV'],
[1, 'I']
];
let result = '';
for (const [val, sym] of list) {
while (num >= val) {
result += sym;
num -= val;
}
}
return result;
}
console.log(intToRoman(1994)); // 正确输出 MCMXCIV
如果一定要用对象,也必须显式对键做降序排序,例如Object.keys(table).map(Number).sort((a,b)=>b-a),但这样多了一步转换和排序开销,不如数组直观。此外,使用Map结构也能保证插入顺序,但普通对象在数字键上的陷阱依然值得铭记。掌握这些差异后,罗马数字转换这类小工具就能写得既简短又可靠,不再被语言细节绊倒。
总结来说,JavaScript对象属性遍历顺序规则是真实存在的底层约束,不是浏览器bug。在罗马数字转换场景中,只要涉及面值匹配顺序,就必须绕开数字键对象的隐性重排。用数组或显式排序,才是符合预期且易维护的做法。
JavaScript罗马数字转换对象属性遍历修改时间:2026-08-18 13:58:29