你知道JavaScript中的Map类型吗

372 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第11天,点击查看活动详情

什么是Map类型

Map 是ES6新增的一种集合类型,它给Javascipt为这门语言带来了真正的键/值存储机制;

Map 的大多数特性都可以通过 Object 类型实现,但二者之间还是存在一些细微的差异;对比object键值规范,它的特殊点在于可以将任何值都作为一个键或一个值,在个别场景下 Map 的使用起来也比 Object巧妙的多,相信看完本文,你就清楚该怎么去运用 Map 给你带来的好处了

基本使用

一个 Map 对象在迭代时会根据对象中元素的插入顺序来进行一个 for...of 循环在每次迭代后会返回一个形式为 键值对[key,value] 的数组

初始化映射

最基础的定义,使用 new 关键字搭配 Map 构造函数创建一个空映射:

 const m = new Map();

嵌套数组初始化映射

如果想在创建的同时初始化实例,可以给 Map 构造函数传入一个可迭代对象,需要包含 键/值对数组

可迭代对象中的每个 键/值 对都会按照迭代顺序插入到新映射实例中:

 const mm = new Map([['a', 1], ['b', 2]]);

来看看分别打印什么:

image-20220608224027705

由上图可见,Map 默认会展示当前长度 size :如 Map(0) 与 Map(2) ,分别对应着有几个元素;通过第二个嵌套数组的映射我们可以清晰的看到每个数组在Map中以键值 key => val 的形式存储

使用自定义迭代器初始化映射

我们可以通过 Symbol.iterator 定义默认迭代器和function *定义生成器函数来搭配使用

 const m = new Map({
     [Symbol.iterator]: function* () {
         yield ["key1", 'vall1'];
         yield ["key2", 'vall2'];
         yield ["key3", 'vall3'];
     }
 })
 // Map(3) {'key1' => 'vall1', 'key2' => 'vall2', 'key3' => 'vall3'}

它可以使用 for of 进行迭代,但是并不能通过next()来迭代每一个值,得通过其他方法来转换成迭代对象

内置方法

Map有独自的内置方法,供于使用时方便删除查找,常用的方法与属性如下:

语法作用返回
.size用于返回 Map 对象的成员数量(长度)Number
.set(key, value)添加或更新指定键(key)和值(value)的键值对Map对象
.has(key)表明 Map 中是否存在指定元素true or false
.get(key)获取某个 Map 对象中的指定元素指定元素 or undefined
.delete(key)移除 Map 对象中指定的元素true or false
.clear()移除 Map 对象中的所有元素undefined

使用示例

便于美观简洁,省略了 console.log

 const m2 = new Map([['key1', 'vall1'], ['key2', 'vall2']]);
 ​
 m2.set("key3", "val3").set("key4", "val4"); // 设置key3和key4
 ​
 // m2 :  Map(4) {'key1' => 'vall1', 'key2' => 'vall2', 'key3' => 'val3', 'key4' => 'val4'}
 m2.has('key2')// true
 m2.get('key1') // vall1
 m2.delete('key1') // true
 m2.size // 3
 m2.clear(); // 清空 
 m2.size // 0

迭代方法

Map 提供了迭代方法使其增加了拓展性,这里列举了四个常用的迭代方法:

Map.prototype[@@iterator]()

返回一个新的迭代对象,其为一个包含 Map 对象中所有键值对的 [key, value] 数组,并以插入 Map 对象的顺序排列

Map.prototype.keys()

返回一个新的迭代对象,其中包含 Map 对象中所有的,并以插入 Map 对象的顺序排列

Map.prototype.values()

返回一个新的迭代对象,其中包含 Map 对象中所有的,并以插入 Map 对象的顺序排列

Map.prototype.entries()

返回一个新的迭代对象,其为一个包含 Map 对象中所有键值对的 [key, value] 数组,并以插入 Map 对象的顺序排列

