Set和Map之Map

map的含义和基本用法

Js中的对象(Object),本质上是键值对的集合(Hash结构),但是传统上只能用字符串当作键,这给它的使用带来了很大的限制。

const data = {};
const element = document.getElementById("myDiv");

data[element] = 'metadata';
data['[Object HTMLDIVElement]'] // metadata

上面代码的原意是将一个DOM节点作为对象data的键,但是由于对象只接受字符串作为键名,所以element被自动转为了字符串[Object HTMLDivElement]。

为了解决这个问题,ES6提供了Map数据结构。它类似于对象,也是键值对的集合,但是键的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object结构提供了”字符串 - 值“的对应,Map结构提供了”值 - 值”的对应,是一种更完善的Hash结构实现。如果你需要“键值对”的数据结构,Map比Object更合适。

const m = new Map();
const o = {p:"hello,world"};
m.set(o,'content')
m.get(o) // "content"

m.has(o) // true
m.delete(o) // true
m.has(o) // false

上面的代码使用Map结构的set方法,将对象o当作m的一个键,然后又使用get方法读取这个键,然后使用delete方法删除了这个键。

以上展示了如何向Map添加成员。作为构造函数,Map也可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组。

const map = new Map([
['name','张三'],
['title','Author']
]);

map.size // 2
map.has('name') //true
map.get('name') // "张三"
map.has('title) //true
map.get('title') // "Author"

上面代码在新建Map实例时,就指定了两个键name和title。
Map构造函数接受数组作为参数,实际上执行的是下面的算法。

const item = [
['name','张三'],
['title','Author']
];
const map = new Map();
items.forEach(
([key,vallue]) => map.set(key,values)
);

事实上,不仅仅是数组,任何具有Interator接口、且每个成员都是一个双元素数组的数据结构都可以当作Map构造函数的参数。这就是说Set和Map都可以用来生成新的Map。

const set = new Set([
['foo',1],
['bar',2]
]);
const m1 = new Map(set);
m1.get('foo') // 1

const m2 = new Map([['baz',3]]);
const m3 = new Map(m2);
m3.get('baz') //3

上面的代码中,我们分别使用Set对象和Map对象,当作Map构造函数的参数,结果都生成了新的Map对象。
如果对同一个键多次赋值,后面的值将覆盖前面的值。

const map = new Map();
map.set(1,'aa').set(1,'bbb');

map.get(1) // "bbb"

上面的代码对键1连续赋值两次后,后一次的值覆盖前一次的值,如果读取一个未知的值,则返回undefined。

new Map().get('asdf'); //undefined

注意只有对同一个对象的引用,Map结构才将其视为同一个键,这一点要非常小心。

const map = new Map();
map.set(['a'],233);
map.get(['a']) // undefined

上面的set和get方法,表面是针对同一个键,但实际上这是两个不同的数组实例,内存地址不一样,因此get方法无法读取该键,返回的是undefied.同理,同样的值的两个实例,在Map结构中被视为两个键。

const map = new Map();

const  k1 = ['a'];
const k2 = ['a'];
map.set(k1,111).set(k2,222);
map.get(k1) // 111
map.get(k2) // 222

上面的代码中,变量k1和k2的值是一样的,但是他们在map结构中被视为两个键。

由上可知,Map的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。这就解决了同名属性碰撞(clash)的问题,我们扩展别人的库的时候,如果使用对象作为键名,就不用担心自己的属性与原作者同名。
如果Map的键是一个简单类型的值(数字,字符串,布尔值),则只要两个值严格相等,Map将其视为一个键,比如0和-0就是一个键,布尔值true和字符串true则是两个不同的键。另外,undefined和null是两个不同的键。虽然NaN不严格等于自身,但Map将其视为同一个键。

let 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

实例的属性和操作方法

1、size属性,返回Map结构的成员总数。

const map =new Map();
map.set('foo',true);
map.set('bar',false);
map.size() // 2

2、Map.prototype.set(key,value)
set方法设置键名key对应的值为value,然后返回Map结构。如果key已经有值,则键值会被更新,否则就新生成该键。

const m = new Map();
m.set('edition',6); //键是字符串
m.set(262,'standard') //键是数值
m.set(undefined,'asf') //键是undefined

set方法返回的是当前Map对象,因此可以采用链式写法。

let map = new Map().set(1,'a').set(2.'b').set(3,'c');

3、Map.prototype.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

4、Map.prototype.has(key)
has方法返回一个布尔值,表示某个键是否在当前Map对象之中。

const m = new Map();
m.set('edi',5);
m.set(12,'qwer');
m.set(undefined,'nan');

m.has('edi') // true
m.has('years') // false
m.has(12) //true
m.has(undefined) // true

5、Map.prototype.delete(key)
delete方法删除某个键,返回true,如果删除失败,返回false.

const m =new Map();
m.set(undefined,'qwer');
m.had(undefined) // true

m.delete(undefined)
m.has(undefined) // false

6、Map.prototype.clear()
clear方法清除所有成员,没有返回值。

let map =new Map();
map.set('foo',true);
map.set('bar',false);

map.size // 2
map.clear() 
map.size //0

遍历方法

Map结构原生提供三个遍历器和一个遍历方法。
-Map.prototype.keys():返回键名的遍历器。
-Map.prototype.values():返回键值的遍历器。
-Map.prototype.entries():返回所有成员的遍历器。
-Map.prototype.forEach():遍历Map的所有成员。
Map的遍历顺序就是插入顺序。

const map = new Map([
['F','no'],
['T','yes'],
]);
for(let key of map.keys()) {
console.log(key);
}
// 'F'
// 'T'

for(let value of map.values()) {
console.log(value);
}
// 'no'
// 'yes'

for(let [key,value] of map.entries()) {
console.log(key,value);
}
// "F" "no"
// "T" "yes"

// 或者
for(let item of map.entries()) {
 console.log(key,value);
 }
 // "F" "no"
// "T" "yes"

// 或者
for(let [key,vallue] of map) {
console.log(key,value);
}
 // "F" "no"
// "T" "yes"
// Map结构默认的遍历器接口就是entries方法。
map[Symbol.iterator] === map.entries // true

Map结构转化为数组结构,比较快速的方法就是使用扩展运算符(…)。

const map = new Map([
[1,'one'],
[2,'two'],
[3,'three']
]);
[...map.keys()]
// [1,2,3]

[...map.values()]
// ['one','two','three']

[...map.entries()]
// [[1,'one'],[2,'two'],[3,'three']]

[...map]
// [[1,'one'],[2,'two'],[3,'three']]

结合数组的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)
);
// 产生map结构 {1 => 'a',2 => 'b',3 => 'c'}

