在我们日常开发过程中,哈希映射表(Hash Map)肯定是最常用的数据结构之一。它以键值对的形式存储数据,所以很容易通过键(Key)来获取对应的值(Value)。在 Java 中,可以明确的使用 HashMap 对象,但是在 Javascript 里,大家普遍默认使用的 Object 来作为 HashMap 使用:

const map = {};

// 键值对
map['key1'] = 'value1';
map['key2'] = 'value2';
map['key3'] = 'value3';

// 检查 map 是否有 'key1' 属性
if (map.hasOwnProperty('key1')) {
  console.log('Map contains key1');
}

// 获取 'key1' 属性对应的值
console.log(map['key1']);

但其实,Javascript 里也有内建的一种数据结构专门用来作为 HashMap 来使用:Map。Map 对象是 es6 的标准,因此在 es5 中要配合适当的 polyfill 来使用。不过不用担心,大部分现代浏览器都已经很好的支持了 Map 对象。

Map 对象的浏览器兼容性

上面是 Map 对象在 MDN 中的列出的浏览器兼容性,可以看到,已经被微软抛弃的 IE11 部分支持,其他平台几乎都是完全支持,绝对可以放心食用。

下面我解释一下为什么要用 Map 而不是 Object。

键支持更丰富的数据类型

Object 的键只能是 Symbol 对象或者字符串。而 Map 可以使用任意数据类型作为键,甚至可以用 Object 或者 Function,只有想不到没有做不到:

const map = new Map();
const myFunction = () => console.log('I am Function');
const myNumber = 666;
const myObject = {
  name: 'objectValue',
  otherKey: 'otherValue'
};
map.set(myFunction, 'Function 作为键');
map.set(myNumber, 'Number 作为键');
map.set(myObject, 'Object 作为键');

console.log(map.get(myFunction)); // Function 作为键
console.log(map.get(myNumber)); // Number 作为键
console.log(map.get(myObject)); // Object 作为键

计算键值对数量的消耗低

因为 Map 提供的一个叫做 size 的属性,用来统计键值对的数量,因此所需要的时间复杂度是 O(1)。而 Object 则需要通过 Object.keys() 来统计,所需要的时间复杂度是 O(n)。

const map = new Map();
map.set('key1', 1);
map.set('key2', 1);
...
map.set('key100', 1);

console.log(map.size) // 100, 时间复杂度: O(1)

const objMap = {};
objMap['key1'] = 1;
objMap['key2'] = 1;
...
objMap['key100'] = 1;

console.log(Object.keys(objMap).length) // 100, 时间复杂度: O(n)

性能更优

Map 针对高频地插入或者删除键值对的场景进行过优化。还有就是上一条说的,Map 可以瞬间获取键值对的数量,而 Object 则需要进行计算。

我在 Macbook Pro 2020 上进行过测试,1000 万个键值对,用 Map 和 Object 分别获取其键值对的数量,最终的平均耗时如下:

  • Object: ~ 1.6 s
  • Map: < 1 ms

再说,使用过程中不需要把键转换成字符串,数据量大的情况下可以节约不少时间。

迭代方式直接

Object 必须先拿到所有键,然后才能进行迭代。而 Map 是可迭代的(iterable),这意味着可以直接进行迭代。

const map = new Map();
map.set('key1', 1);
map.set('key2', 2);
map.set('key3', 3);

for (let [key, value] of map) {
  console.log(`${key} = ${value}`);
}
// key1 = 1
// key2 = 2
// key3 = 3

const objMap = {};
objMap['key1'] = 1;
objMap['key2'] = 2;
objMap['key3'] = 3;

for (let key of Object.keys(objMap)) {
  const value = objMap[key];
  console.log(`${key} = ${value}`);
}
// key1 = 1
// key2 = 2
// key3 = 3

键是有顺序的

之前公司有一个项目,有大量的多层级分类要显示,一开始用 Object 存储来渲染,突然有一天这些分类需要按特定的顺序显示,用 Map 改起来不要太爽。不然就要转成字典数组或者新引进数组用来排序,不论哪一个都没有 Map 来的直接,来的优雅。

不存在属性冲突

Javascript 中一个 Object 其实已经包含了一些属性(property),在使用的过程中可能会有属性冲突,而 Map 不存在这种问题。

⚠️注意:从 ECMAScript 2015 开始,你可以用 Object.create(null) 这样的方式创建纯净的对象,来避免属性冲突导致的问题,Vuejs 就是用的这种方法。

const map = new Map();
map.set('key1', 1);
map.set('key2', 2);
map.set('toString', 3); // Map 不会导致 toString 原生方法被覆盖

const objMap = {};
objMap['key1'] = 1;
objMap['key2'] = 2;
objMap['toString'] = 3; // toString 原生方法没啦