一、Map 简介
在 JS 中 Map 对象是 键值对 的一个 集合:
Map中任何值都可以作为一个键或一个值Map中每个元素是有顺序的, 它能够记住每个元素首次插入顺序- 同
Object在Map中的一个键只能出现一次, 它在Map的集合中是独一无二的 - 同时
Map内部实现了Symbol.iterator接口, 其本身又是一个可迭代对象, 可被for...of等语法进行迭代, 关于更多迭代器的相关知识可查阅 《迭代器、生成器详解🔥🔥🔥》
二、基本操作
2.1 创建
创建一个 Map 实例, 可直接通过 Map 构造函数进行创建
new Map()
如果我们需要在创建 Map 实例时, 就给定初始值, 可传一个二维数组, 如下代码所示: name 和 age 是 key 值, MoYuanJun 和 18 是对应的 value
const map = new Map([
['name', 'MoYuanJun'],
['age', 18],
])
2.2 增、改、删、查
- 可通过
Map.prototype.set(key, value)方法为Map对象,新增或修改对应键值, 该方法返回当前Map对象
const map = new Map([
['name', 'MoYuanJun'],
['age', 18],
])
map.set('name', 'myj') // name key 值已存在, 则修改
map.set('address', 'hz') // address key 值不存在, 则新增
console.log(map) // Map(3) { 'name' => 'myj', 'age' => 18, 'address' => 'hz' }
由于 Map.prototype.set(key, value) 返回的是当前 Map 对象, 所以实际上它是支持链式调用的, 上面代码可以修改为:
const map = new Map([
['name', 'MoYuanJun'],
['age', 18],
])
map.set('name', 'myj').set('address', 'hz')
console.log(map)
- 可通过
Map.prototype.delete(key)删除Map中指定元素, 该方法返回一个布尔值, 表示是否删除成功
const map = new Map([
['name', 'MoYuanJun'],
['age', 18],
])
const res = map.delete('name')
console.log(res, map) // true { 'age' => 1
- 可通过
Map.prototype.clear()清除Map中的所有元素, 该方法没有返回值
const map = new Map([
['name', 'MoYuanJun'],
['age', 18],
])
const res = map.clear()
console.log(res, map) // undefined Map(0) {}
- 可通过
Map.prototype.get(key)获取指定元素, 需要注意的是: 如果该元素是个对象, 那么获取到的将是对象的引用地址, 也就是说对获取到的对象, 所做的任何更改都会同步修改到Map中对应的值
const map = new Map([
['name', 'MoYuanJun'],
['age', 18],
['user', { name: 'lh', address: 'hz' }]
])
const name = map.get('name')
const user = map.get('user')
user.name = 'myj'
console.log(name, map)
// 打印内容如下:
// MoYuanJun Map(3) {
// 'name' => 'MoYuanJun',
// 'age' => 18,
// 'user' => { name: 'myj', address: 'hz' }
// }
- 可通过
Map.prototype.has(key)判断Map对象中指定元素是否存在, 该方法返回一个布尔值, 表示对应的元素是否存在
const map = new Map([
['name', 'MoYuanJun'],
['age', 18],
])
map.has('name') // true
map.has('address') // false
- 可通过
Map.prototype.size属性获取当前Map对象中元素个数
const map = new Map([
['name', 'MoYuanJun'],
['age', 18],
])
map.size // 2
三、循环
3.1 作为「迭代对象」
由于 Map 定义了 Map.prototype[@@iterator]() 属性, 所以 Map 实例对象是一个 可迭代 对象, 我们可以使用所有可以操作可迭代对象的语法, 包括 for...of、展开语法、解构语法、Array.from 等等, 并且每次迭代 Map 对象拿到的值是一个 [key, value] 格式的数组
const map = new Map([
['name', 'MoYuanJun'],
['age', 18],
])
// 1. 使用 for...of 进行迭代
for (let item of map) {
console.log(item); // [ 'name', 'MoYuanJun' ] [ 'age', 18 ]
}
// 2. 使用展开语法
[...map] // [ [ 'name', 'MoYuanJun' ], [ 'age', 18 ] ]
// 3. 使用结构语法
const [first, second] = map // first: [ 'name', 'MoYuanJun' ], second: [ 'age', 18 ]
// 4. 使用 Array.form
Array.form(map) // [ [ 'name', 'MoYuanJun' ], [ 'age', 18 ] ]
3.2 Map.prototype.forEach(callbackFn, thisArg)
可通过 Map.prototype.forEach(callbackFn, thisArg) 方法进行循环, 同 Array.prototype.forEach() 该方法支持传 两个参数:
-
参数
callbackFn必填, 每次循环将执行该函数, 函数接收3个参数:value(当前迭代的值)、key(当前迭代的key)、map(正在迭代的map对象) -
参数
thisArg选填, 将会被作为callbackFn函数的this指向
const map = new Map([
['name', 'MoYuanJun'],
['age', 18],
])
map.forEach((value, key, currentMap) => {
console.log(value, key, currentMap)
// 打印内容:
// MoYuanJun name Map(2) { 'name' => 'MoYuanJun', 'age' => 18 }
// 18 age Map(2) { 'name' => 'MoYuanJun', 'age' => 18 }
})
3.3 获取「keys」「values」「entries」迭代器
Map 对象中我们可通过原型方法 Map.prototype.keys()、Map.prototype.values()、Map.prototype.entries() 分别获取到 Map 对象中 key、value 以及 [key, value] 的一个集合, 需要注意的是这个几个方法返回的是一个迭代器
const map = new Map([
['name', 'MoYuanJun'],
['age', 18],
])
const keys = map.keys() // 返回包含所有 key 的一个迭代器
keys.next() // { value: 'name', done: false }
keys.next() // { value: 'age', done: false }
keys.next() // { value: undefined, done: true }
const values = map.values() // 返回包含所有 value 的一个迭代器
values.next() // { value: 'MoYuanJun', done: false }
values.next() // { value: 18, done: false }
values.next() // { value: undefined, done: true }
const entries = map.entries() // 返回包含所有 [key, value] 的一个迭代器
entries.next() // { value: [ 'name', 'MoYuanJun' ], done: false }
entries.next() // { value: [ 'age', 18 ], done: false }
entries.next() // { value: undefined, done: true }
实际上这几个方法返回的 迭代器 本身又是一个 可迭代对象, 也就是它能够直接被 for...of 进行循环迭代
const map = new Map([
['name', 'MoYuanJun'],
['age', 18],
])
const keys = map.keys() // 返回包含所有 key 的一个可迭代迭代器
for (let key of keys) {
console.log('key: ', key) // key: name、key: age
}
const values = map.values() // 返回包含所有 value 的一个可迭代迭代器
for (let value of values) {
console.log('value: ', value) // value: MoYuanJun、value: 18
}
const entries = map.entries() // 返回包含所有 [key, value] 的一个可迭代迭代器
for (let entry of entries) {
console.log('entry: ', entry) // entry: [ 'name', 'MoYuanJun' ]、entry: [ 'age', 18 ]
}
关于更多迭代器的相关知识可查阅 《迭代器、生成器详解🔥🔥🔥》
3.4 循环的顺序
Map 对象是能够记住每次插入元素的顺序的, 后面修改值不会改变元素的顺序, 所以当我们循环 Map 对象时, 每个元素输出的顺序是和它首次被插入的顺序保持一致的, 当然 Map.prototype.keys()、Map.prototype.values()、Map.prototype.entries() 等方法拿到所有值的顺序也是和插入顺序保持一致
const map = new Map([
['name', 'MoYuanJun'],
['age', 18],
])
map.set('address', 'hz')
map.set('name', 'lh')
const keys = [...map.keys()] // [ 'name', 'age', 'address' ]
for (const item of map) {
console.log(item) // [ 'name', 'lh' ]、[ 'age', 18 ]、[ 'address', 'hz' ]
}
四、复制与合并
- 上文提到
Map对象本身就是一个可迭代对象, 同时Map构造函数是允许传入一个可迭代对象作为初始值的, 所以其实我们是可以把一个Map对象传给Map构造函数, 创建一个新的Map对象, 这个过程我们可以视为Map对象的一个拷贝
const map = new Map([
['name', 'MoYuanJun'],
['age', 18],
])
const clone = new Map(map);
console.log(clone.get('name')); // MoYuanJun
console.log(map === clone); // false
需要特别注意的是, 👆🏻 提到的 拷贝 实际上可以理解为是 浅拷贝, 如下代码, 如果元素 A 值是一个对象, 那么通过上述方式进行拷贝, 拷贝的是 A 的引用地址, 也就是拷贝前后 A 都是指向同一个对象, 如下代码:
const map = new Map([
['name', 'MoYuanJun'],
['age', 18],
['user', { name: 'lh', age: 20 }]
])
const clone = new Map(map);
map.get('user').name = 'myj' // 修改原本 Map 对象
console.log(clone.get('user')) // { name: 'myj', age: 20 }
Map对象间可以进行合并, 但是会保持键的唯一性, 否则前面的会被后面的覆盖, 如下代码所示: 尝试将map1和map2进行了合并, 同时map1的age被map2覆盖
const map1 = new Map([
['name', 'MoYuanJun'],
['age', 18],
])
const map2 = new Map([
['address', 'hz'],
['age', 20],
])
// map 对象本质是可迭代对象, 在这里通过展开语法将其转为数组
const map = new Map([...map1, ...map2])
console.log(map) // Map(3) { 'name' => 'MoYuanJun', 'age' => 20, 'address' => 'hz' }
五、与 「Object」 的比较
在 JS 中 Object 和 Map 类型, 它们都允许按 键 对数据进行存储、删除、修改…… 不过 Map 和 Object 还是存在一些重要的区别:
| Map | Object | |
|---|---|---|
| 意外的键 | 默认不包含任何键、只包含显式插入的键 | 存在原型, 原型链上的键名可能和对象上的设置的键名产生冲突 |
| 键的类型 | 可以是任意值 | String 或 Symbol |
| 键的顺序 | 等于首次插入的顺序 | 键目前是有序的, 但这个顺序相对来说是复杂的、不可控的 |
| Size | 通过 size 属性直接获取 | 通过计算获取 |
| 迭代 | 是个可迭代对象、可以直接被迭代 | 没有实现可迭代协议、不可以直接被迭代 |
| 性能 | 频繁增删键值对的场景下表现得更好 | 在频繁添加和删除键值对的场景下未作出优化 |
| 序列化和解析 | 没有元素的序列化和解析的支持 | 支持使用 JSON.stringify() 进行序列化、支持使用 JSON.parse() 解析序列化 |
六、应用场景
6.1 缓存函数
如下代码, memoizeMap 可生成一个带有缓存功能的函数, 缓存函数会将每次请求参数和请求结果进行缓存, 当再次请求相同的参数时, 不会进行任何运算, 则是直接返回缓存中的结果
const memoizeMap = (fun) => {
const map = new Map();
return (arg) => {
// 1. 如果存在缓存, 则取缓存数据
if (map.has(arg)) {
return map.get(arg);
}
// 2. 没有缓存, 请求函数
const res = fn(arg);
// 3. 缓存结果
map.set(arg, res)
return res;
};
}
const testFn = (foo) => foo + 999;
const memoizeMapFn = memoizeMap(testFn);
memoizeMapFn(1) // map对arg 1生成缓存
memoizeMapFn(2) // map对arg 2生成缓存
memoizeMapFn(1) // 不进行任何运行、直接取缓存结果
memoizeMapFn(2) // 不进行任何运行、直接取缓存结果
6.2 LRU 缓存
LRU 缓存: 即采用 最近最少 使用的缓存策略, 它的原则是, 如果一个数据最近没有被访问到, 那么它将来被访问的几率也很小, 那么在有限的内存空间下我们就可以把长时间没有访问到的数据去除掉
如下代码, 利用 Map 的 key 具有顺序的特性实现 LRU 缓存机制, 每次新增、修改、读取值时会将对应元素的缓存放到最后, 当缓存数量超出时会将第一个元素剔除
class LRUCache {
// capacity 缓存个数
constructor(capacity) {
this.capacity = capacity
this.map = new Map();
}
// 获取值: 每次获取值将对应元素, 放在最后面
get(key){
if (this.map.has(key)) {
const value = this.map.get(key);
this.map.delete(key);
this.map.set(key, value);
return value;
}
return -1;
}
// 新增值: 新增元素、如果缓存超出则删除第一个缓存元素
put(key, value){
if (this.map.has(key)) {
this.map.delete(key)
}
this.map.set(key, value)
if (this.map.size > this.capacity) {
const firstKey = map.keys().next().value
this.map.delete(firstKey);
}
}
}
6.3 去重、计算、分组
借助于 Map key 可以是任意类型、并且键值唯一, 可用于对数据的去重、计数、分组……
const user = {
age: 20,
name: 'lh',
address: 'hz',
}
const data = [
1,
2,
user,
2,
'lh',
'hz',
user,
'lh',
]
// 去重
const newData = [...new Map(data.map(v => [v, true])).keys()]
// 计数
const countMap = new Map()
data.forEach(v => {
const count = countMap.has(v) ? countMap.get(v) + 1 : 1
countMap.set(v, count)
})
// 分组
const groupMap = new Map()
data.forEach(v => {
groupMap.set(v, [...(groupMap.get(v) || []), v])
})
七、扩展知识
7.1 Map 对象序列化
上文提到 Map 对象是无法被 JSON.stringify() 序列化、也无法被 JSON.parse() 反序列化的, 但实际上我们可以通过:
- 配置
JSON.stringify()的replacer参数, 实现Map对象的自定义序列化 - 配置
JSON.parse()的reviver参数, 实现Map对象的反序列化
const replace = (key, value) => {
if (!(value instanceof Map)) {
return value
}
// 针对 Map 类型数据做处理: 添加 dataType 标志位、将值转换存储
return {
dataType: 'Map',
value: Array.from(value.entries())
}
}
const reviver = (key, value) => {
// 针对带有 dataType = Map 标志位的数据进行处理
if (value?.dataType === 'Map') {
return new Map(value.value)
}
return value
}
const originalMap = new Map([
['name', 'MoYuanJun'],
['age', 18],
])
const str = JSON.stringify(originalMap, replace)
console.log(str);
//打印: {"dataType":"Map","value":[["name","MoYuanJun"],["age",18]]}
const newMap = JSON.parse(str, reviver)
console.log(newMap);
// 打印: Map(2) { 'name' => 'MoYuanJun', 'age' => 18 }
7.2 键相等性比较
我们都知道在 == 和 === 运算中, 不同的 NaN 是被视为不同的值
NaN == NaN // false
NaN === NaN // false
NaN === Number('foo') // false
但是呢在 Map 中关于 键的比较 是基于 零值相等 算法, 也就是说 Map 在比较键时不同的 NaN 是被视为同一个值, 同时 0 和 -0 是认为是同一个键
const myMap = new Map();
myMap.set(NaN, 'not a number');
myMap.get(NaN); // not a number
const otherNaN = Number('foo');
myMap.get(otherNaN); // not a number
myMap.set(0, 'lh');
myMap.get(-0); //lh
更多关于 JS 相等性概念可查阅 「我不知道的 JS」 相等性判断 🔥🔥🔥🔥
八、总结
Map中任何值都可以作为一个键或一个值Map中每个元素都是有序的, 是按照首次插入顺序Map对象是个可迭代对象Map在频繁增删键值对的场景下表现得更好Map是不能够被JSON.stringify()进行序列化的, 需要进行额外处理
九、参考
大家好, 我是墨渊君, 如果您喜欢我的文章可以:
- 关注公众号: 「昆仑虚F2E」获取最新文章。
- GitHub: github.com/MoYuanJun
- 个人网站(昆仑虚, 虽然现在没啥东西): www.kunlunxu.cc