const map2 = new Map(
[...map0].map((k,v]) => [k *2,'-'+v])
);
//产生Map结构 {2 => '_a',4 => '_b',6 => '_c'}

此外Map还有一个forEach方法,与数组的forEach方法类似,也可以实现遍历。

map.forEach(function(value,key,map) {
console.log("key:%s,value:%s,key,value);
)};

forEach方法还可以接收第二个参数,用来绑定this。

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);

上面的代码中,forEach方法的回调函数的this,就指向reporter。

与其他数据结构的互相转换

1、Map转为数组
前面已经提过,Map转化为数组最方便的方法,就是使用扩展运算符(…)。

const myMap = new Map().set(true,7).set({foo:3},{''abc'});
[...myMap]
// [[true,7[{foo:3},['abc']]]

2、数组转为Map
将数组传入Map构造函数,就可以转为Map。

new Map([
[true,7],
[{foo:3},['abc']]
])
// Map {
// true => 7,
// Object{foo:3} => ['abc']
// }

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('no',false);
strMapToObj(myMap)
// {yes:true,no:false }

如果有非字符串的键名,那么这个键名会被转成字符串,再作为对象的键名。
4、对象转为Map
对象转为Map可以通过Object.entries()。

let obj = {"a":1,"b":2};
let map new Map(Object.entries(obj));

此外,也可以自己实现一个转换函数。

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 {"yes" => true,"no" => false}

5、Map转为JSON
Map转为JSON要区分两种情况。一种是Map的键名都是字符串,这时可以选择转为对象JSON。

function strMapToJson(strMap) {
return JSON.stringfy(strMap));
}
let myMap = new Map().set('yes',true).set('no',false);
strMapToJson(myMap)
// '{"yes":true,"no":false}'

另一种情况是,Map的键名有非字符串,这时可以转为数组JSON。

function mapToArrayJson(map) {
return JSON.stringfy([...map]);
}
let myMap = new Map().set(true,7).set([foo:3},['abc']);
mapToArrayJson(myMap)
// '[[true,7],[{"foo":3},["abc"]]]'

6、 JSON转为Map
JSON 转为Map,正常情况下,所有键名都是字符串。

function jsonStrMap(jsonStr) {
return objToStrMap(JSON.parse(jsonStr));
}
jsonToStrMap('{"yes":true,"no":false}')
// Map {'yes' => true,'no' => false}

但是有一种特殊情况,整个JSON就是一个素组,且每个数组成员本身,又是一个有两个成员的数组。这时,它可以一一对应地转为Map。这往往是Map转为数组JSON的逆操作。

function jsonToMap(jsonstr) {
return new Map(Json.parse(jsonStr));
}
jsonToMap('[[true,7],[{"foo":3},["abc"]]]")
// Map { true => 7,Object {foo:3} => ['abc']}