Map.prototype.forEach(callbackFn[, thisArg])

如果不使用迭代器,而是使用回调方式,则可以调用映射的 forEach(callback, opt_thisArg) 方法并传入回调,依次迭代每个键/值对 ; 传入的回调接收可选的第二个参数,这个参数用于重写回调内部 this 的值

特殊使用

将 NaN 作为 Map 的键

虽然 NaN 与任何值甚至与自己都不相等(NaN !== NaN 返回 true),但是因为所有的 NaN 的值都是无法区分的,所以下面这种写法是成立的:

 const myMap = new Map()
 myMap.set(NaN, 'not a number')
 ​
 myMap.get(NaN)  // "not a number"
 ​
 const otherNaN = Number("I'm a Number")
 console.log(otherNaN); // NaN
 myMap.get(otherNaN)  // "not a number"

因为使用Number("") 转换后的 otherNaNNaN,因此调用.get(otherNaN)就相当于将键名NaN在Map中找值,则 value"not a number"

数组与Map互相转换

使用常规的 Map 构造函数可以将一个二维键值对数组转换成一个 Map 对象

 const kvArray = [["key1", "value1"], ["key2", "value2"]];
 const myMap = new Map(kvArray);
 ​
 myMap.get("key1"); // 返回值为 "value1"
 // Map(2) { 'key1' => 'value1', 'key2' => 'value2' }

使用 Array.from 函数或展开运算符可以将一个 Map 对象转换成一个二维键值对数组

 Array.from(myMap) // 输出和 kvArray 相同的数组
 [...myMap] // 更简洁的方法来做如上同样的事情,使用展开运算符

选择 Object 还是 Map

对于多数 Web 开发任务来说,选择 Object 还是 Map 只是个人偏好问题,影响不大;不过,对于在乎内存和性能的开发者来说,对象和映射之间确实存在显著的差别。

内存占用

Object 和 Map 的工程级实现在不同浏览器间存在明显差异,但存储单个键/值对所占用的内存数量都会随键的数量线性增加。批量添加或删除键/值对则取决于各浏览器对该类型内存分配的工程实现。不同浏览器的情况不同,但给定固定大小的内存,Map 大约可以比 Object 多存储 50%的键/值对

插入性能

向 Object 和 Map 中插入新键/值对的消耗大致相当,不过插入 Map 在所有浏览器中一般会稍微快一点儿。对这两个类型来说,插入速度并不会随着键/值对数量而线性增加。如果代码涉及大量插入操作,那么显然 Map 的性能更佳。

查找速度

与插入不同,从大型 Object 和 Map 中查找键/值对的性能差异极小,但如果只包含少量键/值对,则 Object 有时候速度更快;在把 Object 当成数组使用的情况下(比如使用连续整数作为属性),浏览器引擎可以进行优化,在内存中使用更高效的布局。这对 Map 来说是不可能的。对这两个类型而言,查找速度不会随着键/值对数量增加而线性增加。如果代码涉及大量查找操作,那么某些情况下可能选择 Object 更好一些。

删除性能

使用 delete 删除 Object 属性的性能一直以来饱受诟病,目前在很多浏览器中仍然如此; 为此,出现了一些伪删除对象属性的操作,包括把属性值设置为 undefined 或 null。对大多数浏览器引擎来说,Map 的 delete()操作都比插入和查找更快,如果代码涉及大量删除操作,那么毫无疑问应该选择 Map。

MDM中给出的建议

这三条提示可以帮你决定用Map还是Object

  • 如果键在运行时才能知道,或者所有的键类型相同,所有的值类型相同,那就使用Map
  • 如果需要将原始值存储为键,则使用Map,因为Object将每个键视为字符串,不管它是一个数字值、布尔值 还是任何其他原始值。
  • 如果需要对个别元素进行操作,使用Object

最后如果本文对于本文有疑惑,还请指导勘正 (●'◡'●)