Map
Map 对象保存键值对。任何值(对象或者原始值)都可以作为一个键或一个值。
它和 Object 对象不同,对象只能用字符串和 Symbol 作为键,而 Map 可以使用任何值。
语法
new Map([iterable]);
参数 | 说明 |
---|---|
iterable |
Iterable 可以是一个数组或者其他 Iterable 对象,其元素或为键值对,或为两个元素的数组。 每个键值对都会添加到新的 Map。null 会被当做 undefined 。 |
描述
键的比较是基于 SameValueZero 算法:NaN
是与 NaN
相同的(虽然 NaN !== NaN
),剩下所有其它的值是根据 ===
运算符的结果判断是否相等。
对象与字典的对比
Object 和 Map 类似的一点是,它们都允许你按键存取一个值,都可以删除键,还可以检测一个键是否绑定了值。
除了键类型上的不同,Map 和 Object 还有以下不同:
- Map 中的键是有序的,而添加到对象中的键则不同
- Map 可以通过
size
获取键值个数,Object 的键值对个数只能手动计算 - Map 可直接进行迭代,而 Object 的迭代需要先获取它的键数组,然后进行迭代
- Object 都有自己的原型,原型链上的键名有可能和你自己在对象上的设置键名产生冲突。虽然 ES5 开始可以用
map = Object.create(null)
来创建一个没有原型的对象,但是这种用法不太常见 - Map 在涉及频繁增删键值对的场景下会有些性能优势
但是这并不意味着你可以随意使用 Map,对象仍旧是最常用的。Map 实例只适合用于集合(Collections),你应当考虑修改你原来的代码——先前使用对象来处理集合的地方。对象应该用其字段和方法来作为记录的。 如果你不确定要使用哪个,请思考下面的问题:
- 在运行之前 key 是否是未知的,是否需要动态地查询 key 呢?
- 是否所有的值都是统一类型,这些值可以互换么?
- 是否需要不是字符串类型的 key ?
- 键值对经常增加或者删除么?
- 是否有任意个且非常容易改变的键值对?
- 这个集合可以遍历么?
假如以上全是"是"的话,那么你需要用 Map 来保存这个集。相反,你有固定数目的键值对,独立操作它们,区分它们的用法,那么你需要的是对象。
原型属性
属性 | 描述 |
---|---|
Map.prototype.constructor |
返回一个函数,它创建了实例的原型。默认是 Map 函数。 |
Map.prototype.size |
返回 Map 对象的键/值对的数量。 |
size
Map.prototype.size
属性返回 Map
结构的成员总数。
const map = new Map();
map.set('foo', true);
map.set('bear', false);
console.log(map.size);
// 2
原型方法
方法 | 描述 |
---|---|
Map.prototype.set(key, value) |
用于设置 Map 对象中键的值,返回该 Map 对象 |
Map.prototype.get(key) |
用于获取一个 Map 对象中指定 key 的元素 |
Map.prototype.has(key) |
用于校验 Map 中是否存在指定 key 值的元素 |
Map.prototype.delete(key) |
用于移除任何与键相关联的值,并且返回该值 |
Map.prototype.clear() |
用于移除 Map 对象中的所有元素 |
Map.prototype.forEach(callbackFn [,thisArg]) |
按插入顺序,为 Map 对象里的每一键值对调用一次回调函数。如果为 forEach 提供了 thisArg ,它将在每次调用回调中作为 this 值 |
Map.prototype.keys() |
返回一个新的 Iterable 对象。它包含按照顺序插入 Map 对象中每个元素的 key 值。 |
Map.prototype.values() |
返回一个新的 Iterator 对象,它按插入顺序包含了 Map 对象中每个元素的值。 |
Map.prototype.entries() |
返回一个新的包含 [key, value] 对的 Iterable 对象,返回迭代器的迭代顺序与 Map 对象的插入顺序相同。 |
Map.prototype[@@iterator]() |
返回一个新的 MapIterator 对象,它按插入顺序包含了 Map 对象中每个元素的 [key, value] 数组。 |
set(key, value)
set
方法设置 key
对应的键值,然后返回整个 Map
结构。如果 Map
结构。如果 key
已经有值,则键值会被更新,否则就新生成该键。
set
方法返回的是当前的 Map 对象,因此可以采用链式写法。
const map = new Map();
// 键是字符串
map.set('edition', 6);
// 键是数值
map.set(262, 'standard');
// 键是 undefined
map.set(undefined, 'nah');
const m = new Map().set(1, 'a').set(2, 'b').set(3, 'c');
get(key)
get
方法读取 key
对应的键值,如果找不到 key
,则返回 undefined
。
const m = new Map();
const hello = function () {
console.log('hello');
};
m.set(hello, 'Hello ES6!');
// 键是函数
m.get(hello);
// Hello ES6!
has(key)
has
方法返回一个布尔值,表示某个键是否在 Map 数据结构中。
const m = new Map();
m.set('edition', 6);
m.set(262, 'standard');
m.set(undefined, 'nah');
m.has('edition');
// true
m.has('years');
// false
m.has(262);
// true
m.has(undefined);
// true
delete(key)
delete
方法删除某个键,返回 true
。如果删除失败,则返回 false
。
const m = new Map();
m.set(undefined, 'nah');
m.has(undefined);
// true
m.delete(undefined);
m.has(undefined);
// false
clear()
clear
方法清除所有成员,没有返回值。
const map = new Map();
map.set('foo', true);
map.set('bar', false);
map.size;
// 2
map.clear();
map.size;
// 0
forEach
入参首位为 value
值,其次为 key
键值。
const map = new Map();
map.set('1', { a: 1 });
map.set('2', { b: 2 });
map.set('3', { c: 3 });
map.forEach((value, key) => {
console.log(key, value);
// 1 { a: 1 }
// 2 { b: 2 }
// 3 { c: 3 }
});
keys
const map = new Map();
map.set('1', { a: 1 });
map.set('2', { b: 2 });
map.set('3', { c: 3 });
const keys = map.keys();
for (const key of keys) {
console.log(key);
// 通过 map.get(key) 可得 value 值
// 1
// 2
// 3
}
values
const map = new Map();
map.set('1', { a: 1 });
map.set('2', { b: 2 });
map.set('3', { c: 3 });
const values = map.values();
for (const value of values) {
console.log(value);
// { a: 1 }
// { b: 2 }
// { c: 3 }
}
entries
const map = new Map();
map.set('1', { a: 1 });
map.set('2', { b: 2 });
map.set('3', { c: 3 });
const entries = map.entries();
for ([key, value] of entries) {
console.log(key, value);
// 1 { a: 1 }
// 2 { b: 2 }
// 3 { c: 3 }
}
MapIterator.next
const map = new Map();
map.set('1', { a: 1 });
map.set('2', { b: 2 });
map.set('3', { c: 3 });
const keys = map.keys();
for (i = 0; i < map.size; i++) {
key = keys.next().value;
console.log(key);
// 1
// 2
// 3
}
const values = map.values();
for (i = 0; i < map.size; i++) {
value = values.next().value;
console.log(value);
// { a: 1 }
// { b: 2 }
// { c: 3 }
}
const entries = map.entries();
for (i = 0; i < map.size; i++) {
entry = entries.next().value;
console.log(entry[0], entry[1]);
// 1 { a: 1 }
// 2 { b: 2 }
// 3 { c: 3 }
}
MapIterator
对象每次遍历下个元素都会调用 next()
,一次遍历完成之后,位置并不会复原。所以多次遍历必须用对应的 map
方法(keys()
、values()
和 entries()
)重新获取 Map Iterator
对象。若不重新获取,则迭代器无返回值。
特性
内存地址绑定
Map 键与内存地址绑定。
⚠️ 注意:只有对同一个对象的引用, Map 结构才将其视为同一个键。
const map = new Map();
map.set(['a'], 555);
map.get(['a']);
// undefined
上面的 set
方法和 get
方法表面上是针对同一个键,实际上却是两个值,这是因为数组引用的内存地址不一样导致的。因此 get
方法无法读取该值,返回 undefined
。
同样的值的两个实例在 Map 结构中被视为两个键。
const map = new Map();
const a = ['foo'];
const b = ['foo'];
map.set(a, 123).set(b, 456);
map.get(a);
// 123
map.get(b);
// 456
Map 的键实际上是和内存地址绑定的,只要内存地址不一样,就视为两个键。这就解决了 同名属性碰撞(Clash) 的问题,我们扩展别人的库时,如果使用对象作为键名,不用担心自己的属性与原作者的属性同名。
以基本类型值作为键名
如果 Map 的键是一个基本类型的值(数字、字符串、布尔值),则只要两个值严格相等,Map 就将其视为一个键,包括 0
和 -0
。
另外,需要注意得失,虽然 NaN
不严格等于自身,但 Map 将其视为同一个键。
const map = new Map();
map.set(-0, 123);
map.get(-0);
// 123
map.set(true, 1);
map.set('true', 2);
map.get(true);
// 1
map.set(undefined, 3);
map.set(null, 4);
map.get(undefined);
// 3
map.set(NaN, 123);
map.get(NaN);
// 123
以 Set 或 Map 作为键名
以 Set 作为参数
const payload = new Set([
['foo', 1],
['bar', 2],
]);
const map = new Map(payload);
map.get('foo');
// 1
以 Map 作为参数
const payload = new Map([['baz'], 3]);
const map = new Map(payload);
map.get('baz');
// 3
如果对同一个键多次赋值,后面的值会将覆盖前面的值。
const map = new Map();
map.set(1, 'foo').set(1, 'baz');
map.get(1);
// 'baz'
以 NaN 作键名
NaN
也可以作为 Map 对象的键,虽然 NaN
和任何值甚至和自己都不相等(NaN !== NaN
返回 true
),但下面的例子表明,两个 NaN
作为 Map 的键来说是没有区别的。
const map = new Map();
map.set(NaN, 'Not a number');
map.get(NaN);
// 'Not a number'
const otherNaN = Number('foo');
map.get(otherNaN);
// 'Not a number'
实践应用
实例对象合并
合并两个 Map 实例对象时,如果有重复的键值,则后面的会覆盖前面的。
扩展运算符本质上是将 Map 对象转换成数组。
const first = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'],
]);
const second = new Map([
[1, 'uno'],
[2, 'dos'],
]);
const merged = new Map([...first, ...second]);
Map 转为数组
Map 转为数组最方便的方法就是使用扩展运算符。
const map = new Map().set(true, 1).set({ foo: 2 }, ['abc']);
console.log([...map]);
// [ [true, 1], [ { foo: 2}, ['abc'] ] ]
数组转为 Map
将数组传入 Map 构造函数就可以转为 Map。
const m = new Map([
[true, 7],
[{ foo: 3 }, ['abc']],
]);
console.log(m);
// Map {
// true => 7,
// Object {foo: 3} => ['abc']
// }
Map 转为对象
如果 Map 的所有键都是字符串,则可以转为对象。
function toObject(strMap) {
let o = Object.create(null);
for (let [k, v] of strMap) {
o[k] = v;
}
return o;
}
const m = new Map().set('yes', true).set('no', false);
console.log(toObject(m));
// {'yes': true, 'no': false}
对象转为 Map
function toMap(obj) {
let strMap = new Map();
for (let k of Object.keys(obj)) {
strMap.set(k, obj[k]);
}
return strMap;
}
const m = toMap({ yes: true, no: false });
console.log(m);
// Map {"yes" => true, "no" => false}
Map 转为 JSON
Map 转为 JSON 要区分两种情况。一种情况是, Map 的键名都是字符串,这时可以选择转为对象 JSON。
const toJSON = (strMap) => JSON.stringify(toObject(strMap));
let m = new Map().set('yes', true).set('no', false);
console.log(toJSON(m));
// '{"yes": true, "no": false}'
另一种情况是,Map 的键名有非字符串,这时可以选择转为数组 JSON。
const toArrayJSON = (map) => JSON.stringify([...map]);
let m = new Map().set(true, 1).set({ foo: 2 }, ['abc']);
console.log(toArrayJSON(m));
// '[[true], 1], [{'foo': 2}, ['abc']]'
JSON 转为 Map
const toMap = (jsonStr) => toMap(JSON.parse(jsonStr));
console.log(toMap('{"yes": true, "no": false}'));
// Map {'yes' => true, 'no': false}
但是,有一种特殊情况:整个 JSON 就是一个数组,且每个数组成员本身又是一个具有两个成员的数组。这时,它可以一一对应地转为 Map 。这往往是数组转为 JSON 的逆操作。
const toMap = (jsonStr) => new Map(JSON.parse(jsonStr));
console.log(toMap('[[true, 7], [{"foo": 3}, ["abc"]]]'));
// Map(true => 7, Object {foo: 3} => ['abc'])
替代 if-else
const timemap = new Map([
[0, '星期天'],
[1, '星期一'],
[2, '星期二'],
[3, '星期三'],
[4, '星期四'],
[5, '星期五'],
[6, '星期六'],
]);
// 中间使用 React Hooks 的 useEffect 实现
const [time, setTime] = setState(new Date());
useEffect(() => {
clearInterval();
setInterval(() => {
setTimeout(new Date());
}, 1000);
});
const res = (timemap.get(time.getDay()) || '') + time.toLacleTimeString();