概述
Map
是 ECMAScript 6 中新增的一种数据结构,它完美高效的实现了 JavaScript
中使用 "键值对" 来存储数据的需求。在它出现之前,基本都是通过 Object
来创建 "键值对" 集合,但这种方式本身存在一些缺陷,使其在一些特殊情况下经常遇到比较棘手的问题,而 Map
的出现则终结了这一局面。为了充分体现出 Map
独特的优势,我们先来看一下使用 Object
作为数据结构时存在的四个问题。
Object
存在的问题
一、类型问题
Object
的键名只能是:字符串或 Symbol
类型,如果将其它类型的数据设置为键名,将会自动调用 toString
方法将其转换为字符串,并且无任何提示。
二、遍历问题
当使用 Symbol
类型作为键名时,会存在一个问题,由于 Symbol
不可枚举的特性,通过常规的遍历方法如:for...in
或 Object.keys()
都无法遍历出这个键的,这会导致遍历操作出现问题。除非使用其它方式如:Object.getOwnPropertyDescriptors
或 Reflect.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
,其中包含 a
和 b
两个属性,我们可以轻松的获取对应的值:
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
上设置 constructor
和 valueOf
属性,获取它们的值理应得到 undefined
,可实际却是上面的结果,这就是 "原型对象继承属性" 问题。根本原因在于,每个对象都有原型,而原型上存在一些固定属性,这些属性是可以正常访问的,因此就会出现上面所遇到的问题。尽管这种问题在实际开发中并不常见,但它确实是一个隐患。那么除了 constructor
和 valueOf
以外,还有哪些属性呢?我们在控制台将 obj
打印出来看看:
js
console.log( obj );
结果如上图所示,除了前面提到的两个属性外,还有 hasOwnProperty
、isPrototypeOf
等多个属性,这些都是可以被读取的,尽管没有显式的声明它们,但它们却依旧真实存在。
对于 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
结构所不具备的特点。
Map
和 Object
对比
两者各有优劣,但综合来看,Map
相对来说还是更强大一些,不过这并非一定要在实际开发中完全使用 Map
来替代 Object
,根据不同的场景和两者的长处,选择最优的方案才是稳妥之法,下面的表格整理了它们之间的大多数区别,可作为参考:
Map | Object | |
---|---|---|
键名数据类型 | 任意数据类型 | 只能是字符串或 Symbol |
遍历顺序 | 严格遵循插入数据时的顺序 | 无法保证与插入数据时的顺序一致 |
遍历检索 | 所有键都能遍历出来 | Symbol 类型的键无法被常规方法遍历出来,需要用专门的方法 |
原型问题 | 无 | 存在 "原型对象继承属性" 问题 |
对 JSON 的支持 |
原生不支持,需手动写转换函数 | 原生支持互转 |
创建方式 | 只能通过 new Map() |
字面量、new Object() 、Object.create() |
获取键值对数量 | 本身具有 size 属性,可直接获取 |
需要遍历计算后获得数量,或者通过 Object.keys().length 间接获取 |
可迭代性 | 有 | 无 |
遍历方式 | for...of 、forEach |
for...in |
插入性能 | 数据量较小时,性能与 Object 差不多,但数据量比较大时,性能高于 Object |
数据量较小时,性能与 Map 差不多,但数据量比较大时,性能低于 Map |
查询性能 | 通常情况下与 Object 性能基本一样,但是当查找不存在的键或者键名并非是连续整数的形式的时候,性能要低于 Object |
通常情况下与 Map 性能基本一样,但是当键名是连续整数的形式的时候,性能要高于 Object ,因为浏览器引擎会对这种情况进行优化处理 |
删除性能 | 本身提供了相应的删除方法,性能高于 Object |
通常使用 delete 运算符删除键,存在性能问题,因此性能低于 Map |