关于 JS 中 Map 的长篇大论

一、Map 简介

在 JS 中 Map 对象是 键值对 的一个 集合:

  1. Map任何值 都可以作为一个键或一个值
  2. Map 中每个元素是有顺序的, 它能够记住每个元素首次 插入顺序
  3. ObjectMap 中的一个键只能出现一次, 它在 Map 的集合中是独一无二的
  4. 同时 Map 内部实现了 Symbol.iterator 接口, 其本身又是一个 可迭代对象, 可被 for...of 等语法进行迭代, 关于更多迭代器的相关知识可查阅 《迭代器、生成器详解🔥🔥🔥》

二、基本操作

2.1 创建

创建一个 Map 实例, 可直接通过 Map 构造函数进行创建

new Map()

如果我们需要在创建 Map 实例时, 就给定初始值, 可传一个二维数组, 如下代码所示: nameagekey 值, MoYuanJun18 是对应的 value

const map = new Map([
  ['name', 'MoYuanJun'],
  ['age', 18],
])

2.2 增、改、删、查

  1. 可通过 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)
  1. 可通过 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
  1. 可通过 Map.prototype.clear() 方法清除 Map 中的所有元素, 该方法没有返回值
const map = new Map([
  ['name', 'MoYuanJun'],
  ['age', 18],
])

const res = map.clear()

console.log(res, map) // undefined Map(0) {}
  1. 可通过 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' }
// }
  1. 可通过 Map.prototype.has(key) 方法判断 Map 对象中指定元素是否存在, 该方法返回一个布尔值, 表示对应的元素是否存在

const map = new Map([
  ['name', 'MoYuanJun'],
  ['age', 18],
])

map.has('name') // true
map.has('address') // false
  1. 可通过 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 对象中 keyvalue 以及 [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' ]
}

四、复制与合并

  1. 上文提到 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 }
  1. Map 对象间可以进行合并, 但是会保持键的唯一性, 否则前面的会被后面的覆盖, 如下代码所示: 尝试将 map1map2 进行了合并, 同时 map1agemap2 覆盖
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」 的比较

JSObjectMap 类型, 它们都允许按 对数据进行存储、删除、修改…… 不过 MapObject 还是存在一些重要的区别:

MapObject
意外的键默认不包含任何键、只包含显式插入的键存在原型, 原型链上的键名可能和对象上的设置的键名产生冲突
键的类型可以是任意值StringSymbol
键的顺序等于首次插入的顺序键目前是有序的, 但这个顺序相对来说是复杂的、不可控的
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 缓存: 即采用 最近最少 使用的缓存策略, 它的原则是, 如果一个数据最近没有被访问到, 那么它将来被访问的几率也很小, 那么在有限的内存空间下我们就可以把长时间没有访问到的数据去除掉

如下代码, 利用 Mapkey 具有顺序的特性实现 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() 反序列化的, 但实际上我们可以通过:

  1. 配置 JSON.stringify()replacer 参数, 实现 Map 对象的自定义序列化
  2. 配置 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」 相等性判断 🔥🔥🔥🔥

八、总结

  1. Map任何值 都可以作为一个键或一个值
  2. Map 中每个元素都是有序的, 是按照首次插入顺序
  3. Map 对象是个可迭代对象
  4. Map 在频繁增删键值对的场景下表现得更好
  5. Map 是不能够被 JSON.stringify() 进行序列化的, 需要进行额外处理

九、参考


image

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值