深入 ES6 是一个系列,介绍了在 ECMAScript 标准的第 6 版(简称 ES6)中添加到 JavaScript 编程语言的新功能。
本周早些时候,ES6 规范(正式名称为 _ECMA-262, 第 6 版, ECMAScript 2015 语言规范_)通过了最后的障碍,被批准为 Ecma 标准。祝贺 TC39 和所有做出贡献的人。ES6 已完成!
更棒的消息是:下一次更新不会再等六年。标准委员会现在计划大约每 12 个月发布一个新版本。 第 7 版的提案 已经开始开发。
因此,在这个场合谈论我一直渴望在 JS 中看到的东西,并且我认为它还有改进的空间,再合适不过了!
共同进化的难题
JS 与其他编程语言不太一样,有时这会以出人意料的方式影响语言的演变。
ES6 模块就是一个很好的例子。其他语言有模块系统。Racket 有一个很棒的模块系统。Python 也有。当标准委员会决定在 ES6 中添加模块时,为什么他们没有直接复制现有的系统呢?
JS 与众不同,因为它在 Web 浏览器中运行。I/O 可能需要很长时间。因此,JS 需要一个能够支持异步加载代码的模块系统。它也不能负担得起在多个目录中按顺序搜索模块。复制现有的系统并不好。ES6 模块系统需要做一些新的事情。
这如何影响最终的设计是一个有趣的故事。但我们今天不是来谈论模块的。
这篇文章是关于 ES6 标准称之为“键值集合”的内容:Set
、Map
、WeakSet
和 WeakMap
。在大多数方面,这些功能就像其他语言中的 哈希表。但标准委员会在此过程中做了一些有趣的权衡,因为 JS 与众不同。
为什么需要集合?
任何熟悉 JS 的人都知道,语言中已经内置了一些类似哈希表的东西:对象。
毕竟,一个普通的 Object
几乎就是一个开放式的键值对集合。您可以获取、设置和删除属性,迭代它们——所有哈希表可以做的事情。那么为什么要添加一个新功能呢?
嗯,许多程序确实使用普通对象来存储键值对,对于这些程序来说,如果这能很好地工作,就没有特别的理由切换到 Map
或 Set
。不过,使用这种方式的对象有一些众所周知的问题。
-
用作查找表的对象不能同时具有方法,除非存在一些冲突风险。
-
因此,程序必须使用
Object.create(null)
(而不是普通的{}
),或者小心避免将内置方法(如Object.prototype.toString
)误解为数据。 -
属性键始终是字符串(或者,在 ES6 中,是符号)。对象不能作为键。
-
没有有效的方法来询问对象有多少个属性。
ES6 添加了一个新的问题:普通对象不是 可迭代的,因此它们不会与 for
-of
循环、...
运算符等合作。
再说一次,有很多程序并不关心这些问题,而普通对象将继续是正确选择。Map
和 Set
适用于其他情况。
由于它们旨在避免用户数据与内置方法之间的冲突,因此 ES6 集合 _不会_ 将它们的数据暴露为属性。这意味着表达式如 obj.key
或 obj[key]
不能用于访问哈希表数据。您必须编写 map.get(key)
。此外,与属性不同,哈希表条目 _不会_ 通过原型链继承。
优点是,与普通的 Object
不同,Map
和 Set
确实有方法,并且可以添加更多方法,无论是在标准中还是在您自己的子类中,都不会发生冲突。
Set
Set
是一个值的集合。它是可变的,因此您的程序可以在运行时添加和删除值。到目前为止,这与数组非常相似。但是,集合和数组之间的差异和相似之处一样多。
首先,与数组不同,集合永远不会包含相同的值两次。如果您尝试向集合中添加一个已经存在的 value,则不会发生任何事情。
<pre>
> var desserts = new Set("🍪🍦🍧🍩");
> desserts.size
4
> desserts.add("🍪");
Set [ "🍪", "🍦", "🍧", "🍩" ]
> desserts.size
4
</pre>
此示例使用字符串,但 Set
可以包含任何类型的 JS 值。与字符串一样,多次添加相同对象或数字不会有任何额外影响。
其次,Set
会保持数据的组织,以使一项特定操作速度更快:成员资格测试。
<pre>
> // 检查 “zythum” 是否是一个单词。
> arrayOfWords.indexOf("zythum") !== -1 // 慢
true
> setOfWords.has("zythum") // 快
true
</pre>
您在 Set
中无法获得的是索引
<pre>
> arrayOfWords[15000]
"anapanapa"
> setOfWords[15000] // 集合不支持索引
undefined
</pre>
以下是集合的所有操作
-
new Set
创建一个新的空集合。 -
new Set(iterable)
创建一个新的集合,并用 任何可迭代值 中的数据填充它。 -
set.size
获取集合中的值数量。 -
set.has(value)
如果集合包含给定值,则返回true
。 -
set.add(value)
向集合中添加一个值。如果该值已经存在于集合中,则不会发生任何事情。 -
set.delete(value)
从集合中删除一个值。如果该值不存在于集合中,则不会发生任何事情。.add()
和.delete()
都返回集合对象本身,因此您可以将它们链接起来。 -
set[Symbol.iterator]()
返回一个集合中值的新的迭代器。您通常不会直接调用它,但此方法是使集合可迭代的原因。这意味着您可以编写for (v of set) {...}
等等。 -
set.forEach(f)
最容易用代码解释。它就像以下代码的简写<pre>
for (let value of set)
f(value, value, set);
</pre>此方法类似于数组上的
.forEach()
方法。 -
set.clear()
从集合中删除所有值。 -
set.keys()
、set.values()
和set.entries()
返回不同的迭代器。这些是为与Map
保持兼容性而提供的,因此我们将在下面讨论它们。
在所有这些功能中,构造函数 new Set(iterable)
最为突出,因为它是在整个数据结构级别上运行的。您可以使用它将数组转换为集合,使用一行代码消除重复值。或者,将一个生成器传递给它:它将运行生成器直到完成,并将生成的 value 收集到一个集合中。此构造函数也是您复制现有 Set
的方式。
我上周承诺要抱怨 ES6 中的新的集合。我将从这里开始。Set
虽然很好,但缺少一些方法,这些方法将成为未来标准的很好的补充
-
已经存在于数组上的函数助手,例如
.map()
、.filter()
、.some()
和.every()
。 -
非变异
set1.union(set2)
和set1.intersection(set2)
。 -
可以同时对多个值执行操作的方法:
set.addAll(iterable)
、set.removeAll(iterable)
和set.hasAll(iterable)
。
好消息是,所有这些都可以使用 ES6 提供的方法有效地实现。
Map
Map
是一个键值对的集合。以下是 Map
可以做的事情
-
new Map
返回一个新的空映射。 -
new Map(pairs)
创建一个新的映射,并用来自现有[key, value]
对集合的数据填充它。_pairs_ 可以是现有的Map
对象、一个包含两个元素的数组、生成两个元素的数组的生成器等。 -
map.size
获取映射中的条目数量。 -
map.has(key)
测试一个键是否存在(就像key in obj
一样)。 -
map.get(key)
获取与一个键关联的值,如果不存在这样的条目,则返回 undefined(就像obj[key]
一样)。 -
map.set(key, value)
向映射中添加一个条目,将 _key_ 与 _value_ 关联,覆盖任何具有相同键的现有条目(就像obj[key] = value
一样)。 -
map.delete(key)
删除一个条目(就像delete obj[key]
一样)。 -
map.clear()
从映射中删除所有条目。 -
map[Symbol.iterator]()
返回一个映射中条目的迭代器。迭代器将每个条目表示为一个新的[key, value]
数组。 -
map.forEach(f)
的工作方式如下<pre>
for (let [key, value] of map)
f(value, key, map);
</pre>奇怪的参数顺序同样是为了与
Array.prototype.forEach()
保持一致。 -
map.keys()
返回一个映射中所有键的迭代器。 -
map.values()
返回一个映射中所有值的迭代器。 -
map.entries()
返回一个映射中所有条目的迭代器,就像map[Symbol.iterator]()
一样。实际上,它只是同一个方法的另一个名称。
有什么可抱怨的呢?以下是一些 ES6 中不存在但我认为有用的功能
-
用于默认值的工具,例如 Python 的
collections.defaultdict
。 -
一个辅助函数
Map.fromObject(obj)
,可以轻松地使用对象字面量语法编写映射。
再说一次,这些功能很容易添加。
好的。还记得我是如何用一些关于在浏览器中运行的独特问题如何影响 JS 语言特性设计的话来开始这篇文章的吗?这就是我们开始谈论这些问题的地方。我这里有三个例子。以下的前两个。
JS 与众不同,第一部分:没有哈希码的哈希表?
据我所知,ES6 集合类完全不支持一个有用的功能。
假设我们有一个包含 URL
对象的 Set
。
<pre>
var urls = new Set;
urls.add(new URL(location.href)); // 两个 URL 对象。
urls.add(new URL(location.href)); // 它们相同吗?
alert(urls.size); // 2
</pre>
这两个 URL
实际上应该被认为是相等的。它们具有所有相同的字段。但是在 JavaScript 中,这两个对象是不同的,并且无法重载语言的等式概念。
其他语言支持这一点。在 Java、Python 和 Ruby 中,各个类可以重载等式。在许多 Scheme 实现中,可以创建使用不同等式关系的单个哈希表。C++ 支持两者。
但是,所有这些机制都需要用户实现自定义哈希函数,并且所有机制都公开系统的默认哈希函数。委员会选择不在 JS 中公开哈希码,至少现在还没有,因为有关互操作性和安全性的问题尚待解决,这些问题在其他语言中并不那么紧迫。
JS 与众不同,第二部分:惊喜!可预测性!
你可能会认为计算机的确定性行为不可能令人感到惊讶。但当我说 Map
和 Set
迭代按将条目插入集合的顺序访问条目时,人们经常会感到惊讶。它是确定的。
我们习惯了哈希表的某些方面是任意的。我们已经学会接受它。但是,有充分的理由尝试避免随意性。正如我在 2012 年所写
当所有这一切在 2012 年 2 月被讨论时,我争论支持任意的迭代顺序。然后,我着手通过实验表明,跟踪插入顺序会使哈希表速度过慢。我编写了一些 C++ 微基准测试。 结果让我吃惊。
这就是我们在 JS 中最终使用跟踪插入顺序的哈希表的原因!
使用弱集合的强有力理由
上周,我们讨论了一个涉及 JS 动画库的示例。我们希望为每个 DOM 对象存储一个布尔标志,如下所示
<pre>
if (element.isMoving) {
smoothAnimations(element);
}
element.isMoving = true;
</pre>
不幸的是,在 DOM 对象上设置扩展属性(如上所述)是个坏主意,因为原始文章中讨论了原因。
那篇文章展示了如何使用符号解决这个问题。但我们不能使用 Set
做同样的事情吗?它可能看起来像这样
<pre>
if (movingSet.has(element)) {
smoothAnimations(element);
}
movingSet.add(element);
</pre>
只有一个缺点:Map
和 Set
对象对它们包含的每个键和值都保持强引用。这意味着,如果 DOM 元素从文档中删除并丢弃,垃圾回收无法回收该内存,直到该元素也从 movingSet
中删除为止。库通常在对用户施加复杂的自我清理要求方面取得了参差不齐的成功,充其量如此。因此,这可能会导致内存泄漏。
ES6 为此提供了一个令人惊讶的解决方法。将 movingSet
设为 WeakSet
而不是 Set
。内存泄漏已解决!
这意味着可以使用弱集合或符号来解决这个特定问题。哪个更好?不幸的是,对权衡的全面讨论会使这篇文章过长。如果可以在网页的整个生命周期内使用单个符号,那么这可能没问题。如果你最终想要许多短暂的符号,这是一个危险的信号:考虑使用 WeakMap
而不是使用符号来避免内存泄漏。
WeakMap
和 WeakSet
WeakMap
和 WeakSet
被指定为行为与 Map
和 Set
完全相同,但有一些限制
-
WeakMap
只支持new
、.has()
、.get()
、.set()
和.delete()
。 -
WeakSet
只支持new
、.has()
、.add()
和.delete()
。 -
存储在
WeakSet
中的值以及存储在WeakMap
中的键必须是对象。
请注意,两种类型的弱集合都不能迭代。你无法从弱集合中获取条目,除非通过专门请求它们,传入你感兴趣的键。
这些精心设计的限制使垃圾回收器能够从活动的弱集合中收集死对象。效果类似于使用弱引用或弱键字典可以获得的效果,但 ES6 弱集合在不向脚本公开 GC 发生的事实的情况下获得了内存管理优势。
JS 与众不同,第三部分:隐藏 GC 非确定性
在幕后,弱集合被实现为短暂表.
简而言之,WeakSet
不会对它包含的对象保持强引用。当 WeakSet
中的对象被回收时,它将简单地从 WeakSet
中删除。WeakMap
类似。它不会对任何键保持强引用。如果键还活着,关联的值也还活着。
为什么要接受这些限制?为什么不直接将弱引用添加到 JS 中?
同样,标准委员会一直非常不愿意向脚本公开非确定性行为。跨浏览器兼容性差是 Web 开发的 bane。弱引用公开了底层垃圾回收器的实现细节,这是特定于平台的任意行为的定义。当然,应用程序不应依赖特定于平台的细节,但弱引用也使得很难知道你究竟在多大程度上依赖于你当前正在测试的浏览器的 GC 行为。它们难以推理。
相比之下,ES6 弱集合的功能集更加有限,但该功能集非常可靠。键或值已被回收的事实永远不会被观察到,因此应用程序不会意外地依赖它。
这是一种情况,其中特定于 Web 的问题导致了一个令人惊讶的设计决策,使 JS 成为一种更好的语言。
我什么时候可以在代码中使用集合?
所有四个集合类目前都在 Firefox、Chrome、Microsoft Edge 和 Safari 中发布。要支持旧版浏览器,请使用 polyfill,例如 es6-collections.
WeakMap
最初由 Andreas Gal 在 Firefox 中实现,他后来担任了 Mozilla 的 CTO 一职。Tom Schuster 实现了 WeakSet
。我实现了 Map
和 Set
。感谢 Tooru Fujisawa 在这个领域贡献了多个补丁。
下周,ES6 深入探究将开始为期两周的夏季休假。本系列已经涵盖了许多内容,但 ES6 中一些最强大的功能尚未发布。因此,请在我们于 7 月 9 日发布新内容时加入我们。
11 条评论