Map
含义和基本用法
JavaScript的对象(Object),本质上是键值对的集合(Hash结构),但传统上只能用字符串当做键。
ES6提供了Map数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当做键。Object提供的是“字符串-值”的对应Map结构提供了“值-值”的对应,是一种更完善的Hash结构对应。
const m = new Map()
const o = { p: 'hello' }
m.set(o, 'content')
console.log(m)
const map = new Map([['name', '张三'], ['title', 'author']])
//实际进行的操作
const items = [['name', '张三'], ['title', 'author']]
const map2 = new Map()
items.forEach(([key, value]) => map2.set(key, value))
console.log(map, map2)
不仅仅是数组,任何具有Iterator接口、且每个成员都是一个双元素的数组的数据结构都可以当做Map构造函数的参数——Set和Map都可以用来生成新的Map。
const set = new Set([['foo', 1], ['bar', 2]])
const m1 = new Map(set)
const m2 = new Map([['baz', 3]])
const m3 = new Map(m2)
console.log('m1:', m1, 'm2:', m2, 'm3:', m3)
如果获取一个未知的键,返回undefined
只有对同一个对象的引用,Map结构才会将其视为同一个键
const map = new Map()
map.set(['a'], 555)
console.log(map.get(['a']))
这里的set和get是两个不同的数组实例,内存地址是不一样的,所以get方法无法获取该键返回undefined
Map的键是一个简单类型的值(数字,字符串,布尔值),只要两个值严格相等,Map将其视为一个键,比如0和-0是一个键,布尔值的true和字符串的true是两个不同的键。另外undefined和null也是两个不同的键,虽然NaN不严格等于自身,但Map将其视为同一个键
Map结构的实例的属性和操作方法
- size:返回Map结构的成员数
- Map.prototype.set(key,value)
set方法设置键名key对应的键值为value,然后返回整个Map结构。如果key已经有值,则键值会被更新,否则就生成该键
set方法返回的是当前Map对象,因此可采用链式写法 - Map.prototype.get(key)
get方法读取key对应的键值,如果找不到key,则返回undefined - Map.prototype.delete(key)
delete方法删除某个键,返回true。如果删除失败,返回false - Map.prototype.clear()
clear()方法清除所有成员,没有返回值
遍历方法
Map结构原生提供三个遍历器生成函数和一个遍历方法
- Map.prototype.keys():返回键名的遍历器
- Map.prototype.values(): 返回键值的遍历器
- Map.prototype.entries():返回所有成员的遍历器
- Map.prototype.forEach():遍历Map的所有成员‘
Map的遍历顺序就是插入顺序
Map结构的默认遍历器接口(Symbol.interator属性)就是entires方法
结合数组的map方法、filter方法,可以实现Map的遍历和过滤(Map本身没有map和filter方法)
const map0 = new Map()
.set(1, 'a')
.set(2, 'b')
.set(3, 'c')
const map1 = new Map([...map0].filter(([k, v]) => k < 3))
const map2 = new Map([...map0].map(([k, v]) => [k * 2, '_' + v]))
console.log('map0:', map0)
console.log('map1:', map1)
console.log('map2:', map2)
Map有个forEach方法与数组的forEach类似
forEach可以接受第二个参数,用来绑定this
let map = new Map()
.set(1, 'a')
.set(2, 'b')
.set(3, 'c')
const reporter = {
report: function(key, value) {
console.log('key: %s,Value: %s', key, value)
}
}
map.forEach(function(value, key, map) {
this.report(key, value)
}, reporter)
与其他数据结构的相互转换
’1. Map转成数组最方便的方法:扩展运算符(…)
const myMap = new Map().set(true, 7).set({ foo: 3 }, ['abc'])
let arr = [...myMap]
console.log(arr)
2. 数组转成Map
let map = new Map([[true, 7], [{ foo: 3 }, ['abc']]])
console.log(map)
3. Map转成对象
如果所有Map的键都是字符串,则可以无损地转为对象,但若存在非字符串的键名,这个键名会先转成字符串,再作为对象的键名。
function strMapToObj(strMap) {
let obj = Object.create(null)
for (let [k, v] of strMap) {
obj[k] = v
}
return obj
}
const myMap = new Map().set('yes', true).set(false, false)
let res = strMapToObj(myMap)
console.log(res)
4. 对象转为Map
function objToStrMap(obj) {
let strMap = new Map();
for (let k of Object.keys(obj)) {
strMap.set(k, obj[k]);
}
return strMap;
}
objToStrMap({yes: true, no: false})
- Map转为JSON
- Map键名都是字符串,转成对象JSON
- Map键名有非字符串,可以选择转成数组JSON
function strMapToObj(strMap) {
let obj = Object.create(null)
for (let [k, v] of strMap) {
obj[k] = v
}
return obj
}
function strMapToJson(strMap) {
return JSON.stringify(strMapToObj(strMap))
}
function mapToArrayJson(map) {
return JSON.stringify([...map])
}
let myMap1 = new Map().set('yes', true).set('no', false)
let toJson = strMapToJson(myMap1)
let myMap2 = new Map().set(true, 7).set({ foo: 3 }, ['abc'])
let toArray = mapToArrayJson(myMap2)
console.log(toJson, toArray)
6. JSON 转成Map
JSON转成Map,键名都是字符串
function objToStrMap(obj) {
let strMap = new Map()
for (let k of Object.keys(obj)) {
strMap.set(k, obj[k])
}
return strMap
}
function jsonToStrMap(jsonStr) {
return objToStrMap(JSON.parse(jsonStr))
}
let myMap = jsonToStrMap('{"yes": true, "no": false}')
console.log(myMap)
如果整个JSON就是一个数组,且每个数组成员本身,又是一个有两个成员的数组,那么可以转为Map。
function jsonToStrMap(jsonStr) {
return new Map(JSON.parse(jsonStr))
}
let myMap = jsonToStrMap('[[true,7],[{"foo":3},["abc"]]]')
console.log(myMap)
WeakMap
WeakMap与Map的区别:
- WeakMap只接受对象作为键名
- WeakMap的键名所指向的对象,不计入垃圾回收机制
WeakMap的键名所引用的对象都是弱引用——垃圾回收机制不考虑在内(只要所引用的对象的其他引用被清除,来及回收机制会释放对该对象所占用的内存)
适用场所
其键对应的对象可能在将来会消失。此结构有助于防止内存泄漏
语法
有4个方法可用:
- get()
- set()
- has()
- delete()
示例
// 手动执行一次垃圾回收,保证获取的内存使用状态准确
> global.gc();
undefined
// 查看内存占用的初始状态,heapUsed 为 4M 左右
> process.memoryUsage();
{ rss: 21106688,
heapTotal: 7376896,
heapUsed: 4153936,
external: 9059 }
> let wm = new WeakMap();
undefined
// 新建一个变量 key,指向一个 5*1024*1024 的数组
> let key = new Array(5 * 1024 * 1024);
undefined
// 设置 WeakMap 实例的键名,也指向 key 数组
// 这时,key 数组实际被引用了两次,
// 变量 key 引用一次,WeakMap 的键名引用了第二次
// 但是,WeakMap 是弱引用,对于引擎来说,引用计数还是1
> wm.set(key, 1);
WeakMap {}
> global.gc();
undefined
// 这时内存占用 heapUsed 增加到 45M 了
> process.memoryUsage();
{ rss: 67538944,
heapTotal: 7376896,
heapUsed: 45782816,
external: 8945 }
// 清除变量 key 对数组的引用,
// 但没有手动清除 WeakMap 实例的键名对数组的引用
> key = null;
null
// 再次执行垃圾回收
> global.gc();
undefined
// 内存占用 heapUsed 变回 4M 左右,
// 可以看到 WeakMap 的键名引用没有阻止 gc 对内存的回收
> process.memoryUsage();
{ rss: 20639744,
heapTotal: 8425472,
heapUsed: 3979792,
external: 8956 }
用途
DOM节点作为键名,布置私有属性
//作为键名
let myElement = document.getElementById('logo');
let myWeakmap = new WeakMap();
myWeakmap.set(myElement, {timesClicked: 0});
myElement.addEventListener('click', function() {
let logoData = myWeakmap.get(myElement);
logoData.timesClicked++;
}, false);
//布置私有属性
const _counter = new WeakMap();
const _action = new WeakMap();
class Countdown {
constructor(counter, action) {
_counter.set(this, counter);
_action.set(this, action);
}
dec() {
let counter = _counter.get(this);
if (counter < 1) return;
counter--;
_counter.set(this, counter);
if (counter === 0) {
_action.get(this)();
}
}
}
const c = new Countdown(2, () => console.log('DONE'));
c.dec()
c.dec()
// DONE