[ES6深度解析]8:集合(Collections)
艰难的共同进化案例
JS不太像其他编程语言,有时这会以令人惊讶的方式影响语言的发展。ES6模块就是一个很好的例子。其他语言有模块系统。比如,Python有个非常好的模块系统。当标准委员会决定在ES6中添加模块时,他们为什么不直接复制现有的系统呢?
JS是不同的,因为它是在web浏览器中运行的。I/O可能需要很长时间。因此,JS需要一个能够支持异步加载代码的模块系统。它也不能连续地在多个目录中搜索模块。复制现有的系统是不好的。ES6模块系统需要做一些新事情。
这对最终设计的影响是一个有趣的故事。但我们不是来讨论模块的。这篇文章是关于ES6标准所谓的“键集合”: Set
, Map
, WeakSet
, WeakMap
。在大多数方面,这些特性就像其他语言中的散列表(hash table)
一样。但是标准委员会在这个过程中做了一些有趣的权衡,因为JS是不同的。
为什么需要集合(Collections)?
任何熟悉JS的人都知道,该语言中已经内置了类似哈希表的东西:Object
。毕竟,普通对象基本上就是键值对的开放式集合。可以获取get、设置set和删除delete属性,并对它们进行迭代——所有哈希表可以做的事情。那么为什么要添加新功能呢?
许多程序确实使用普通对象来存储键值对,对于这种方法运行良好的程序,没有特别的理由切换到Map
或Set
。然而,以这种方式使用对象仍然存在一些众所周知的问题:
- 作为查找表使用的对象不能拥有方法,否则会有冲突的风险。
- 因此,程序必须要么使用
Object.create(null)
(而不是普通的{}
),要么小心翼翼地避免将内置方法(如Object.prototype.toString
)误解为数据。 - 属性键(key)总是字符串(或者,在ES6中是符号)。对象不能是键key。
- 没有有效的方法来询问一个对象有多少属性。
ES6增加了一个新的问题:普通对象(类似{}
)是不可迭代的,因此它们不会与for-of
循环、...
spread运算符,等等同时使用。
同样,在许多程序中,这些都不重要,普通对象将继续是正确的选择。Map
和Set
用于其他情况。因为ES6集合的设计是为了避免用户数据和内置方法之间的冲突,所以它们不会将数据作为属性公开。这意味着像obj.key
和obj[key]
这样的表达式。不能访问哈希表数据。你必须写map.get(key)
。另外,哈希表项(entries)与属性不同,不是通过原型链继承的。它的优点是,与普通对象
不同,Map
和Set
可以拥有方法,而且可以添加更多的方法,无论是在标准对象中还是在自己的子类中,都不会产生冲突。
Set
Set
是值的集合。它是可变的,因此程序可以添加和删除值。到目前为止,这就像一个数组。但是Set
和数组之间的区别和相似之处一样多。
首先,不像数组,Set
永远不会两次包含相同的值。如果你试图向已经存在的Set
添加值,什么也不会发生。
> var desserts = new Set("🍪🍦🍧🍩");
> desserts.size
4
> desserts.add("🍪");
Set [ "🍪", "🍦", "🍧", "🍩" ]
> desserts.size
4
这个例子使用了字符串,但是Set
可以包含任何类型的JS值。就像字符串一样,多次添加相同的对象或数字不会产生重复的数据。
其次,Set
保持其数据的组织,以使一个特定的操作快速: membership testing(使用拥有某个对象)。
> // 检查 "zythum" 是不是一个单词.
> arrayOfWords.indexOf("zythum") !== -1 // slow
true
> setOfWords.has("zythum") // fast
true
再次,Set
不支持通过索引获取数据:
> arrayOfWords[15000]
"anapanapa"
> setOfWords[15000] // sets don't support indexing
undefined
下面是对Set
的所有操作:
new Set
创建一个新的空的set。new Set(iterable)
创建一个新的集合,并用来自任何可迭代值的数据填充它。set.size
获取集合中值的数量。set.has(value)
如果集合包含给定的值,则返回true。set.add(value)
向集合中添加一个值。如果值已经在集合中,则什么也不会发生。set.delete(value)
从集合中移除一个值。如果值不在集合中,则什么也不会发生。.add()
和.delete()
都返回set
对象本身,因此可以将它们链接起来。set[Symbol.iterator]()
返回对集合中值的一个新迭代器。你通常不会直接调用它,但是这个方法使set可迭代。这意味着你可以写for (v of set){…}
等等。set.forEach(f)
是最容易用代码解释的。这个方法类似于数组上的.foreach()
方法。:
for (let value of set)
f(value, value, set);
set.clear()
从集合中删除所有值。set.keys()
、set.values()
和set.entries()
返回各种迭代器。这些是为了与Map
兼容而提供的,所以将在下面讨论它们。
在所有这些特性中,构造函数new Set(iterable)
脱颖而出,因为它在整个数据结构级别上操作。可以使用它将数组转换为集合,用一行代码消除重复值。或者,给它传递一个生成器generator:它将运行生成器直至完成,并将生成的值收集到一个集合中。此构造函数也是复制现有Set
的方法。
尽管Set
很好,但仍有一些缺失的方法可以作为未来标准的补充:
- 已经在数组中出现的帮助函数,如
.map()
、.filter()
、.some()
和.every()
- 不改变原set结构的
set1.union(set2)
合并,set1.intersection(set2)
插入 - 可以同时操作多个值的方法:
set.addAll(iterable)
、set.removeAll(iterable)
和set.hasAll(iterable)
。
好消息是,所有这些都可以使用ES6提供的方法有效地实现。
Map
Map
是键值对的集合。这是Map
可以做的:
new Map
返回一个新的空Map
new Map(pairs)
创建一个新的Map
,并用现有的[key, value] pairs
中的数据填充它。pair
可以是一个现有的Map
对象、一个双元素数组数组[["key1", "value1"], ["key2", "value2"]]
、一个生成双元素数组的生成器generator等等。map.size
获取映射中的条目(entries)数map.has(key)
测试键是否存在(如key in obj
)map.get(key)
获取与键相关联的值,如果没有这样的条目则获取undefined
(如obj[key])map.set(key,value)
将此[key,value]条目添加到map中,覆盖使用相同键的任何现有条目(如obj[key] = value)map.delete(key)
删除一个entry(如delete obj[key])map.clear()
从map中删除所有条目map[Symbol.iterator]()
返回map所有entries的迭代器。迭代器将每个条目表示为一个新的[key, value]数组。map.forEach(f)
与set.forEach(f)
工作原理类似map.keys()
返回一个遍历map中所有键的迭代器map.values
返回一个遍历map中所有值的迭代器。map.entries()
返回对map中所有条目的迭代器,就像map[Symbol.iterator]()
。事实上,它只是同一个方法的另一个名称。
以下是ES6中没有的一些功能,我认为它们会很有用:
- 用于设置默认值的工具,类似Python的
collections.defaultdict
- 一个帮助函数
Map.fromObject(obj)
,使使用对象字面语法编写map变得容易。
同样,这些特性使用ES6现有功能很容易添加。
还记得开头提到的,在浏览器中运行的独特关注点如何影响JS语言特性的设计的吗?这就是我们开始讨论的地方。我有三个例子。这里是前两个。
JS是不同的,第1部分:没有哈希码的哈希表?
据我所知,ES6集合类完全不支持一个有用的特性。假设我们有一组URL对象。
var urls = new Set;
urls.add(new URL(location.href)); // two URL objects.
urls.add(new URL(location.href)); // 这两个url对象是一样的吗?
alert(urls.size); // 2
这两个url应该相等。它们都有相同的字段。但在JavaScript中,这两个对象是不同的,并且没有办法重载(overload)判断两个url是否相等的概念。
其他语言都支持这一点。在Java、Python和Ruby中,单个类可以重载判断两个对象是否相等的方法。在许多Scheme实现中,可以使用不同的相等关系创建单独的哈希表。c++支持。
然而,所有这些机制都要求用户实现自定义散列函数,并公开系统的默认散列函数。委员会选择不公开js中的哈希代码——至少目前还没有——因为互操作性和安全性的开放性问题,而这些问题在其他语言中并不紧迫。
JS是不同的,第2部分:惊喜!可预见性!
你可能会认为计算机的确定性行为不足为奇。但是,当我告诉人们Map和Set迭代访问条目entries时,他们通常会感到惊讶,因为它们是按照插入到集合中的顺序访问的。这是确定的。我们习惯了哈希表的某些方面是任意的。我们已经学会了接受它。但我们有充分的理由去避免随意性。
经过测试后发现,跟踪集合的插入顺序不会让哈希表变得太慢,这就是为什么我们在JS中使用了跟踪插入顺序的哈希表!
强烈推荐使用weak集合
我们之前的文章中讨论了一个涉及JS动画库的例子。我们想为每个DOM对象存储一个布尔标志,像这样:
if (element.isMoving) {
smoothAnimations(element);
}
element.isMoving = true;
不幸的是,像这样在DOM对象上设置expando属性并不是一个好主意,原因在最初的文章中已经讨论过。那篇文章展示了如何使用符号Symbol来解决这个问题。但是我们不能用Set来做同样的事情吗?它可能是这样的:
if (movingSet.has(element)) {
smoothAnimations(element);
}
movingSet.add(element);
只有一个缺点:Map
和Set
对象保持对它们包含的每个键和值的强引用。这意味着,如果一个DOM元素从文档中删除并被废弃,垃圾收集不能恢复该内存(指的是把被删除的DOM元素所占用的内存,恢复为可用状态,可以为下一次被内存分配并使用),直到该元素也从movingSet
中删除。在将复杂的“事后清理”要求强加给用户方面,JS库通常有成功也有失败。但是这可能常常会导致内存泄漏。
WeakMap WeakSet
WeakMap
和WeakSet
被指定为与Map
和Set
完全相同的行为,但有一些限制:
WeakMap
只支持new
、.has()
、.get()
、.set()
和.delete()
。WeakSet
只支持new
、.has()
、.add()
和.delete()
。- 存储在
WeakSet
中的值(values)和存储在WeakMap
中的键(keys)必须是对象。
注意,两种类型的弱集合都是不可迭代的。不能从弱集合中获取条目entries,除非通过特定地请求它们,传递您感兴趣的键。
这些精心设计的限制使垃圾收集器能够从活动的弱集合中收集死对象。这种效果类似于你使用弱引用(weak references)或弱键控字典(weak-keyed dictionaries)所获得的效果,但是ES6弱集合在内存管理方面获得了好处,而没有暴露GC发生在脚本上的事实。
JS是不同的,第3部分:隐藏GC不确定性
本质上,弱集合被实现为蜉蝣表(ephemeron tables)。简而言之,WeakSet
并不保留对它所包含对象的强引用。当WeakSet
中的对象被收集时,它将被简单地从WeakSet
中删除。WeakMap
是相似的。它不保留对其任何键的强引用。如果一个键是活的,则关联的值也是活的。
为什么接受这些限制?为什么不直接添加对JS的弱引用呢?
同样,标准委员会非常不愿意向JS脚本公开不确定性行为。跨浏览器兼容性差是Web开发的祸根。弱引用会公开底层垃圾收集器的实现细节,这是特定于平台的任意行为的定义。当然,应用程序不应该依赖于平台特定的细节,但是弱引用也很难知道你在当前测试的浏览器中对GC行为的依赖程度。他们很难思考。
相比之下,ES6的弱集合有一个更有限的特性集,但这个特性集是可靠的。一个键或值已经被收集的事实是不可观察的,因此应用程序不能最终依赖它,即使是偶然的。
在这种情况下,特定于web的关注导致了一个令人惊讶的设计决定,使JS成为一种更好的语言。
本文来自博客园,作者:Max力出奇迹,转载请注明原文链接:https://www.cnblogs.com/welody/p/15179941.html
如果觉得文章不错,欢迎点击推荐