先记住这个答案
Map 按键第一次被插入的顺序遍历,不管键是字符串、数字还是对象。对一个已经存在的键再调用 set,只会更新值,它在遍历里的位置不变;先 delete 再 set,才算重新插入,会排到最后。普通对象的规则复杂一些:像 "2"、"10" 这样的整数键,会按数值从小到大排在最前面,其余的字符串键按创建顺序排在后面,Symbol 键排在最后,而且 Object.keys 根本不返回 Symbol 键。
Map按插入顺序遍历,和键的类型无关- 对已有键
set只改值,不改位置 - 先
delete再set,键才会移到最后 - 对象的整数键按数值升序,排在其他键前面
同样的插入顺序,两种结果
先插入 '10',再插入 '2',分别放进 Map 和普通对象,然后试一下更新和删除后重新插入。
const map = new Map([['10', 'ten'], ['2', 'two']])
const object = { '10': 'ten', '2': 'two' }
console.log(JSON.stringify([...map.keys()])) // Map 按插入顺序
console.log(JSON.stringify(Object.keys(object))) // 对象按整数大小
map.set('10', 'updated') // 更新已有键
console.log(JSON.stringify([...map.keys()]))
map.delete('10')
map.set('10', 'again') // 删除后重新插入
console.log(JSON.stringify([...map.keys()]))查看输出与解释
["10","2"]
["2","10"]
["10","2"]
["2","10"]前两行是同样的插入顺序,Map 保持了 '10' 在前,对象却把 '2' 排到了前面,因为这两个键都是整数形式的字符串,对象会按数值排。第三行说明更新值不改位置。第四行删除后再插入,'10' 才跑到最后。
用 Map 实现 LRU 缓存
LRU 缓存的意思是容量满了就淘汰最久没用过的那一项。Map 的插入顺序正好能用来记录「谁最久没用」:最前面的就是最老的。
- 访问时先删再插
- 读取一个键时,要把它挪到最后,表示刚刚用过。但只调用
set是不行的,前面说过更新值不会改变位置。正确做法是先delete再set。容量超了就删掉第一个键,用map.keys().next().value就能拿到它。 这是面试手写 LRU 最常见的解法,每次读写都是O(1)。关键就在于分清「更新」和「重新插入」对顺序的影响不同。
容易答错的地方
- 以为对已有键
set会把它移到最后 set已有的键只改值,位置不动。需要刷新顺序,必须先delete再set。- 说对象的属性顺序是随机的
- 现在的规范对对象自有属性的顺序有明确规则:整数键按数值升序在前,字符串键按创建顺序在中间,Symbol 键按创建顺序在最后。ES2015 先给
Reflect.ownKeys这类方法定了这个顺序,ES2020 又把Object.keys和for...in也统一了进来,只是Object.keys不含 Symbol 键。 - 用对象的键顺序代替业务排序
- 如果表格要按编号排序,应该显式调用
sort。依赖对象恰好把整数键排好,一旦编号变成"A01"或带前导零的"007",排序就不成立了。
面试官还会怎么问?
键 "02" 会和 "2" 一样被排到前面吗?
不会。只有标准的整数形式才会被当成整数键,"02" 转成数字再转回字符串是 "2",和原来不一致,所以它是普通字符串键,按插入顺序排。
用对象当 Map 的键,改了对象的字段会影响顺序吗?
不会。Map 按对象的引用识别键,改字段不改引用,还是同一个键,位置不变。
把 Map 转成对象,顺序还在吗?
不一定。Object.fromEntries(map) 之后,整数形式的键会被挪到前面,原来的插入顺序就乱了。而且非字符串的键会被转成字符串,数字 1 和字符串 '1' 会合并成一个。需要保留顺序和键类型,就转成条目数组 [...map]。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。