JavaScript的对象,本质上是键值对的集合(hash结构),但是传统上只能用字符串作为建,这有了很大的限制。
const data = {};
let obj = { name: 'obj' };
data[obj] = "hahaha";
console.log(data); // { '[object Object]': 'hahaha' }
//上面代码原意是将一个obj对象作为对象data的键,但是由于对象只接受字符串作为键名,所以obj被自动的转为了字符串。
ES6为了解决这个问题,实现真正的键\值存储机制。Map的大多数特性都可以通过Object类型实现,但二者之间还是存在着一些细微的差异。
创建Map
使用new关键字和Map构造函数可以创建一个空映射。
const m = new Map();
如果想要在创建的同时初始化实例,可以给Map构造函数传入一个可迭代对象,需要包含键/值对象组。可迭代对象中的每个键/值对都会按照迭代顺序插入到新映射实例中:
const m1 = new Map([
['key1', 'val1'],
['key2', 'val2'],
['key3', 'val3']
])
console.log(m1); // Map(3) { 'key1' => 'val1', 'key2' => 'val2', 'key3' => 'val3' }
Map构造函数接受数组作为参数,实际上执行的是以下算法。
const arr = [
['key1', 'val1'],
['key2', 'val2'],
['key3', 'val3']
]
const map = new Map();
arr.forEach(([key,value]) => map.set(key,value));
console.log(map); // Map(3) { 'key1' => 'val1', 'key2' => 'val2', 'key3' => 'val3' }
Map的基本使用
初始化之后,可以用 set() 方法再添加键/值对,另外,可以使用get()和has()进行查询,可以通过size属性获取映射中的键\值对的数量,还可以使用delete()和clear()删除值。
set()
const m = new Map();
const obj = { name: 'ly' };
m.set(obj, 'content');
console.log(m); // Map(1) { { name: 'ly' } => 'content' }
set()方法返回映射实例,因此可以吧多个操作连缀起来,包括初始化声明:
const m = new Map().set('key1', 'val1');
m.set('key2', 'val2').set('key3', 'val3')
console.log(m); // Map(3) { 'key1' => 'val1', 'key2' => 'val2', 'key3' => 'val3' }
get()和has()
const m1 = new Map([
['key1', 'val1'],
['key2', 'val2'],
['key3', 'val3']
])
console.log(m1.get('key2')); // val2
console.log(m1.has('name')); // false
console.log(m1.has('key3')); // true
delete()和clear()
delete是删除这一个键值对,而clear是清除这个实例中的所有键值对
const m1 = new Map([
['key1', 'val1'],
['key2', 'val2'],
['key3', 'val3']
])
m1.delete('key2')
console.log(m1.has('key2'));
m1.clear(); // false
console.log(m1); // Map(0) {}
在Map结构中,如果对同一个值复制多次,后面的值将覆盖前面的值。
const map = new Map();
map.set(1, "aaa").set(1, "bbb");
console.log(map.get(1)); //bbb aaa被覆盖
如果读取一个未知的键,则返回undefined。
console.log(new Map().get("name")); //undefined
与Object只能使用数值、字符串或符号作为健不同,Map可以使用任何JavaScript数据类型作为健。Map内部使用SameValueZero比较操作(ECMAScript规范内部定义,语言中不能使用),基本上相当于使用严格对象相等的标准来检查键的匹配性。与Object类似,映射的值是没有限制的。
const m = new Map();
const fun = function() {};
const sym = Symbol();
const obj = new Object();
m.set(fun, 'function').set(sym, 'symbol').set(obj, 'object');
console.log(m.get(fun)); // function
console.log(m.get(sym)); // symbol
console.log(m.get(obj)); // object
注意: 只有对同一个对象的引用,Map结构才将其视为同一个键。引用值由于存储地址不同,相同的会被视为两个值。
const map = new Map();
map.set([],"arr");
console.log(map.get([])); //undefined
同理,同样的值的两个实例,在Map结构中被视为两个键。
const map = new Map();
const k1 = ['a'];
const k2 = ['a'];
map.set(k1,111);
map.set(k2,222);
console.log(map.get(k1)); //111
console.log(map.get(k2)); //222
与严格相等一样,在映射中用作键和值的对象及其他“集合”类型,在自己的内容或属性被修改时仍然保持不变。
const m = new Map();
const objKey = {},
objVal = {},
arrKey = [],
arrVal = [];
m.set(objKey, objVal);
m.set(arrKey, arrVal);
objKey.foo = 'foo';
objVal.bar = 'bar'
arrKey.push('foo');
arrVal.push('bar');
console.log(m); // Map(2) { { foo: 'foo' } => { bar: 'bar' }, [ 'foo' ] => [ 'bar' ] }
console.log(m.get(objKey)); // { bar: 'bar' }
console.log(m.get(arrKey)); // [ 'bar' ]
如果Map的键是一个原始数据类型,只要两个键严格相同,就视为是同一个键。
const map = new Map();
map.set(-0,1);
console.log(map.get(+0)); //1
map.set(true,2);
map.set('true',3);
console.log(map.get(true)); //2
map.set(undefined,4);
map.set(null,5);
console.log(map.get(undefined)); //4
map.set(NaN,6);
console.log(map.get(NaN)); //6
遍历方法
Map结构原生提供是三个遍历器生成函数和一个遍历方法。
keys():返回键名的遍历器。
values():返回键值的遍历器。
entries():返回所有成员的遍历器。
forEach():遍历Map的所有成员。
注意:Map的遍历顺序是插入顺序。
const map = new Map([
["foo",1],
["bar",2],
])
for(let key of map.keys()){
console.log(key);
}
--> //foo
bar
for(let value of map.values()){
console.log(value);
}
--> // 1
2
for(let items of map.entries()){
console.log(items);
}
--> // [ 'foo', 1 ]
[ 'bar', 2 ]
Map结构的默认遍历器接口就是entries方法。
const map = new Map();
console.log(map[Symbol.iterator] === map.entries); //true
Map的使用
Map结构转为数组可以用扩展运算符。
const map = new Map([
[1,"foo"],
[2,"bar"],
[3,"baz"]
])
console.log([...map.keys()]) //[1,2,3]
console.log([...map.values()]); //['foo','bar','baz']
console.log([...map.entries()]); //[[1,'foo'],[2,'bar'],[3,'baz']]
console.log([...map]) // [ [ 1, 'foo' ], [ 2, 'bar' ], [ 3, 'baz' ] ]
结合数组的map方法,filter方法,可以实现Map的遍历和过滤。
const map = new Map([
[1, 'a'],
[2, 'b'],
[3, 'c'],
]);
const map1 = new Map([...map].filter(([k, v]) => k < 3));
console.log(map1);
// Map(2) { 1 => 'a', 2 => 'b' }
const map2 = new Map([...map].map(([k, v]) => [k * 2, v]));
console.log(map2);
// Map(3) { 2 => 'a', 4 => 'b', 6 => 'c' }
Map有一个forEach方法,与数组的forEach方法类似,也可以实现遍历,还可以接受第二个参数,用来绑定this。
const map = new Map();
map.set(1,"for").set(2,"bar").set(3,"baz");
map.forEach( (value,key,map) => {
console.log(key,value);
})
// 1 for
2 bar
3 baz
const reporter = {
report(key,value) {
console.log("Key %s","Value %s",key,value);
}
}
map.forEach( function (value,key,map) {
this.report(key,value);
},reporter);
// Key Value %s 1 for
Key Value %s 2 bar
Key Value %s 3 baz
WeakMap
Weakmap结构与Map结构类似,也是用于生成键值的集合。WeakMap是Map的兄弟类型,其API也是Map的子集。WeakMap中的“weak(弱)”描述的是JS垃圾回收对待“弱映射”中键的方式。
WeakMap的基本使用
可以使用mew关键字实例化一个空的WeakMap。
cosnt m = new WeakMap();
弱映射中的键只能是Object或者继承自Object的类型,尝试使用非对象设置键会抛出TypeError。值的类型没有限制。
如果想在初始化时填充弱映射,则构造函数可以接收一个可迭代对象,其中包括键/值对数组。可迭代对象中的每一个键\值都会按照迭代顺序插入到新实例中:
const key1 = { id: 1 },
key2 = { id: 2 },
key3 = { id: 3 }
const wm1 = new WeakMap([
[key1, 'val1'],
[key2, 'val2'],
[key3, 'val3']
])
console.log(wm1.get(key1)); // val1
console.log(wm1.get(key2)); // val2
console.log(wm1.get(key3)); // val3
初始化是一个全有或全无的操作,只要有一个键无效或者抛出错误,导致整个初始化失败。
使用原始值作为健会抛出错误
const stringKey = 'key1';
const wm2 = new WeakMap([
[stringKey, 'val1']
]);
console.log(wm2.get(stringKey));
// TypeError: Invalid value used as weak map key
但是原始值可以先包装成对象再用作键
const stringKey = new String('key1');
const wm2 = new WeakMap([
[stringKey, 'val1']
]);
console.log(wm2.get(stringKey)); // val1
也可以初始化后再使用set()添加键值对,可以使用get()和has()查询,还可以使用delete()删除。(使用方法同Map)
WeakMap与Map的区别
WeakMap与Map有两点区别。
第一:和WeakSet一样,WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。
第二:WeakSet的键名所指的对象,不计入垃圾回收机制。
WeakMap的设计目的在于,有时我们想在某个对象上面存放一些数据,但是这会形成对于这个对象的引用。一旦不再需要这两个对象,我们就必须手动删除这个引用,否则垃圾回收机制就不会释放对象占用的内存。
const e1 = document.getElementById("foo");
const e2 = document.getElementById("bar");
const arr = [
[e1,"foo元素"],
[e2,"bar元素"],
]
//不再使用必须手动删除引用
arr[0] = null;
arr[1] = null;
WeakMap 就是为了解决这个问题而诞生的,它的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。
基本上,如果你要往对象上添加数据,又不想干扰垃圾回收机制,就可以使用 WeakMap。一个典型应用场景是,在网页的 DOM 元素上添加数据,就可以使用WeakMap结构。当该 DOM 元素被清除,其所对应的WeakMap记录就会自动被移除。
const wm = new WeakMap();
const element = document.getElementsByTagName("div")[0];
wm.set(element,"some information");
console.log(wm.get(element));
注意的是,WeakMap弱引用的只是键名,而不是键值。键值依然是正常引用。
const wm = new WeakMap();
let key = {};
let obj = {foo : 1};
wm.set(key,obj);
obj = null;
console.log(wm.get(key)); //{foo : 1}
WeakMap的遍历
WeakMap和WeakSet相似,没有遍历方法和清空方法,WeakMap只有四个方法可以使用:get(), set(), has(), delete()。(可以查看上一章的WeakSet了解具体)
WeakMap的应用
WeakMap应用的典型场合就是Dom节点作为键名。
let myElement = document.getElementsByTagName('div')[0];
let myWeakMap = new WeakMap();
myWeakMap.set(myElement,{timesClicked: 0});
myElement.addEventListener('click',function(){
let logoData = myWeakMap.get(myElement);
console.log(logoData.timesClicked ++);
},false)
//上面代码中,myElement是一个 DOM 节点,每当发生click事件,就更新一下状态。我们将这个状态作为键值放在 WeakMap 里,
//对应的键名就是myElement。一旦这个 DOM 节点删除,该状态就会自动消失,不存在内存泄漏风险。
WeakMap的另一个用处是部署私有属性。这个我们等后面构造函数或者类的时候再总结总结吧...