6.4 Map
ECMAScript 6以前,在JavaScript中实现“键/值”式存储可以使用Object来方便高效地完成,也就是使用对象属性作为键,再使用属性来引用值。但这种实现并非没有问题,为此TC39委员会专门为“键/值”存储定义了一个规范。
作为ECMAScript 6的新增特性,Map是一种新的集合类型,为这门语言带来了真正的键/值存储机制。
Map的大多数特性都可以通过Object类型实现,但二者之间还是存在一些细微的差异。
具体实践中使用哪一个,还是值得细细甄别。
6.4.1 基本API
使用new关键字和Map构造函数可以创建一个空映射:
const m = new Map();
如果想在创建的同时初始化实例,可以给Map构造函数传入一个可迭代对象,需要包含键/值对数组。
可迭代对象中的每个键/值对都会按照迭代顺序插入到新映射实例中:
初始化之后,可以使用set()方法再添加键/值对。
使用get()和has()进行查询
通过size属性获取映射中的键/值对的数量
使用delete()和clear()删除值
set()方法返回映射实例,因此可以把多个操作连缀起来,包括初始化声明:
与Object只能使用数值、字符串或符号作为键不同,Map可以使用任何JavaScript数据类型作为键。
Map内部使用SameValueZero比较操作(ECMAScript规范内部定义,语言中不能使用),基本上相当于使用严格对象相等的标准来检查键的匹配性。
与Object类似,映射的值是没有限制的。
与严格相等一样,在映射中用作键和值的对象及其他“集合”类型,在自己的内容或属性被修改时仍然保持不变:
SameValueZero比较也可能导致意想不到的冲突:
const m = new Map();const a = 0 / '', // NaNb = 0 / '', // NaNpz = +0,nz = -0;console.log(a === b); // falseconsole.log(pz === nz); // falsem.set(a, 'foo');m.set(pz, 'bar');console.log(m.get(b)); // fooconsole.log(m.get(nz)); // bar
注:SameValueZero是ECMAScript规范新增的相等性比较算法。关于ECMAScript的相等性比较,可以参考MDN文档中的文章“Equality Comparisons and Sameness”。
6.4.2 顺序与迭代
与Object类型的一个主要差异是,Map实例会维护键值对的插入顺序,因此可以根据插入顺序执行迭代操作。
映射实例可以提供一个迭代器(Iterator),能以插入顺序生成[key, value]形式的数组。可以通过entries()方法(或者Symbol.iterator属性,它引用entries())取得这个迭代器:
因为entries()是默认迭代器,所以可以直接对映射实例使用扩展操作,把映射转换为数组:
如果不使用迭代器,而是使用回调方式,则可以调用映射的forEach(callback, opt_thisArg)方法并传入回调,依次迭代每个键/值对。
传入的回调接收可选的第二个参数,这个参数用于重写回调内部this的值:
keys()和values()分别返回以插入顺序生成键和值的迭代器:
键和值在迭代器遍历时是可以修改的,但映射内部的引用则无法修改。
当然,这并不妨碍修改作为键或值的对象内部的属性,因为这样并不影响它们在映射实例中的身份:
6.4.3 选择Object还是Map
对于多数Web开发任务来说,选择Object还是Map只是个人偏好问题,影响不大。不过,对于在乎内存和性能的开发者来说,对象和映射之间确实存在显著的差别。
1.内存占用
Object和Map的工程级实现在不同浏览器间存在明显差异,但存储单个键/值对所占用的内存数量都会随键的数量线性增加。批量添加或删除键/值对则取决于各浏览器对该类型内存分配的工程实现。不同浏览器的情况不同,但给定固定大小的内存,Map大约可以比Object多存储50%的键/值对。
2.插入性能
向Object和Map中插入新键/值对的消耗大致相当,不过插入Map在所有浏览器中一般会稍微快一点儿。对这两个类型来说,插入速度并不会随着键/值对数量而线性增加。如果代码涉及大量插入操作,那么显然Map的性能更佳。3.查找速度
与插入不同,从大型Object和Map中查找键/值对的性能差异极小,但如果只包含少量键/值对,则Object有时候速度更快。在把Object当成数组使用的情况下(比如使用连续整数作为属性),浏览器引擎可以进行优化,在内存中使用更高效的布局。这对Map来说是不可能的。对这两个类型而言,查找速度不会随着键/值对数量增加而线性增加。如果代码涉及大量查找操作,那么某些情况下可能选择Object更好一些。
4.删除性能
使用delete删除Object属性的性能一直以来饱受诟病,目前在很多浏览器中仍然如此。为此,出现了一些伪删除对象属性的操作,包括把属性值设置为undefined或null。但很多时候,这都是一种讨厌的或不适宜的折中。而对大多数浏览器引擎来说,Map的delete()操作都比插入和查找更快。如果代码涉及大量删除操作,那么毫无疑问应该选择Map。
6.5 WeakMap
ECMAScript 6新增的“弱映射”(WeakMap)是一种新的集合类型,为这门语言带来了增强的键/值对存储机制。WeakMap是Map的“兄弟”类型,其API也是Map的子集。
WeakMap中的“weak”(弱),描述的是JavaScript垃圾回收程序对待“弱映射”中键的方式。
6.5.1 基本API
可以使用new关键字实例化一个空的WeakMap:
const wm = new WeakMap();
弱映射中的键只能是Object或者继承自Object的类型,尝试使用非对象设置键会抛出TypeError。
值的类型没有限制。
如果想在初始化时填充弱映射,则构造函数可以接收一个可迭代对象,其中需要包含键/值对数组。
可迭代对象中的每个键/值都会按照迭代顺序插入新实例中:
初始化之后可以使用set()再添加键/值对,可以使用get()和has()查询,还可以使用delete()删除:
set()方法返回弱映射实例,因此可以把多个操作连缀起来,包括初始化声明:
6.5.2 弱键
WeakMap中“weak”表示弱映射的键是“弱弱地拿着”的。意思就是,这些键不属于正式的引用,不会阻止垃圾回收。
但要注意的是,弱映射中值的引用可不是“弱弱地拿着”的。
只要键存在,键/值对就会存在于映射中,并被当作对值的引用,因此就不会被当作垃圾回收。
来看下面的例子:
const wm = new WeakMap();wm.set({}, 'val');
set()方法初始化了一个新对象并将它用作一个字符串的键。
因为没有指向这个对象的其他引用,所以当这行代码执行完成后,这个对象键就会被当作垃圾回收。
然后,这个键/值对就从弱映射中消失了,使其成为一个空映射。
在这个例子中,因为值也没有被引用,所以这对键/值被破坏以后,值本身也会成为垃圾回收的目标。
再看一个稍微不同的例子:
const wm = new WeakMap();const container = {key: {}};wm.set(container.key, 'val');function removeReference() {container.key = null;}
这一次,container对象维护着一个对弱映射键的引用,因此这个对象键不会成为垃圾回收的目标。
不过,如果调用了removeReference(),就会摧毁键对象的最后一个引用,垃圾回收程序就可以把这个键/值对清理掉。
6.5.3 不可迭代键
因为WeakMap中的键/值对任何时候都可能被销毁,所以没必要提供迭代其键/值对的能力。
当然,也用不着像clear()这样一次性销毁所有键/值的方法。WeakMap确实没有这个方法。
因为不可能迭代,所以也不可能在不知道对象引用的情况下从弱映射中取得值。
即便代码可以访问WeakMap实例,也没办法看到其中的内容。
WeakMap实例之所以限制只能用对象作为键,是为了保证只有通过键对象的引用才能取得值。
如果允许原始值,那就没办法区分初始化时使用的字符串字面量和初始化之后使用的一个相等的字符串了。
6.5.4 使用弱映射
WeakMap实例与现有JavaScript对象有着很大不同,可能一时不容易说清楚应该怎么使用它。这个问题没有唯一的答案,但已经出现了很多相关策略。
1.私有变量
弱映射造就了在JavaScript中实现真正私有变量的一种新方式。前提很明确:私有变量会存储在弱映射中,以对象实例为键,以私有成员的字典为值。
下面是一个示例实现:
对于上面的实现,外部代码只需要拿到对象实例的引用和弱映射,就可以取得“私有”变量了。为了避免这种访问,可以用一个闭包把WeakMap包装起来,这样就可以把弱映射与外界完全隔离开了:
这样,拿不到弱映射中的健,也就无法取得弱映射中对应的值。虽然这防止了前面提到的访问,但整个代码也完全陷入了ES6之前的闭包私有变量模式。
2.DOM节点元数据
因为WeakMap实例不会妨碍垃圾回收,所以非常适合保存关联元数据。
来看下面这个例子,其中使用了常规的Map:
假设在上面的代码执行后,页面被JavaScript改变了,原来的登录按钮从DOM树中被删掉了。但由于映射中还保存着按钮的引用,所以对应的DOM节点仍然会逗留在内存中,除非明确将其从映射中删除或者等到映射本身被销毁。
如果这里使用的是弱映射,如以下代码所示,那么当节点从DOM树中被删除后,垃圾回收程序就可以立即释放其内存(假设没有其他地方引用这个对象):
思考:
- WeakMap与Map的区别?
① Map可以使用任何JavaScript数据类型作为键
WeakMap弱映射中的键只能是Object或者继承自Object的类型,尝试使用非对象设置键会抛出TypeError。
② Map的方法:set(),get(),has(),delete(),clear()
WeakMap的方法:set(),get(),has(),delete()
③ WeakMap:每个键对自己所引用对象的引用都是弱引用,没有指向这个对象的其他引用,当这行代码执行完成后,这个对象键就会被当作垃圾回收,因此在WeakMap不可迭代键
- WeakMap的作用是什么?
① 实现真正私有变量(前提:私有变量会存储在弱映射中,以对象实例为键,以私有成员的字典为值。)
② 因为WeakMap实例不会妨碍垃圾回收,所以非常适合保存关联元数据,可以用来保存DOM节点。
