Map 数据结构

概述

Map 是 ECMAScript 6 中新增的一种数据结构,它完美高效的实现了 JavaScript 中使用 "键值对" 来存储数据的需求。在它出现之前,基本都是通过 Object 来创建 "键值对" 集合,但这种方式本身存在一些缺陷,使其在一些特殊情况下经常遇到比较棘手的问题,而 Map 的出现则终结了这一局面。为了充分体现出 Map 独特的优势,我们先来看一下使用 Object 作为数据结构时存在的四个问题。

Object 存在的问题

一、类型问题

Object 的键名只能是:字符串或 Symbol 类型,如果将其它类型的数据设置为键名,将会自动调用 toString 方法将其转换为字符串,并且无任何提示。

二、遍历问题

当使用 Symbol 类型作为键名时,会存在一个问题,由于 Symbol 不可枚举的特性,通过常规的遍历方法如:for...inObject.keys() 都无法遍历出这个键的,这会导致遍历操作出现问题。除非使用其它方式如:Object.getOwnPropertyDescriptorsReflect.ownKeys 等方法。

三、顺序问题

当通过 for...in 循环输出键值对时,是无法保证输出结果的顺序与插入数据的顺序始终一致的,这是因为 Object 有一套自己的遍历排序规则,当遇到像 "1""2""3" 这种正整数形式的字符串时,其遍历顺序将按照数值从小到大的规则进行排序,例如:

js 复制代码
const obj = {
    "a": "AAAA",
    "b": "BBBB",
    "1": "1111",
    "2": "2222"
};
for ( const key in obj ) {
    console.log( `${ key }: ${ obj[ key ] }` );
}

/** 
 * 若按照插入时的顺序
 * 则应该打印如下结果
 *   a: "AAAA"
 *   b: "BBBB"
 *   1: "1111"
 *   2: "2222"
 * 
 * 但实际打印结果却是
 *   1: "1111"
 *   2: "2222"
 *   a: "AAAA"
 *   b: "BBBB"
 */

四、原型问题

首先我们看一下这个例子:

js 复制代码
const obj = {
    a: 10,
    b: 20
};

这里定义了一个对象:obj,其中包含 ab 两个属性,我们可以轻松的获取对应的值:

js 复制代码
console.log( obj.a );   // 10
console.log( obj.b );   // 20

如果我们获取 "不存在" 的属性时,正常情况下应该返回 undefined,例如:

js 复制代码
console.log( obj.c );   // undefined
console.log( obj.d );   // undefined 

可是当我们获取一些 "特殊属性" 时,结果却出乎意料:

js 复制代码
console.log( obj.constructor );   // ƒ Object() { [native code] }
console.log( obj.valueOf );       // ƒ valueOf() { [native code] }

为什么会出现这样的结果呢?我们并没有在 obj 上设置 constructorvalueOf 属性,获取它们的值理应得到 undefined,可实际却是上面的结果,这就是 "原型对象继承属性" 问题。根本原因在于,每个对象都有原型,而原型上存在一些固定属性,这些属性是可以正常访问的,因此就会出现上面所遇到的问题。尽管这种问题在实际开发中并不常见,但它确实是一个隐患。那么除了 constructorvalueOf 以外,还有哪些属性呢?我们在控制台将 obj 打印出来看看:

js 复制代码
console.log( obj ); 

结果如上图所示,除了前面提到的两个属性外,还有 hasOwnPropertyisPrototypeOf 等多个属性,这些都是可以被读取的,尽管没有显式的声明它们,但它们却依旧真实存在。


对于 Object 存在的上述问题, 可以通过使用 Map 结构完美解决,并且还提供了更加方便的操作功能,首先让我们来看一下 Map 的基本用法:

js 复制代码
/** 创建 */
const map = new Map();

/** 设置 */
map.set( "a", 10 );

/** 获取 */
map.get( "a" )

/** 删除 */
map.delete( "a" );

/** 清空 */
map.clear();

/** 判断是否存在某个键 */
map.has( "a" )

/** 获取键值对数量 */
map.size

可以看到,Map 结构拥有极为便捷的 "增删改查" 功能,这些功能在 Object 中虽然也都能实现,但却没有这么优雅且如此具有语义化。接下来,我们将全面剖析相关用法。


Map 方法和属性

  • set()
  • get()
  • delete()
  • clear()
  • has()
  • size

创建

通过 new 关键字和 Map 构造函数就可以创建一个空的 Map

js 复制代码
const map = new Map();

如果希望在创建时就添加一些初始化数据,通常情况下,可以传入一个二维数组作为参数:

js 复制代码
const map = new Map( [
    [ "a", 10 ],
    [ "b", 20 ],
    [ "c", 30 ]
] );

实际上,不仅仅是二维数组,任何 "具有 Iterator 接口并且数据中的每一个成员都是一个包含两个元素的数组" 的数据都可以作为参数,例如,可以用一个 Map 初始化另一个 Map

