- Map是一种新的集合类型,为这门语言带来了真正的键值存储机制。
- Map的大多数特性都可以通过Object类型实现。
基本API
const m = new Map();
// 使用嵌套数组初始化映射
const m1 = new Map([
["key1", "val1"],
["key2", "val2"],
["ket3", "val3"]
]);
alert(m1.size); // 3
// 使用自定义迭代器初始化映射
const m2 = new Map({
[Symbol.iterator]: function*() {
yield ["key1", "val1"];
yield ["key2", "val2"];
}
});
alert(m2.size); // 2
// 映射期待的键值对,无论是否提供
const m3 = new Map([]);
alert(m3.has(undefined)) // true
// 注意:浏览器环境和Node环境下,m3.has(undefined)返回的结果不同;浏览器环境默认Map(1) {undefined => undefined}
// 而Node环境,则m3返回Map(0){},所以m3.has(undefined)返回false。
-
set():添加键值对
-
get():查询
-
has():查询
-
size:获取映射中的键值对
-
delete():删除指定键值对
-
clear():清除该映射实例中所有键值对
-
set()方法返回映射实例,因此可以把多个操作连缀起来,包括初始化声明
const m = new Map().set('key1', 'val1').set('key2', 'val2')
-
Object只能使用数值、字符串或Symbol作为键,Map可以使用任何js数据类型作为键。
-
Map内部使用SameValueZero比较操作,基本上相当于使用严格对象相等的标准来检查键的匹配性。
-
与严格相等一样,在映射中用作键和值的对象及其他集合类型,在自己的内容或属性被修改时任然保持不变。
const m = new Map();
const objKey = {};
const objVal = {};
const arrKey = [];
const arrVal = [];
m.set(objKey, objVal);
m.set(arrKey, arrVal);
objKey.foo = "foo";
objVal.bar = "bar";
arrKey.push("foo");
arrVal.push("bar");
m.get(objKey); // {bar: "bar"}
m.get(arrKey); // ["bar"]
// SameValueZero比较也可能导致意想不到的冲突:
const m = new Map();
const a = 0/"", // NaN
b = 0/"", // NaN
pz = +0,
nz = -0;
alert(a === b); // false
alert(pz === nz); // true
m.set(a, 'foo').set(pz, 'bar');
alert(m.get(b)); // foo
alert(m.get(nz)); // bar
顺序与迭代
- 与Object类型的一个主要差异是,Map实例会维护键值对的插入顺序,因此可以根据插入顺序执行迭代操作。
- 映射实例可以提供一个迭代器Iterator,能以插入顺序生成[key, value]形式的数组。
- 可以通过entries()方法(或者Symbol.iterator属性,它引用entries())取得这个迭代器:
const m = new Map([
["key1", "val1"],
["key2", "val2"],
]);
alert(m.entries === m[Symbol.iterator]); // true
for(let pair of m.entries()) {
alert(pair);
}
// [key1, val1]
// [key2, val2]
- 因为entries()是默认迭代器,所以可以直接对映射实例使用扩展操作,把映射转换为数组:
console.log([...m]); // [[key1, val1],[key2, val2]]
- 若不使用迭代器,而是使用回调方式,则可以调用映射的forEach(callback, opt_thisArg)方法并传入回调,依次迭代每个键值对。传入的回调接收可选的第二个参数,这个参数用于重写回调内部this的值。
const m = new Map([
["key1", "val1"],
["key2", "val2"],
["key3", "val3"]
]);
m.forEach((val, key) => alert(`${key} -> ${val}`));
// key1 -> val1
// key2 -> val2
// key3 -> val3
for (let key of m.keys())
{ alert(key); }
// key1
// key2
// key3
for (let key of m.values())
{ alert(key); }
// value1
// value2
// value3
-
keys()和values()分别返回以插入顺序生成键和值的迭代器。
-
键和值在迭代器遍历时是可以修改的,但映射内部的引用则无法修改。当然这不妨碍修改作为键或值得对象内部得属性,因为这样并不影响它们在映射中得身份:
const m1 = new Map([
["key1", "val1"]
]);
// 作为键得字符串原始值是不能修改的
for (let key of m1.keys()) {
key = "newKey";
alert(key); // newKey
alert(m1.get("key1")); // val1
}
const keyObj = {id: 1};
const m = new Map([
[keyObj], "val1"
]);
// 修改了作为键的对象的属性,但对象在映射内部仍然引用相同的值
for (let key of m.keys()) {
key.id = "newKeys";
alert(key); // {id: "newKey"}
alert(m.get(keyObj)); // val1
}
alert(keyObj); // { id: "newKey"}
选择Objec还是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。