ES6中的Map集合(与java里类似)
Set类型可以用来处理列表中的值,但是不适用于处理键值对这样的信息结构。ES6也添加了Map集合来解决类似的问题
一、Map集合
JS的对象(Object),本质上是键值对的集合(Hash结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制
为了解决这个问题,ES6提供了Map数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object结构提供了“字符串—值”的对应,Map结构提供了“值—值”的对应,是一种更完善的Hash结构实现
ES6中的Map类型是一种储存着许多键值对的有序列表,其中的键名和对应的值支持所有的数据类型。键名的等价性判断是通过调用Object.is()方法实现的,所以数字5与字符串"5"会被判定为两种类型,可以分别作为独立的两个键出现在程序中,这一点与对象不一样,因为对象的属性名总会被强制转换成字符串类型
注意:有一个例外,Map集合中将+0和-0视为相等,与Object.is()结果不同
如果需要“键值对”的数据结构,Map比Object更合适
1、创建Map集合
如果要向Map集合中添加新的元素,可以调用set()方法并分别传入键名和对应值作为两个参数;如果要从集合中获取信息,可以调用get()方法
let map = new Map();
map.set("title", "Understanding ES6");
map.set("year", 2017);
console.log(map.get("title")); // "Understanding ES6"
console.log(map.get("year")); // 2017
在这个示例中,两组键值对分别被存入了集合Map中,键名"title"对应的值是一个字符串,键名"year"对应的值是一个数字。
调用get()方法可以获得两个键名对应的值。如果调用get()方法时传入的键名在Map集合中不存在,则会返回undefined
在对象中,无法用对象作为对象属性的键名。但是在Map集合中,却可以这样做
let map = new Map(),
key1 = {},
key2 = {};
map.set(key1, 5);
map.set(key2, 42);
console.log(map.get(key1)); // 5
console.log(map.get(key2)); // 42
在这段代码中,分别用对象key1和key2作为两个键名在Map集合里存储了不同的值。这些键名不会被强制转换成其他形式,所以这两个对象在集合中是独立存在的,也就是说,以后不再需要修改对象本身就可以为其添加一些附加信息
2、Map集合支持的方法
在设计语言新标准时,委员会为Map集合与Set集合设计了如下3个通用的方法
(1)has(key)检测指定的键名在Map集合中是否已经存在
(2)delete(key)从Map集合中移除指定键名及其对应的值
(3)clear()移除Map集合中的所有键值对
Map集合同样支持size属性,其代表当前集合中包含的键值对数量
let map = new Map();
map.set("name", "huochai");
map.set("age", 25);
console.log(map.size); // 2
console.log(map.has("name")); // true
console.log(map.get("name")); // "huochai"
console.log(map.has("age")); // true
console.log(map.get("age")); // 25
map.delete("name");
console.log(map.has("name")); // false
console.log(map.get("name")); // undefined
console.log(map.size); // 1
map.clear();
console.log(map.has("name")); // false
console.log(map.get("name")); // undefined
console.log(map.has("age")); // false
console.log(map.get("age")); // undefined
console.log(map.size); // 0
Map集合的size属性与Set集合中的size属性类似,其值为集合中键值对的数量。在此示例中,首先为Map的实例添加"name"和"age"这两个键名;然后调用has()方法,分别传入两个键名,返回的结果为true;调用delete()方法移除"name",再用has()方法检测返回false,且size的属性值减少1;最后调用clear()方法移除剩余的键值对,调用has()方法检测全部返回false,size属性的值变为0;clear()方法可以快速清除Map集合中的数据,同样,Map集合也支持批量添加数据
3、传入数组来初始化Map集合
可以向Map构造函数传入数组来初始化一个Map集合,这一点同样与Set集合相似。数组中的每个元素都是一个子数组,子数组中包含一个键值对的键名与值两个元素。因此,整个Map集合中包含的全是这样的两元素数组
let map = new Map([["name", "huochai"], ["age", 25]]);
console.log(map.has("name")); // true
console.log(map.get("name")); // "huochai"
console.log(map.has("age")); // true
console.log(map.get("age")); // 25
console.log(map.size); // 2
初始化构造函数之后,键名"name"和"age"分别被添加到Map集合中。数组包裹数组的模式看起来可能有点儿奇怪,但由于Map集合可以接受任意数据类型的键名,为了确保它们在被存储到Map集合中之前不会被强制转换为其他数据类型,因而只能将它们放在数组中,因为这是唯一一种可以准确地呈现键名类型的方式
4、同名属性碰撞
Map的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。这就解决了同名属性碰撞(clash)的问题,扩展别人的库的时候,如果使用对象作为键名,就不用担心自己的属性与原作者的属性同名
const map = new Map();
map.set(['a'], 555);
map.get(['a']) // undefined
上面代码的set
和get
方法,表面是针对同一个键,但实际上这是两个值,内存地址是不一样的,因此get
方法无法读取该键,返回undefined
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 结构中被视为两个键
5、遍历
Map结构原生提供三个遍历器生成函数和一个遍历方法
keys():返回键名的遍历器
values():返回键值的遍历器
entries():返回所有成员的遍历器
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 item of map.entries()) {
console.log(item[0], item[1]);
}
// "F" "no"
// "T" "yes"
// 或者
for (let [key, value] of map.entries()) {
console.log(key, value);
}
// "F" "no"
// "T" "yes"
// 等同于使用map.entries()
for (let [key, value] of map) {
console.log(key, value);
}
// "F" "no"
// "T" "yes"
上面代码最后的那个例子,表示Map结构的默认遍历器接口,就是entries
方法
map[Symbol.iterator] === map.entries// true
6、转为数组
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 的遍历和过滤
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'}
const map2 = new Map(
[...map0].map(([k, v]) => [k * 2, '_' + v])
);
// 产生 Map 结构 {2 => '_a', 4 => '_b', 6 => '_c'}
7、forEach()
Map还有一个forEach
方法,与数组的forEach
方法类似,也可以实现遍历
const map = new Map([[1, 'one'],[2, 'two'],[3, 'three']]);
map.forEach((value,key,map)=>{
//one 1 {1 => "one", 2 => "two", 3 => "three"}
//two 2 {1 => "one", 2 => "two", 3 => "three"}
//three 3 {1 => "one", 2 => "two", 3 => "three"}
console.log(value,key,map);
})
注意:遍历过程中,Map会按照键值对插入Map集合的顺序将相应信息传入forEach()方法的回调函数;而在数组中,会按照数值型索引值的顺序依次传入回调函数
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
二、WeakMap
WeakSet是引用Set集合,相对地,WeakMap是弱引用Map集合,也用于存储对象的弱引用
WeakMap集合中的键名必须是一个对象,如果使用非对象键名会报错;集合中保存的是这些对象的弱引用,如果在弱引用之外不存在其他的强引用,引擎的垃圾回收机制会自动回收这个对象,同时也会移除WeakMap集合中的键值对。但是只有集合的键名遵从这个规则,键名对应的值如果是一个对象,则保存的是对象的强引用,不会触发垃圾回收机制
WeakMap集合最大的用途是保存Web页面中的DOM元素,例如,一些为Web页面打造的JS库,会通过自定义的对象保存每一个引用的DOM元素
使用这种方法最困难的是,一旦从Web页面中移除保存过的DOM元素,如何通过库本身将这些对象从集合中清除;否则,可能由于库过于庞大而导致内存泄露,最终程序不再正常执行。如果用WeakMap集合来跟踪DOM元素,这些库仍然可以通过自定义的对象整合每一个DOM元素,而且当DOM元素消失时,可以自动销毁集合中的相关对象
1、使用WeakMap集合
ES6中的Weak Map类型是一种存储着许多键值对的无序列表,列表的键名必须是非null类型的对象,键名对应的值则可以是任意类型。WeakMap的接口与Map非常相似,通过set()方法添加数据,通过get()方法获取数据
let map = new WeakMap(),
element = document.querySelector(".element");
map.set(element, "Original");
let value = map.get(element);
console.log(value); // "Original"
// 移除元素
element.parentNode.removeChild(element);
element = null;
// 该 Weak Map 在此处为空
在这个示例中储存了一个键值对,键名element是一个DOM元素,其对应的值是一个字符串,将DOM元素传入get()方法即可获取之前存过的值。如果随后从document对象中移除DOM元素并将引用这个元素的变量设置为null,那么WeakMap集合中的数据也会被同步清除
与WeakSet集合相似的是,WeakMap集合也不支持size属性,从而无法验证集合是否为空;同样,由于没有键对应的引用,因而无法通过get()方法获取到相应的值,WeakMap集合自动切断了访问这个值的途径,当垃圾回收程序运行时,被这个值占用的内存将会被释放
2、WeakMap集合的初始化方法
WeakMap集合的初始化过程与Map集合类似,调用WeakMap构造函数并传入一个数组容器,容器内包含其他数组,每一个数组由两个元素构成:第一个元素是一个键名,传入的值必须是非null的对象;第二个元素是这个键对应的值(可以是任意类型)
let key1 = {},
key2 = {},
map = new WeakMap([[key1, "Hello"], [key2, 42]]);
console.log(map.has(key1)); // true
console.log(map.get(key1)); // "Hello"
console.log(map.has(key2)); // true
console.log(map.get(key2)); // 42
对象key1和key2被当作WeakMap集合的键使用,可以通过get()方法和has()方法去访问。如果给WeakMap构造函数传入的诸多键值对中含有非对象的键,会导致程序抛出错误
3、WeakMap集合支持的方法
WeakMap集合只支持两个可以操作键值对的方法:
has()方法可以检测给定的键在集合中是否存在;
delete()方法可移除指定的键值对。
WeakMap集合与WeakSet集合一样,都不支持键名枚举,从而也不支持clear()方法
let map = new WeakMap(),
element = document.querySelector(".element");
map.set(element, "Original");
console.log(map.has(element)); // true
console.log(map.get(element)); // "Original"
map.delete(element);
console.log(map.has(element)); // false
console.log(map.get(element)); // undefined
在这段代码中,我们还是用DOM元素作为Weak Map集合的键名。has()方法可以用于检查Weak Map集合中是否存在指定的引用;Weak Map集合的键名只支持非null的对象值;调用delete()方法可以从Weak Map集合中移除指定的键值对,此时如果再调用has()方法检查这个键名会返回false,调用get()方法返回undefined
4、用途
(1)储存DOM元素:前面介绍过,WeakMap应用的典型场合就是 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);
上面代码中,myElement
是一个 DOM 节点,每当发生click
事件,就更新一下状态。我们将这个状态作为键值放在WeakMap里,对应的键名就是myElement
。一旦这个 DOM 节点删除,该状态就会自动消失,不存在内存泄漏风险
进一步说,注册监听事件的listener
对象,就很合适用 WeakMap 实现
const listener = new WeakMap();
listener.set(element1, handler1);
listener.set(element2, handler2);
element1.addEventListener('click', listener.get(element1), false);
element2.addEventListener('click', listener.get(element2), false);
上面代码中,监听函数放在WeakMap里面。一旦 DOM 对象消失,跟它绑定的监听函数也会自动消失
(2)部署私有属性:WeakMap的另一个用处是部署私有属性
function Person(name) {
this._name = name;
}
Person.prototype.getName = function() {
return this._name;
};
在这段代码中,约定前缀为下划线_的属性为私有属性,不允许在对象实例外改变这些属性。例如,只能通过getName()方法读取this._name属性,不允许改变它的值。然而没有任何标准规定如何写_name属性,所以它也有可能在无意间被覆写
在ES5中,可以通过以下这种模式创建一个对象接近真正的私有数据
var Person = (function() {
var privateData = {},
privateId = 0;
function Person(name) {
Object.defineProperty(this, "_id", { value: privateId++ });
privateData[this._id] = {
name: name
};
}
Person.prototype.getName = function() {
return privateData[this._id].name;
};
return Person;
}());
在上面的示例中,变量person由一个立即调用函数表达式(IIFE)生成,包括两个私有变量privateData和privateld。privateData对象储存的是每一个实例的私有信息,privateld则为每个实例生成一个独立ID。当调用person构造函数时,属性_id的值会被加1,这个属性不可枚举、不可配置并且不可写
然后,新的条目会被添加到privateData对象中,条目的键名是对象实例的ID;privateData对象中储存了所有实例对应的名称。调用getName()函数,即可通过this_id获得当前实例的ID,并以此从privateData对象中提取实例名称。在IIFE外无法访问privateData对象,即使可以访问this._id,数据实际上也很安全
这种方法最大的问题是,如果不主动管理,由于无法获知对象实例何时被销毁,因此privateData中的数据就永远不会消失。而使用WeakMap集合可以解决这个问题
let Person = (function() {
let privateData = new WeakMap();
function Person(name) {
privateData.set(this, { name: name });
}
Person.prototype.getName = function() {
return privateData.get(this).name;
};
return Person;
}());
经过改进后的Person构造函数选用一个WeakMap集合来存放私有数据。由于Person对象的实例可以直接作为集合的键使用,无须单独维护一套ID的体系来跟踪数据。调用Person构造函数时,新条目会被添加到WeakMap集合中,条目的键是this,值是对象包含的私有信息。在这个示例中,值是一个包含name属性的对象。调用getName()函数时会将this传入privateData.get()方法作为参数获取私有信息,亦即获取value对象并且访问name属性。只要对象实例被销毁,相关信息也会被销毁,从而保证了信息的私有性
5、使用方式及使用限制
要在WeakMap集合与普通的Map集合之间做出选择时,需要考虑的主要问题是,是否只用对象作为集合的键名。如果是,那么Weak Map集合是最好的选择。当数据再也不可访问后,集合中存储的相关引用和数据都会被自动回收,这有效地避免了内存泄露的问题,从而优化了内存的使用
相对Map集合而言,WeakMap集合对用户的可见度更低,其不支持通过forEach()方法、size属性及clear()方法来管理集合中的元素。如果非常需要这些特性,那么Map集合是一个更好的选择,只是一定要留意内存的使用情况
当然,如果只想使用非对象作为键名,那么普通的Map集合是唯一的选择