js 复制代码
const map1 = new Map( [
    [ "a", 10 ],
    [ "b", 20 ],
    [ "c", 30 ]
] );
const map2 = new Map( map1 );

如果希望将一个 Object 数据作为初始化数据传到 Map 里,应该怎么做呢?

js 复制代码
const obj = {
    "a": 10,
    "b": 20,
    "c": 30
};

/**
 * 不要直接传参:new Map( obj ) 这将导致如下错误:
 * Uncaught TypeError: object is not iterable (cannot read property Symbol(Symbol.iterator))
 * 
 * 可以使用 Object.entries() 将其转换成符合要求的数组
 * 然后再传递给 Map 就可以了
 */
const map = new Map( Object.entries( obj ) );

设置

通过 set 方法可以给 Map 设置值,语法:set( key, value )

js 复制代码
const map = new Map();
map.set( "a", 10 );

Object 仅支持使用字符串和 Symbol 作为键名所不同的是,Map 支持将任意类型的数据作为键名,这一点 Object 可谓是望尘莫及。但与 Object 相同的是,键值是没有任何类型限制的,并且当出现相同的键名时,将采用后者。

js 复制代码
const map = new Map();

map.set( "a", 10 ); 
map.set( 1, 10 ); 
map.set( true, 10 );
map.set( [ 1, 2, 3 ], 10 );
map.set( { a: 1 }, 10 );
map.set( NaN, 10 );
map.set( null, 10 );
map.set( undefined, 10 );
map.set( document.querySelector( "body" ), 10 ); 

set 方法会返回当前的 Map 结构,因此可进行链式调用:

js 复制代码
const map = new Map();

map.set( "a", 10 ) 
   .set( "b", 20 )
   .set( "c", 30 );

需要注意的是,当使用对象作为键名时,两个看起来相同的对象,实际上代表的是两个不同的键:

js 复制代码
const map = new Map();

map.set( { a: 1 }, 10 );

/**
 * 可能你会觉得设置和获取的都是 { a: 1 } 
 * 两者的键名是 "相同的",应该输出的是 10
 * 但实际上输出的却是 undefined
 */
console.log( map.get( { a: 1 } ) );

上述情况是因为两个看起来相同的对象,实际上是两个完全不同的 "引用",两者的内存地址是不同的,而 Map 结构只会将对同一对象的引用视为同一个键,所以上面的代码要改成如下写法才能正常返回 10

js 复制代码
const obj = { a: 1 };
const map = new Map();

map.set( obj, 10 );
console.log( map.get( obj ) ); // 10 

获取

通过 get 方法可以获取 Map 的值,语法:get( key )

js 复制代码
const map = new Map();

map.set( "a", 10 );
console.log( map.get( "a" ) ); // 10

如果未找到指定的 key,则返回 undefined

删除

通过 delete 方法可以删除 Map 中指定的键,语法:delete( key )

js 复制代码
const map = new Map();

map.set( "a", 10 );
map.delete( "a" );

console.log( map.get( "a" ) === undefined ); // true

删除成功返回 true,失败返回 false

清空

通过 clear 方法可以清空 Map 中的所有键,语法:clear()

js 复制代码
const map = new Map();

map.set( "a", 10 );
map.set( "b", 20 );
map.set( "c", 30 );

map.clear();

console.log( map.get( "a" ) === undefined ); // true
console.log( map.get( "b" ) === undefined ); // true
console.log( map.get( "c" ) === undefined ); // true

clear 方法是没有返回值的。

判断存在某个键

通过 has 方法可以判断 Map 中是否存在指定的键,若存在则返回 true,否则返回 false,语法:has( key )

js 复制代码
const map = new Map();

map.set( "a", 10 );
map.set( "b", 20 );
map.set( "c", 30 );

console.log( map.has( "a" ) ); // true
console.log( map.has( "b" ) ); // true
console.log( map.has( "x" ) ); // false

获取键值对数量

通过 size 属性可以获取 Map 中的键值对数量:

js 复制代码
const map = new Map();

map.set( "a", 10 );
map.set( "b", 20 );
map.set( "c", 30 );

console.log( map.size ); // 3

Map 遍历

  • keys()
  • values()
  • entries()
  • forEach()

键名遍历器

通过 keys 方法可以返回一个遍历器( MapIterator ),其中包含了所有的键名,语法:keys()

js 复制代码
const map = new Map();

map.set( "a", 10 );
map.set( "b", 20 );
map.set( "c", 30 );

console.log( map.keys() );  // MapIterator {'a', 'b', 'c'}

可以使用 for...of 循环遍历:

js 复制代码
const map = new Map();

map.set( "a", 10 );
map.set( "b", 20 );
map.set( "c", 30 );

for ( const key of map.keys() ) {
    console.log( key );
}

/** 
 * 上述语句将打印出 
 *  a
 *  b
 *  c
 */

键值遍历器

通过 values 方法可以返回一个遍历器( MapIterator ),其中包含了所有的键值,语法:values()

js 复制代码
const map = new Map();

map.set( "a", 10 );
map.set( "b", 20 );
map.set( "c", 30 );

console.log( map.values() );  // MapIterator {10, 20, 30}

可以使用 for...of 循环遍历:

js 复制代码
const map = new Map();

map.set( "a", 10 );
map.set( "b", 20 );
map.set( "c", 30 );

for ( const value of map.values() ) {
    console.log( value );
}

/** 
 * 上述语句将打印出 
 *  10
 *  20
 *  30
 */

键值对遍历器

通过 entries 方法可以返回一个遍历器( MapIterator ),其中包含了所有的键值对,语法:entries()

js 复制代码
const map = new Map();

map.set( "a", 10 );
map.set( "b", 20 );
map.set( "c", 30 );

console.log( map.entries() );  // MapIterator {'a' => 10, 'b' => 20, 'c' => 30}

可以使用 for...of 循环遍历:

js 复制代码
const map = new Map();

map.set( "a", 10 );
map.set( "b", 20 );
map.set( "c", 30 );

for ( const entrie of map.entries() ) {
    console.log( entrie );
}

/** 
 * 上述语句将打印出 
 *  ['a', 10]
 *  ['b', 20]
 *  ['c', 30]
 */



for ( const [ key, value ] of map.entries() ) {
    console.log( key, value );
}

/** 
 * 上述语句将打印出 
 *  a 10
 *  b 20
 *  c 30
 */

遍历方法

通过 forEach 方法可以直接遍历一个 Map,用法与数组中的 forEach 类似 :

js 复制代码
const map = new Map();

map.set( "a", 10 );
map.set( "b", 20 );
map.set( "c", 30 );

map.forEach( ( value, key, map ) => {
    console.log( value, key, map );
} )

这里需要说明的是,Map 的遍历顺序将严格遵循插入数据时的顺序,这是 Object 结构所不具备的特点。


MapObject 对比

两者各有优劣,但综合来看,Map 相对来说还是更强大一些,不过这并非一定要在实际开发中完全使用 Map 来替代 Object,根据不同的场景和两者的长处,选择最优的方案才是稳妥之法,下面的表格整理了它们之间的大多数区别,可作为参考:

Map Object
键名数据类型 任意数据类型 只能是字符串或 Symbol
遍历顺序 严格遵循插入数据时的顺序 无法保证与插入数据时的顺序一致
遍历检索 所有键都能遍历出来 Symbol 类型的键无法被常规方法遍历出来,需要用专门的方法
原型问题 存在 "原型对象继承属性" 问题
JSON 的支持 原生不支持,需手动写转换函数 原生支持互转
创建方式 只能通过 new Map() 字面量、new Object()Object.create()
获取键值对数量 本身具有 size 属性,可直接获取 需要遍历计算后获得数量,或者通过 Object.keys().length 间接获取
可迭代性
遍历方式 for...offorEach for...in
插入性能 数据量较小时,性能与 Object 差不多,但数据量比较大时,性能高于 Object 数据量较小时,性能与 Map 差不多,但数据量比较大时,性能低于 Map
查询性能 通常情况下与 Object 性能基本一样,但是当查找不存在的键或者键名并非是连续整数的形式的时候,性能要低于 Object 通常情况下与 Map 性能基本一样,但是当键名是连续整数的形式的时候,性能要高于 Object,因为浏览器引擎会对这种情况进行优化处理
删除性能 本身提供了相应的删除方法,性能高于 Object 通常使用 delete 运算符删除键,存在性能问题,因此性能低于 Map
相关推荐
小马哥编程1 小时前
Function.prototype和Object.prototype 的区别
javascript
王小王和他的小伙伴2 小时前
解决 vue3 中 echarts图表在el-dialog中显示问题
javascript·vue.js·echarts
学前端的小朱2 小时前
处理字体图标、js、html及其他资源
开发语言·javascript·webpack·html·打包工具
outstanding木槿2 小时前
react+antd的Table组件编辑单元格
前端·javascript·react.js·前端框架
好名字08212 小时前
前端取Content-Disposition中的filename字段与解码(vue)
前端·javascript·vue.js·前端框架
摇光933 小时前
js高阶-async与事件循环
开发语言·javascript·事件循环·宏任务·微任务
胡西风_foxww3 小时前
【ES6复习笔记】Class类(15)
javascript·笔记·es6·继承··class·静态成员
布兰妮甜3 小时前
使用 WebRTC 进行实时通信
javascript·webrtc·实时通信
艾斯特_3 小时前
JavaScript甘特图 dhtmlx-gantt
前端·javascript·甘特图
飞翔的渴望3 小时前
react18与react17有哪些区别
前端·javascript·react.js