0%

JS高程第4版新增章节翻译:类型化数组、Map和Set

今天在知乎看到了一个回答说李松峰老师已经神速的把 1000 多页翻译完了,有三名编辑负责交接。太振奋了,英文版实在看的头有点疼,自己翻译自己看的日子终于快到头了,一定要买到首发第一批!

在 Map 和 Set 章节发现了多处错误,已更正。

下述内容翻译自 Professional JavaScript for Web Developes,4th Edition(JavaScript 高级程序设计第四版),190 页,COLLECTION REFERENCE TYPES 章节内,前半部分是普通 Array 的介绍,已经比较熟悉,主要翻译后半部分——类型化数组、Map 和 Set。

类型化数组

在 ECMAScript6 中,类型化数组(TYPED ARRAYS)是一种设计用于向本地库高效传递二进制数据的结构。JavaScript 中并没有真正的“TypedArray”类型,相反,这个术语指的是一组包含数字类型的特殊数组。想要理解如何使用类型化数组,首先需要理解它的用途。

历史

随着 web 浏览器的普及,可以预见在浏览器内部运行复杂的 3D 应用的能力将会大受欢迎。早在 2006 年,包括 Mozilla 和 Opera 在内的浏览器厂商就开始试验一种编程平台,用于在浏览器内部无需插件就可渲染图形密集型应用。目标是开发一个 JavaScript API,可以利用 3D 图形 API 和 GPU 加速来实现在<canvas>元素上渲染复杂图形。

WebGL

最终实现的 JavaScript API 是基于 OpenGL for Embedded Systems(OpenGL ES)2.0 规范,该规范是 OpenGL 的一个子集,专门用于实现 2D 和 3D 计算机图形。这个新的 API 被命名为 Web Graphics Library(WebGL),在 2011 年 3 月发布了它的 1.0 版本。有了该 API,开发者就能够编写图形密集型的应用程序代码,这些代码可以被任何兼容 WebGL 的 web 浏览器原生地编译。

在 WebGL 的初始版本中,JavaScript 数组和原生数组之间的不匹配导致出现了性能问题。图形驱动的 API 通常不希望传递的数组内容是 JavaScript 默认的双浮点格式。图形驱动的 API 期望以二进制格式的数字数组格式传递,这当然与内存中的 JavaScript 数组格式不同。因此,每次在 WebGL 和 JavaScript 运行时之间传递一个数组时,WebGL 都会执行一个浪费性能的操作,即在目标环境中分配一个新数组,把当前数组的数字转换为适当的格式迭代给新数组。

类型化数组的出现

当然,上述转换操作是不合适的,Mozilla 通过实现 CanvasFloatArray 解决了这个问题,这是一个 C 语言风格的浮点数数组,提供了一个 JavaScript 接口。这种类型允许 JavaScript 运行时分配、读取和写入一个数组,该数组可以直接传递给图形驱动程序的 API。Canvasfloatarray 最终将被重塑为 Float32Array,这是目前类型化数组可用的第一个“类型”。

使用 ArrayBuffers

Float32array 实际上是一种“视图(view)”类型,该类型允许 JavaScript 运行时访问一块分配好的内存,称为 ArrayBuffer。Arraybuffer 是所有类型化数组和视图引用的基本单元。

Typedarraybuffer 是 ArrayBuffer 类型的一个变体,该变体可以在执行上下文环境之间传递而不生成副本。有关该类型的内容,请参阅“Workers”一章。

Arraybuffer 是一个普通的 JavaScript 构造函数,可用于在内存中分配特定数量的字节。

1
2
const buf = new ArrayBuffer(16); // 分配16字节的内存
alert(buf.byteLength); // 16

数组缓冲区(Arraybuffer)一旦创建就不能调整大小。但是可以使用 slice()将现有 ArrayBuffer 的全部或部分复制到新实例中:

1
2
3
const buf1 = new ArrayBuffer(16);
const buf2 = buf1.slice(4, 12);
alert(buf2.byteLength); // 8

Arraybuffer 在某些方面类似于 c++malloc(),但有几个不同的地方值得注意:

  • 当 malloc()分配失败时,它返回一个空指针,如果 ArrayBuffer 分配失败,它抛出一个 error。
  • malloc()调用可以使用虚拟内存,因此最大的内存分配只受可寻址的系统内存大小限制。Arraybuffer 的内存分配不能超过 Number.MAXSAFEINTEGER(2^53)字节。
  • 成功的 malloc()调用不初始化实际地址。声明一个 ArrayBuffer 将所有位初始化为 0。
  • 在调用 free()或程序退出之前,系统不能使用 malloc()分配的堆内存。通过声明一个 ArrayBuffer 分配的堆内存仍然是可垃圾回收的ー不需要进行手动内存管理。

不能通过引用缓冲区(buffer)实例来读取或写入 ArrayBuffer 的内容。若要在内部读取或写入数据,必须使用视图(view)。有多种不同类型的视图,但它们都引用存储在 ArrayBuffer 中的二进制数据。

DataView

允许读写 ArrayBuffer 的第一种视图类型是 DataView。这个视图是为文件和网络 I/O 设计的;该 API 允许高度自由控制 buffer 数据的操作,但是与其他不同的视图类型相比,该类型的性能比较低。Dataview 不预设任何关于缓冲区的内容,也不可迭代。

读取和写入已经存在的 ArrayBuffer 时必须创建一个 DataView 实例。它可以使用整个或部分 buffer,并维护对 buffer 实例的引用,以及设定视图在 buffer 中的开始位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const buf = new ArrayBuffer(16);
// DataView默认使用全部的ArrayBuffer
const fullDataView = new DataView(buf);
alert(fullDataView.byteOffset); // 0
alert(fullDataView.byteLength); // 16
alert(fullDataView.buffer === buf); // true
// 构造函数中有两个可选参数:字节偏移和字节长度
// byteOffset=0 视图从buffer的起始位置开始
// byteLength=8 限制视图为前8个字节
const firstHalfDataView = new DataView(buf, 0, 8);
alert(firstHalfDataView.byteOffset); // 0
alert(firstHalfDataView.byteLength); // 8
alert(firstHalfDataView.buffer === buf); // true
// 如果不指定长度的话,DataView将会使用buffer的剩余部分
// byteOffset=8 视图从buffer的第9位开始
// byteLength默认时剩余buffer的长度
const secondHalfDataView = new DataView(buf, 8);
alert(secondHalfDataView.byteOffset); // 8
alert(secondHalfDataView.byteLength); // 8
alert(secondHalfDataView.buffer === buf); // true

要通过 DataView 读写缓冲区,你需要使用以下几点内容:

  • 要读或写位置的字节偏移量。这可以看作是 DataView 中的一种“地址”。
  • DataView 应使用 ElementType 在 JavaScript 运行时中的 Number 类型和缓冲区中的二进制格式之间进行转换。
  • 内存中值的端序(endianness)。默认为 大端(big-endian)。
ElementType

Dataview 对缓冲区中存储的数据类型不做任何假设。API 在读写时强制指定 ElementType,然后 DataView 将全面转换以执行读写操作。

ECMAScript 6 支持 8 种不同的 ElementType:

ELEMENTTYPE 字节数 描述 C 语言对照 值范围
Int8 1 8 位有符号整数 signed char –128 到 127
Uint8 1 8 位无符号整数 unsigned char 0 到 255
Int16 2 16 位有符号整数 short –32768 到 32767
Uint16 2 16 位无符号整数 unsigned short 0 到 65535
Int32 4 32 位有符号整数 int –2,147,483,648 到 2,147,483,647
Uint32 4 32 位无符号整数 unsigned int 0 到 4,294,967,295
Float32 4 32 位 IEEE-754 浮点数 float –3.4E+38 到 +3.4E+38
Float64 8 64 位 IEEE-754 浮点数 double –1.7E+308 到 +1.7E+308

Dataview 公开了每种类型的 get 和 set 方法,这些方法使用 byteOffset 在 buffer 中寻址以读取和写入值。类型是可以互换使用的,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 分配2字节的内存并声明一个DataView实例
const buf = new ArrayBuffer(2);
const view = new DataView(buf);
// 证明整个buffer实际所有位都是0
// 检查第一个和第二个字节
alert(view.getInt8(0)); // 0
alert(view.getInt8(1)); // 0
// 检查整个buffer
alert(view.getInt16(0)); // 0
// 把整个buffer设成一个字节
// 255按位表示是11111111 (2^8 – 1)
view.setUint8(0, 255);
// DataView将会自动的把值转换为指定的ElementType类型
// 255在16进制中表示为0xFF
view.setUint8(1, 0xff);
// 现在是个两个字节的带符号整数,结果为-1
alert(view.getInt16(0)); // -1
大端和小端

在前一个示例中,buffer 的字节故意全都设为相同的,以避免端序问题。“端序(Endianness)”是指由计算系统维护的字节排序约定。对于 DataViews 只支持两个约定:大端(big-endian)和小端(little-endian)。大端(也称为“网络字节顺序”)意味着最高有效字节位于第一个字节,最低有效字节位于最后一个字节。Little-endian 意味着最低有效字节保存在第一个字节中,最高有效字节保存在最后一个字节中。

执行 JavaScript 运行时系统的原生端序将约定如何读取和写入字节,但 DataView 不遵守这个约定。Dataview 是一个无偏见的内存片段接口,遵循指定的任何端序。所有 DataView API 方法都默认大端,但接受一个可选的布尔参数,该参数允许通过将 little-endian 设置为 true 来启用小端模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 分配2字节的内存并声明一个DataView实例
const buf = new ArrayBuffer(2);
const view = new DataView(buf);
// 把buffer的最左位和最右位设为1
view.setUint8(0, 0x80); // 设置最左位为1
view.setUint8(1, 0x01); // 设置最右位为1
// buffer内容(为了可读性而分隔开):
// 0x8 0x0 0x0 0x1
// 1000 0000 0000 0001
// 按大端读取16位无符号整数
// 0x80是最高有效字节,0x01是最低有效字节
// 0x8001 = 2^15 + 2^0 = 32768 + 1 = 32769
alert(view.getUint16(0)); // 32769
// 按小端读取16位无符号整数
// 0x01是最高有效字节,0x80是最低有效字节
// 0x0180 = 2^8 + 2^7 = 256 + 128 = 384
alert(view.getUint16(0, true)); // 384
// 按大端写入16位无符号整数
view.setUint16(0, 0x0004);
// buffer内容(为了可读性而分隔开):
// 0x0 0x0 0x0 0x4
// 0000 0000 0000 0100
alert(view.getUint8(0)); // 0
alert(view.getUint8(1)); // 4
// 按小端写入16位无符号整数
view.setUint16(0, 0x0002, true);
// buffer内容(为了可读性而分隔开):
// 0x0 0x2 0x0 0x0
// 0000 0010 0000 0000
alert(view.getUint8(0)); // 2
alert(view.getUint8(1)); // 0
极端案例

只有在有足够的 buffer 空间情况下,DataView 才能完成读写操作;否则会抛出一个 RangeError:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const buf = new ArrayBuffer(6);
const view = new DataView(buf);
// 部分越过了buffer后界时尝试读取值
view.getInt32(4);
// RangeError
// 越过了buffer后界时尝试读取值
view.getInt32(8);
// RangeError
// 越过了buffer后界时尝试读取值
view.getInt32(-1);
// RangeError
// 越过了buffer后界时尝试写入值
view.setInt32(4, 123);
// RangeError

当写入 buffer 时,DataView 将尽力将值强制转换为适当的类型或回退到 0。如果不能将抛出一个 error:

1
2
3
4
5
6
7
8
9
10
const buf = new ArrayBuffer(1);
const view = new DataView(buf);
view.setInt8(0, 1.5);
alert(view.getInt8(0)); // 1
view.setInt8(0, [4]);
alert(view.getInt8(0)); // 4
view.setInt8(0, 'f');
alert(view.getInt8(0)); // 0
view.setInt8(0, Symbol());
// TypeError

类型化数组

类型化数组(Typed arrays)是 ArrayBuffer 视图的另一种形式。虽然在概念上类似于数据视图(Data View),但类型化数组的不同之处在于强制使用单个 ElementType 类型 并服从系统的原生端序。优点是提供了更广泛的 API 和更好的性能。类型化数组的设计是为了高效地与 WebGL 这样的本地库交换二进制数据。因为类型化数组的二进制表示对于本地操作系统来说是一种易于理解的格式,所以 JavaScript 引擎能够对类型化数组的算术运算、位运算和其他常见操作进行大幅度优化,因此它们的使用速度非常快。

类型化数组可以使用多种方式创建:从现有的 buffer 读取、用自己的 buffer 初始化、用迭代器填充或从任何类型的现有类型化数组填充。它们也可以使用<ElementType>.from()<ElementType>.of():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 创建一个12字节的buffer
const buf = new ArrayBuffer(12);
// 引用buffer创建一个Int32Array类型化数组
const ints = new Int32Array(buf);
// 类型化数组能识别出每个元素需要4个字节,所以长度是3
alert(ints.length); // 3
// 创建一个长度为6的Int32Array类型化数组
const ints2 = new Int32Array(6);
// 每一个数字使用了4个字节,所以ArrayBuffer有24个字节
alert(ints2.length); // 6
// 与DataView类似,类型化数组有一个相关buffer的引用
alert(ints2.buffer.byteLength); // 24
// 创建一个包含[2, 4, 6, 8]的Int32Array类型化数组
const ints3 = new Int32Array([2, 4, 6, 8]);
alert(ints3.length); // 4
alert(ints3.buffer.byteLength); // 16
alert(ints3[2]); // 6
// 拷贝ints3创建一个Int16Array类型的类型化数组
const ints4 = new Int16Array(ints3);
// 新的类型化数组分配他自己的内存,相同下标处的值会转化为新的表达。
alert(ints4.length); // 4
alert(ints4.buffer.byteLength); // 8
alert(ints4[2]); // 6
// 从一个普通数组创建Int16Array类型化数组
const ints5 = Int16Array.from([3, 5, 7, 9]);
alert(ints5.length); // 4
alert(ints5.buffer.byteLength); // 8
alert(ints5[2]); // 7
// 用参数创建Float32Array类型化数组
const floats = Float32Array.of(3.14, 2.718, 1.618);
alert(floats.length); // 3
alert(floats.buffer.byteLength); // 12
alert(floats[2]); // 1.6180000305175781

构造函数和实例都有一个 BYTES_PER_ELEMENT 属性,该属性返回该类型数组中每个元素的字节数:

1
2
3
4
5
6
alert(Int16Array.BYTES_PER_ELEMENT); // 2
alert(Int32Array.BYTES_PER_ELEMENT); // 4
const ints = new Int32Array(1),
floats = new Float64Array(1);
alert(ints.BYTES_PER_ELEMENT); // 4
alert(floats.BYTES_PER_ELEMENT); // 8

除非使用具体值来初始化类型化数组,否则其关联的 buffer 由 0 填充:

1
2
3
4
5
const ints = new Int32Array(4);
alert(ints[0]); // 0
alert(ints[1]); // 0
alert(ints[2]); // 0
alert(ints[3]); // 0
类型化数组行为

在大多数情况下,类型化数组的行为与常规数组的行为类似。类型化数组支持以下操作符、方法和属性:

  • []
  • copyWithin()
  • entries()
  • every()
  • fill()
  • filter()
  • find()
  • findIndex()
  • forEach()
  • indexOf()
  • join()
  • keys()
  • lastIndexOf()
  • length
  • map()
  • reduce()
  • reduceRight()
  • reverse()
  • copyWithin()
  • entries()
  • every()
  • fill()
  • filter()
  • find()
  • findIndex()
  • forEach()
  • indexOf()
  • join()
  • keys()
  • lastIndexOf()
  • length
  • map()
  • reduce()
  • reduceRight()
  • reverse()

返回新数组的方法使用时将返回具有相同元素类型的类型化数组:

1
2
3
const ints = new Int16Array([1, 2, 3]);
const doubleints = ints.map((x) => 2 * x);
alert(doubleints instanceof Int16Array); // true

类型化数组定义了一个 Symbol.iterator,意味着也可以使用..of 循环和扩展操作符:

1
2
3
4
5
6
7
8
const ints = new Int16Array([1, 2, 3]);
for (const int of ints) {
alert(int);
}
// 1
// 2
// 3
alert(Math.max(...ints)); // 3
合并、复制和更改类型化数组

类型化的数组仍然使用 array buffer 作为存储空间,而且 buffer 不能调整大小。因此,类型化数组不支持以下方法:

  • concat()
  • pop()
  • push()
  • shift()
  • splice()
  • unshift()

但是,类型化数组确实提供了两种新的方法:set()和 subarray(),可以快速地从数组中复制值。

Set()将提供的数组或类型化数组中的值复制到当前类型化数组中的指定索引处:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 创建一个长度为8的Int16Array类型化数组
const container = new Int16Array(8);
// 复制一个类型化数组到前四个值
// Offset偏移默认是0
container.set(Int8Array.of(1, 2, 3, 4));
alert(container); // [1,2,3,4,0,0,0,0]
// 复制一个普通数组到后四个值
// Offset为4代表着从第四个索引处插入
container.set([5, 6, 7, 8], 4);
alert(container); // [1,2,3,4,5,6,7,8]
// 溢出后抛出一个error
container.set([5, 6, 7, 8], 7);
// RangeError

subarray()执行与 set()相反的操作,返回一个值从源数组复制出来的新的类型化数组。可提供开始和结束位置两个可选参数:

1
2
3
4
5
6
7
8
9
10
const source = Int16Array.of(2, 4, 6, 8);
// 复制完整的数组到一个相同类型的新数组中
const fullCopy = source.subarray();
alert(fullCopy); // [2, 4, 6, 8]
// 从第二个索引处开始复制数组
const halfCopy = source.subarray(2);
alert(halfCopy); // [6, 8]
// 复制1到3索引位置处的数组
const partialCopy = source.subarray(1, 3);
alert(partialCopy); // [4, 6]

类型化数组不具备原生的连接数组的方法,但是可以利用类型化数组的 API 手动构建该方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 第一个参数是应当返回的数组类型,剩余参数是需要连接的类型化数组
// Remaining arguments are all the typed arrays that should be concatenated
function typedArrayConcat(typedArrayConstructor, ...typedArrays) {
// 计算出整个数组中的元素数目
const numElements = typedArrays.reduce((x, y) => (x.length || x) + y.length);
// 为所有的元素提供一个空的指定类型的数组
const resultArray = new typedArrayConstructor(numElements);
// 执行数组转换
let currentOffset = 0;
typedArrays.map((x) => {
resultArray.set(x, currentOffset);
currentOffset += x.length;
});
return resultArray;
}
const concatArray = typedArrayConcat(
Int32Array,
Int8Array.of(1, 2, 3),
Int16Array.of(4, 5, 6),
Float32Array.of(7, 8, 9)
);
alert(concatArray); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
alert(concatArray instanceof Int32Array); // true
向上和向下溢出

类型化数组中值不会向上或向下溢出到其他索引中,但是仍然必须考虑数组相关的元素类型。类型化数组只接受数组中每个索引可以容纳的相关位,而不考虑它对实际数值的影响。下面演示如何处理向上和向下溢出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// ints是一个长度为2的数组
// 每一个索引都可以容纳2进制带符号整数补码,范围从-128 (-1 * 2^7) 到 127 (2^7 – 1)。
const ints = new Int8Array(2);
// unsignedInts是一个长度为2的数组
// 每一个索引都可以容纳无符号整数,范围从0 到 255 (2^7 – 1)。
const unsignedInts = new Uint8Array(2);
// 向上溢出位不会影响到相邻的索引
// 每个索引只采用最低的8个有效位
unsignedInts[1] = 256; // 0x100 100000000
alert(unsignedInts); // [0, 0]
unsignedInts[1] = 511; // 0x1FF 111111111
alert(unsignedInts); // [0, 255]
// 向下溢出位将会转化为无符号的等效位
// 0xFF是-1,作为2进制整数补码(8位截断)。
// 但是255是无符号整型
unsignedInts[1] = -1; // 0xFF (8位截断) 11111111
alert(unsignedInts); // [0, 255]
// 2的补码上溢。
// 0x80表示无符号整型数128,而-128表示2进制整型数补码
ints[1] = 128; // 0x80 10000000
alert(ints); // [0, -128]
// 2的补码下溢。
// 0xFF表示无符号整型数255,而-1表示2进制整型数补码
ints[1] = 255; // 0xFF 11111111
alert(ints); // [0, -1]

除了八个元素类型之外,还有一个额外的“夹紧”数组类型 Uint8ClampedArray,它可以防止向任何一个方向溢出。高于其最大值 255 的数值将四舍五入到 255,低于 0 的数值将四舍五入到 0。

1
2
const clampedInts = new Uint8ClampedArray([-1, 0, 255, 256]);
alert(clampedInts); // [0, 0, 255, 255]

根据 Brendan Eich 的说法,“Uint8ClampedArray 完全是 HTML5 canvas 元素的历史产物。除非你真的在做类似 canvas-y 的东西,否则要避免使用 Uint8ClampedArray 。”

Map 类型

在 ECMAScript6 规范之前,通过使用 Object,对象属性作为键,属性引用作为值,可以有效且容易地在 JavaScript 中实现键/值存储。然而这种实现方式有一定缺陷,因此 TC39 委员会认为应该为实现真正的键/值存储定义一个规范。

Map 是 ECMAScript6 中新添加的一种集合类型,向语言中引入了真正的键/值行为。Map 的大部分内容与 Object 类型的内容重叠,但是在选择要使用 Object 和 Map 类型时应该考虑到它们之间的细微差别。

基础 API

使用 new 关键字实例化一个空的 Map 实例:

1
const m = new Map();

如果希望在 Map 初始化就填充键值对,那么构造函数可接受一个包含键/值对数组的可迭代对象。迭代参数中的每一对键值都将按顺序插入到新创建的 Map 实例中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 使用嵌套数组来初始化一个Map实例
const m1 = new Map([
['key1', 'val1'],
['key2', 'val2'],
['key3', 'val3'],
]);
alert(m1.size); // 3
// 使用自定义迭代器初始化一个Map实例
const m2 = new Map({
[Symbol.iterator]: function* () {
yield ['key1', 'val1'];
yield ['key2', 'val2'];
yield ['key3', 'val3'];
},
});
alert(m2.size); // 3
// 无论是否提供了键值对,Map期望值都是键值对
const m3 = new Map([[]]);
alert(m3.has(undefined)); // true
alert(m3.get(undefined)); // undefined

初始化后可以使用 set()方法添加键/值对,使用 get()和 has()方法查询,使用 size 属性计数,使用 delete()和 clear()方法删除键/值对:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const m = new Map();
alert(m.has('firstName')); // false
alert(m.get('firstName ')); // undefined
alert(m.size); // 0
m.set('firstName', 'Matt').set('lastName', 'Frisbie');
alert(m.has('firstName')); // true
alert(m.get('firstName')); // Matt
alert(m.size); // 2
m.delete('firstName'); // 只删除这一个键值对
alert(m.has('firstName')); // false
alert(m.has('lastName')); // true
alert(m.size); // 1
m.clear(); // 删除所有的键值对
alert(m.has('firstName')); // false
alert(m.has('lastName')); // false
alert(m.size); // 0

set() 方法返回 Map 实例,因此可以将多个 set 操作链接在一起,包括初始声明:

1
2
3
const m = new Map().set('key1', 'val1');
m.set('key2', 'val2').set('key3', 'val3');
alert(m.size); // 3

Object 只能使用数字或字符串作为键,但是 Map 可以使用任何 JavaScript 数据类型作为键。它使用“SameValueZero”比较操作(在 ECMAScript 规范中定义,在实际语言中不可用),与使用严格对象等价检查的键名匹配基本相似。与 Object 一样,对于值中包含的内容没有限制。

1
2
3
4
5
6
7
8
9
10
11
12
const m = new Map();
const functionKey = function () {};
const symbolKey = Symbol();
const objectKey = new Object();
m.set(functionKey, 'functionValue');
m.set(symbolKey, 'symbolValue');
m.set(objectKey, 'objectValue');
alert(m.get(functionKey)); // functionValue
alert(m.get(symbolKey)); // symbolValue
alert(m.get(objectKey)); // objectValue
// SameValueZero检查意味着不同实例不会造成冲突
alert(m.get(function () {})); // undefined

与严格等价一样,当内容或属性改变时,Map 中用于键和值的对象和其他“集合”类型保持不变:

1
2
3
4
5
6
7
8
9
10
11
12
13
const m = new Map();
const objKey = {},
objVal = {},
arrKey = [],
arrVal = [];
m.set(objKey, objVal);
m.set(arrKey, arrVal);
objKey.foo = 'foo';
objVal.bar = 'bar';
arrKey.push('foo');
arrVal.push('bar');
alert(m.get(objKey)); // {bar: "bar"}
alert(m.get(arrKey)); // ["bar"]

使用 SameValueZero 操作可能会引发意外的冲突:

1
2
3
4
5
6
7
8
9
10
11
const m = new Map();
const a = 0 / '', // NaN
b = 0 / '', // NaN
pz = +0,
nz = -0;
alert(a === b); // false
alert(pz === nz); // true
m.set(a, 'foo');
m.set(pz, 'bar');
alert(m.get(b)); // foo
alert(m.get(nz)); // bar

注意:SameValueZero 操作对于 ECMAScript 规范来说是新的内容。在 Mozilla 的文档网站上有一篇关于 SameValueZero 和其他 ECMAScript 等价约定的优秀文章https://developer.mozilla.org/en-US/docs/ Web/JavaScript/Equality_comparisons_and_sameness

顺序和迭代

与 Object 类型的一个主要区别是,Map 实例维护键值对插入的顺序,并允许按照插入顺序执行迭代操作。

Map 实例可以提供一个迭代器,该迭代器内按插入顺序排列形式为[key,value]的数组对。可以使用 entries()或 symboli.iterator 属性检索该迭代器(实际是引用了 entries()),如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const m = new Map([
['key1', 'val1'],
['key2', 'val2'],
['key3', 'val3'],
]);
alert(m.entries === m[Symbol.iterator]); // true
for (let pair of m.entries()) {
alert(pair);
}
// [key1,val1]
// [key2,val2]
// [key3,val3]
for (let pair of m[Symbol.iterator]()) {
alert(pair);
}
// [key1,val1]
// [key2,val2]
// [key3,val3]

因为 entries()是默认的迭代器,所以可以使用扩展操作符方便地将 Map 转换为数组:

1
2
3
4
5
6
const m = new Map([
['key1', 'val1'],
['key2', 'val2'],
['key3', 'val3'],
]);
alert([...m]); // [[key1,val1],[key2,val2],[key3,val3]]

forEach(callback,optthisarg)为每个键值对调用回调函数而不是迭代器。有第二个可选参数,该参数将覆盖每个回调函数内部的 this 参数的值。

1
2
3
4
5
6
7
8
9
const m = new Map([
['key1', 'val1'],
['key2', 'val2'],
['key3', 'val3'],
]);
m.forEach((val, key) => alert(`${key} -> ${val}`));
// key1 -> val1
// key2 -> val2
// key3 -> val3

Keys()和 values()返回一个迭代器,该迭代器按照排序顺序迭代 Map 中的所有键或所有值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const m = new Map([
['key1', 'val1'],
['key2', 'val2'],
['key3', 'val3'],
]);
for (let key of m.keys()) {
alert(key);
}
// key1
// key2
// key3
for (let key of m.values()) {
alert(key);
}
// value1
// value2
// value3

迭代器中的键和值是可变的,但不能更改 Map 中的引用。但是,并不限制键或值对象内的属性更改。如下行为不会改变键和值对于 Map 实例的关系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const m1 = new Map([['key1', 'val1']]);
// 原始字符串键名不可替换
for (let key of m.keys()) {
key = 'newKey';
alert(key); // newKey
alert(m.get('key1')); // val1
}
const keyObj = { id: 1 };
const m = new Map([[keyObj, 'val1']]);
// 键名对象的属性更改,但是这个对象在Map实例内部仍然指向相同的值
for (let key of m.keys()) {
key.id = 'newKey';
alert(key); // {id: "newKey"}
alert(m.get(keyObj)); // val1
}
alert(keyObj); // {id: "newKey"}

在 Objects 和 Map 之间选择

对于大多数 web 开发来说,在 Map 或者普通 Object 之间进行选择只是一个个人喜好问题,在其他方面几乎没有什么影响。然而,对于关心内存和性能的开发者来说,Object 和 Map 之间存在着显著的差异。

内存文件

不同浏览器的 Object 和 Map 的在引擎级别实现时会有明显的差异,但是存储键值对所需的内存量与键的数量成线性关系。键值对的批量添加或删除还取决于引擎如何实现该类型的内存分配。虽然结果可能因浏览器而异,但是给定一个固定的内存量,Map 能够比 Object 多存储大约 50%的键值对。

插入性能

在 Object 和 Map 中插入一个新的键值对是大致相同的操作,但是在所有的浏览器引擎中插入 Map 通常会稍微快一些。对于这两种类型来说,插入的速度不都会随 Object 或 Map 中键值对的数量线性降低。如果代码大量使用插入操作,则 Map 实例提供更好一点的性能。

查找性能

与插入操作不同,在 Object 和 Map 中查找键值对的速度上大致相当,但在某些情况下,较少数量的键值对时,Object 实例会更快。在将 Object 实例用作数组的情况下(例如,连续的整数属性),浏览器引擎可以执行优化(例如在内存中提高布局效率)ーー这在 Map 中是不可能的。对于这两种类型,查找速度不会随 Object 或 Map 中键值对的数量线性变慢。如果您的代码大量使用查找操作,在某些情况下使用 Object 可能更为有利。

删除性能

对 Object 属性执行的删除操作的性能是臭名昭著的,这在许多浏览器引擎中仍然很常见。删除对象属性的伪解决方案包括将属性值设为 undefined 的或 null,但在许多情况下,这是不合适的折衷方案。在大多数浏览器引擎中,Map 的 delete()操作比插入和查找更快。如果代码大量使用删除操作,则 Map 类型是最合适的类型。

WeakMap 类型

在 ECMAScript6 中新添加的 WeakMap 类型是一种新的集合类型,它在语言中引入了增强的键值行为。Weakmap 类型是 Map 类型的近亲,它的 API 是 Map 类型 API 的严格子集。“弱(weak)”这个名称描述了 JavaScript 的垃圾回收器如何处理 Weakmap。

基础 API

使用 new 关键字实例化一个空的 WeakMap:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const key1 = { id: 1 },
key2 = { id: 2 },
key3 = { id: 3 };
// 使用潜逃数组实例化一个WeakMap
const wm1 = new WeakMap([
[key1, 'val1'],
[key2, 'val2'],
[key3, 'val3'],
]);
alert(wm.get(key1)); // val2
alert(wm.get(key2)); // val2
alert(wm.get(key3)); // val3
// 实例化过程是一个要么全有要么全无的过程,一个错误的键名会抛出一个error并中止实例化过程
const wm2 = new WeakMap([
[key1, 'val1'],
['BADKEY', 'val2'],
[key3, 'val3'],
]);
// TypeError: Invalid value used as WeakMap key
typeof wm2;
// ReferenceError: wm2 is not defined
// 使用对象封装的原始类型仍然可以使用
const stringKey = new String('key1');
const wm3 = new WeakMap([stringKey, 'val1']);
alert(wm3.get(stringKey)); // "val1"

键值对可以在初始化后使用 set()方法添加,用 get()方法和 has()方法查询,用 delete()方法删除:

1
2
3
4
5
6
7
8
9
10
11
const wm = new WeakMap();
const key1 = { id: 1 },
key2 = { id: 2 };
alert(wm.has(key1)); // false
alert(wm.get(key1)); // undefined
wm.set(key1, 'Matt').set(key2, 'Frisbie');
alert(wm.has(key1)); // true
alert(wm.get(key1)); // Matt
wm.delete(key1); // 只删除这一个键值对
alert(wm.has(key1)); // false
alert(wm.has(key2)); // true

set()方法返回 WeakMap 实例,因此可以将多个 set 操作包括初始声明链接在一起:

1
2
3
4
5
6
7
8
const key1 = { id: 1 },
key2 = { id: 2 },
key3 = { id: 3 };
const wm = new WeakMap().set(key1, 'val1');
wm.set(key2, 'val2').set(key3, 'val3');
alert(wm.get(key1)); // val1
alert(wm.get(key2)); // val2
alert(wm.get(key3)); // val3

弱键

“弱(weak)”源于 WeakMap 中的键名是“弱持有(weakly held)”的,这意味着它们不被计算为正式的引用,也就不会阻止垃圾收集。Weakmap 的一个重要区别是值引用不被弱持有。只要键名存在,键值对就会保留在映射中,并作为对该值的引用计数,从而防止对该值进行垃圾收集。

比如下面的例子:

1
2
const wm = new WeakMap();
wm.set({}, 'val');

在 set()中,将初始化一个新对象,并将其用作虚拟字符串的键。因为没有对这个对象的其他引用,所以只要执行完这一行,对象键名就可以用于垃圾收集。发生这种情况时,键值对将从 WeakMap 中消失且为空。在这个示例中,由于没有对该键值的其他引用,这个键值对删除还意味着该键值可以进行垃圾收集。

参考一个稍微不同的例子:

1
2
3
4
5
6
7
8
const wm = new WeakMap();
const container = {
key: {},
};
wm.set(container.key, 'val');
function removeReference() {
container.key = null;
}

在这里,container 对象维护对 WeakMap 中键的引用,因此该对象不适用于垃圾收集。但是,一旦调用 removeReference(),对键对象的最后一个强引用将被销毁,垃圾收集机制最终将清除键值对。

不可迭代的键

由于 WeakMap 中的键值对可以随时销毁,因此提供遍历键值对的能力是无意义的。这也排除了使用 clear()一次销毁所有键值对的需求,clear()不是 WeakMap API 的一部分。因为不允许迭代,所以也不可能从 WeakMap 实例检索值(除非您有对键对象的引用)。即使代码可以访问 WeakMap 实例,也无法检查其内容。

Weakmap 实例将键名限制只为对象的原因是为了遵守这样一个约定:即从 WeakMap 中只能通过引用键对象检索值。如果允许原始类型,WeakMap 实例将无法区分最初用于设置键值对的字符串原始类型和后来初始化的相同字符串原始类型ーー这是一种不希望出现的行为。

实用性

Weakmap 实例与现有的 JavaScript 工具有着显著的不同,并且没有很明显地指示出应该如何使用。这个问题没有一个简单的答案,但已经出现了一些策略。

私有变量

Weakmap 实例为在 JavaScript 实现真正的私有变量提供了一种全新的方式。前提比较直接:私有变量将存储在 WeakMap 中,对象实例作为键名,私有成员字典作为值。

实现方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const wm = new WeakMap();
class User {
constructor(id) {
this.idProperty = Symbol('id');
this.setId(id);
}
setPrivate(property, value) {
const privateMembers = wm.get(this) || {};
privateMembers[property] = value;
wm.set(this, privateMembers);
}
getPrivate(property) {
return wm.get(this)[property];
}
setId(id) {
this.setPrivate(this.idProperty, id);
}
getId() {
return this.getPrivate(this.idProperty);
}
}
const user = new User(123);
alert(user.getId()); // 123
user.setId(456);
alert(user.getId()); // 456
// 证明这不是一个真正私有变量
alert(wm.get(user)[user.idProperty]); // 456

仔细观察可以发现,在这种实现中,外部代码只需要对对象实例和 WeakMap 的引用,就可以检索私有变量。为了防止这种情况,可以将 WeakMap 包装在一个闭包中以便完全隐藏 WeakMap 实例,不让外部代码看到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const User = (() => {
const wm = new WeakMap();
class User {
constructor(id) {
this.idProperty = Symbol('id');
this.setId(id);
}
setPrivate(property, value) {
const privateMembers = wm.get(this) || {};
privateMembers[property] = value;
wm.set(this, privateMembers);
}
getPrivate(property) {
return wm.get(this)[property];
}
setId(id) {
this.setPrivate(this.idProperty, id);
}
getId(id) {
return this.getPrivate(this.idProperty);
}
}
return User;
})();

const user = new User(123);
alert(user.getId()); // 123
user.setId(456);
alert(user.getId()); // 456

因此,如果没有用于插入的键,就不能检索 WeakMap 中的值。尽管这会阻止前面提到的私有变量的访问模式,但在某些方面,它将代码推向了 ES6 之前的闭包私有变量模式。

DOM 节点元数据

因为 WeakMap 实例不会干扰垃圾收集,所以它可用于清除无需清除的元数据关联。比如下面的例子,使用了一个普通的 Map:

1
2
3
4
const m = new Map();
const loginButton = document.querySelector('#login');
// 关联一些节点的元数据
m.set(loginButton, { disabled: true });

假设这段代码执行后,页面被 JavaScript 更改,登录按钮从 DOM 树中删除。由于在 Map 中存在一个引用,所以 DOM 节点将永久地停留在内存中,直到显式地从 Map 中删除或者 Map 被销毁。

如果使用 WeakMap,如下面的代码所示,从 DOM 中删除节点将允许垃圾收集器立即释放分配的内存(假设没有其他对象的延迟引用)。

1
2
3
4
const wm = new WeakMap();
const loginButton = document.querySelector('#login');
// 关联一些节点的元数据
wm.set(loginButton, { disabled: true });

Set 类型

在 ECMAScript6 中新添加的 Set 是一种新的集合类型,引入了 set 行为。因为许多 API 和行为都是共享的,Set 在很多方面更像是一个扩展的 Map。

基础 API

使用 new 关键字实例化一个空的 Set 类型:

1
const m = new Set();

如果希望在初始化时就填充,构造函数可以选择接受一个要添加到新创建的 Set 实例中的元素组成的的可迭代对象。

1
2
3
4
5
6
7
8
9
10
11
12
// 使用数组实例化一个Set
const s1 = new Set(["val1", "val2", "val3]);
alert(s1.size); // 3
// 使用自定义迭代器实例化一个Set
const s2 = new Set({
[Symbol.iterator]: function*() {
yield "val1";
yield "val2";
yield "val3";
}
});
alert(s2.size); // 3

初始化后使用 add()方法可以添加值,使用 has()方法查询值,使用 size 属性计数,使用 delete()和 clear()方法删除值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const s = new Set();
alert(s.has('Matt')); // false
alert(s.size); // 0
s.add('Matt').add('Frisbie');
alert(s.has('Matt')); // true
alert(s.size); // 2
s.delete('Matt');
alert(s.has('Matt')); // false
alert(s.has('Frisbie')); // true
alert(s.size); // 1
s.clear(); // 销毁Set实例中所有的值
alert(s.has('Matt')); // false
alert(s.has('Frisbie')); // false
alert(s.size); // 0

add()方法返回 Set 实例,因此可以将多个 add()操作(包括初始声明)连接在一起,:

1
2
3
const s = new Set().add('val1');
s.set('val2').set('val3');
alert(s.size); // 3

像 Map 一样,Set 可以包含任何 JavaScript 数据类型作为值。它使用“SameValueZero”进行比较操作(在 ECMAScript 规范中定义,在实际语言中不可用),与使用严格的对象等价来检查匹配大致相同。对于值中包含的内容没有限制。

1
2
3
4
5
6
7
8
9
10
11
12
const s = new Set();
const functionVal = function () {};
const symbolVal = Symbol();
const objectVal = new Object();
s.add(functionVal);
s.add(symbolVal);
s.add(objectVal);
alert(s.has(functionVal)); // true
alert(s.has(symbolVal)); // true
alert(s.has(objectVal)); // true
// SameValueZero 检查意味着相同内容的不同实例不会发生冲突
alert(s.has(function () {})); // false

与严格等价一样,当用于值的对象和其他“集合(collection)”类型的内容或属性被更改时保持不变:

1
2
3
4
5
6
7
8
const s = new Set();
const objVal = {},
arrVal = [];
s.add(objVal).add(arrVal);
objVal.bar = 'bar';
arrVal.push('bar');
alert(s.has(objVal)); // true
alert(s.has(arrVal)); // true

Add()和 delete()操作是幂等的。delete()返回一个布尔值,指示该值是否存在于 set 中。

1
2
3
4
5
6
7
8
9
const s = new Set();
s.add('foo');
alert(s.size); // 1
s.add('foo');
alert(s.size); // 1
// 值在set种存在
alert(s.delete('foo')); // true
// 值在set种不存在
alert(s.delete('foo')); // false

顺序和迭代

Set 保持值插入的顺序,并允许按照插入顺序执行迭代操作。

Set 实例有将 Set 内容的按插入顺序排列的迭代器。可以使用 values()、别名 keys()或 symbolist.iterator 属性检索这个迭代器,这些都引用 values():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const s = new Set(['val1', 'val2', 'val3']);
alert(s.values === s[Symbol.iterator]); // true
alert(s.keys === s[Symbol.iterator]); // true
for (let value of s.values()) {
alert(value);
}
// val1
// val2
// val3
for (let value of s[Symbol.iterator]()) {
alert(value);
}
// val1
// val2
// val3

因为 value()是默认的迭代器,所以可以使用 spread 操作符简洁地将 set 转换为数组:

1
2
const s = new Set(['val1', 'val2', 'val3']);
alert([...s]); // [val1,val2,val3]

entries()返回一个迭代器,该迭代器包含一个二元素数组,该数组按插入顺序包含 Set 中所有值的副本:

1
2
3
4
5
6
7
const s = new Set(['val1', 'val2', 'val3']);
for (let pair of s.entries()) {
alert(pair);
}
// [val1,val1]
// [val2,val2]
// [val3,val3]

forEach(callback,optthisarg)调用每个值的回调时,使用回调函数而不是迭代器。接受第二个可选参数,该参数将覆盖每个回调函数内部的 this 指向。

1
2
3
4
5
const s = new Set(['val1', 'val2', 'val3']);
s.forEach((val, dupVal) => alert(`${val} -> ${dupVal}`));
// val1 -> val1
// val2 -> val2
// val3 -> val3

更改 Set 中值的属性不会更改 Set 实例的值的 id:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const s1 = new Set(['val1']);
// 原始字符串类型值不能更改
for (let value of m.values()) {
value = 'newVal';
alert(value); // newVal
alert(s.has('val1')); // true
}
const valObj = { id: 1 };
const s2 = new Set([valObj]);
// 值对象的属性改变了,但是这个对象依然存在在set中
for (let value of s.values()) {
value.id = 'newVal';
alert(value); // {id: "newVal"}
alert(s.has(valObj)); // true
}
alert(valObj); // {id: "newKey"}

定义形式化的 Set 操作

Set 在许多方面感觉像是 Map,只不过 API 稍微重新排列了一下。通过其 API 仅支持自引用操作来强调这一点。许多开发者对使用 Set 操作有兴趣,这需要采用继承 Set 的方式或定义实用程序库形式手工实现。为了同时提供这两种方式,可以在子类上实现静态方法,然后在实例方法中使用这些静态方法。在执行这些操作时,需要记住以下几点:

  • 有些 Set 操作是相互关联的,因此最好能够实现该方法以便它能够处理任意数量的 Set 实例是。

  • Set 保留插入顺序,从这些方法返回的 set 应该反映处这一事实。

  • 尽可能高效地使用内存。扩展操作符提供了很好的语法,但尽可能避免在 set 和数组之间来回切换以节省对象初始化成本。

  • 不要修改现有的 Set 实例。union(a, b) 或 a.union(b))应该返回一个新的 Set 类型的实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
class XSet extends Set {
union(...sets) {
return XSet.union(this, ...sets);
}
intersection(...sets) {
return XSet.intersection(this, ...sets);
}
difference(set) {
return XSet.difference(this, set);
}
symmetricDifference(set) {
return XSet.symmetricDifference(this, set);
}
cartesianProduct(set) {
return XSet.cartesianProduct(this, set);
}
powerSet() {
return XSet.powerSet(this);
}
// 返回多个set实例的并集
static union(a, ...bSets) {
const unionSet = new XSet(a);
for (const b of bSets) {
for (const bValue of b) {
unionSet.add(bValue);
}
}
return unionSet;
}

// 返回多个set实例的交集
static intersection(a, ...bSets) {
const intersectionSet = new XSet(a);
for (const aValue of intersectionSet) {
for (const b of bSets) {
if (!b.has(aValue)) {
intersectionSet.delete(aValue);
}
}
}
return intersectionSet;
}
// 返回多个set实例的补集
static difference(a, b) {
const differenceSet = new XSet(a);
for (const bValue of b) {
if (a.has(bValue)) {
differenceSet.delete(bValue);
}
}
return differenceSet;
}

// 返回多个set实例的对称差
static symmetricDifference(a, b) {
// a∪b - a∩b
return a.union(b).difference(a.intersection(b));
}
// 返回两个set的笛卡尔积,第一个是a的元素而第二个是b的所有可能有序对的其中一个元素
static cartesianProduct(a, b) {
const cartesianProductSet = new XSet();
for (const aValue of a) {
for (const bValue of b) {
cartesianProductSet.add([aValue, bValue]);
}
}
return cartesianProductSet;
}
// 返回一个set的幂集
static powerSet(a) {
const powerSet = new XSet().add(new XSet());
for (const aValue of a) {
for (const set of new XSet(powerSet)) {
powerSet.add(new XSet(set).add(aValue));
}
}
return powerSet;
}
}

WeakSet 类型

在 ECMAScript6 中新添加的 WeakSet 是一种新的集合类型。Weakset 类型是 Set 类型的近亲,它的 API 是 Set 类型 API 的严格子集。“弱(weak)”这个名称描述了 JavaScript 的垃圾收集器如何处理弱映射中的值。

基础 API

用 new 关键字实例化一个空的 WeakSet 类型:

1
const ws = new WeakSet();

Weakset 中的值只能是 Object 类型或者继承自 Object 类型 ー尝试使用非 Object 类型去设置值会抛出 TypeError。

如果希望在 WeakSet 初始化时就填充值,构造函数可以地接受一个可选包含有效值的对象。参数中的每个值将按照迭代的顺序插入到新创建的 WeakSet 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const val1 = {id: 1},
val2 = {id: 2},
// 用嵌套数组实例化一个WeakSet
const ws1 = new WeakSet([val1, val2, val3]);
alert(ws1.has(val1)); // true
alert(ws1.has(val2)); // true
alert(ws1.has(val3)); // true
// 实例化过程是一个要么全有要么全无的过程,一个错误的键名会抛出一个error并中止实例化过程
const ws2 = new WeakSet([val1, "BADVAL", val3]);
// TypeError: Invalid value used in WeakSet
typeof ws2;
// ReferenceError: ws2 is not defined
// 使用对象封装的原始类型仍然可以使用
const stringVal = new String("val1");
const ws3 = new WeakSet([stringVal]);
alert(ws3.has(stringVal)); // true

初始化后可以使用 add()方法添加值,使用 has()方法查询值,使用 delete()方法删除值:

1
2
3
4
5
6
7
8
9
10
const ws = new WeakSet();
const val1 = { id: 1 },
val2 = { id: 2 };
alert(ws.has(val1)); // false
ws.add(val1).add(val2);
alert(ws.has(val1)); // true
alert(ws.has(val2)); // true
ws.delete(val1); // 只删除这一个值
alert(ws.has(val1)); // false
alert(ws.has(val2)); // true

add()方法返回 WeakSet 实例,因此可以将多个 add 包括初始声明操作连接在一起,:

1
2
3
4
5
6
7
8
const val1 = { id: 1 },
val2 = { id: 2 },
val3 = { id: 3 };
const ws = new WeakSet().add(val1);
ws.add(val2).add(val3);
alert(ws.has(val1)); // true
alert(ws.has(val2)); // true
alert(ws.has(val3)); // true

弱键

“弱(weak)”源于 WeakSet 中的值是“弱持有(weakly held)”的,这意味着它们不被计算为正式的引用,也就不会阻止垃圾收集。

比如下面的例子:

1
2
const ws = new WeakSet();
ws.add({});

在 add()中,一个新对象被初始化并用作 WeakSet 实例的一个值。因为没有对这个对象的其他引用,所以只要这一行执行完毕,该对象值就可以被垃圾收集器释放。当这种情况发生时,这个值将从 WeakSet 中消失并且为空。

下面是一个稍微不同的例子:

1
2
3
4
5
6
7
8
const ws = new WeakSet();
const container = {
val: {},
};
ws.add(container.val);
function removeReference() {
container.val = null;
}

在这里,container 对象维护对 WeakSet 实例中的值的引用,因此该对象不会被垃圾收集。但是,一旦调用 removeReference(),就会销毁对 val 对象的最后一个强引用,最终垃圾收集将清除该值。

不可迭代的值

因为 WeakSet 中的值可以在任何时候被销毁,所以提供迭代这些值的方法是无意义的。也排除了一次销毁所有值的方法,所以 clear()不是 WeakSet API 的一部分。因为迭代是不可能的,所以从 WeakSet 实例中检索值也是不可能的(除非您有对值对象的引用)。即使代码可以访问 WeakSet 实例,也无法检查其内容。

WeakSet 实例将键限制仅为对象,也就是只能通过引用值对象从 WeakSet 中检索值。如果允许原始类型,WeakSet 将无法区分最初用于设置值的原始类型字符串和稍后初始化的相同原始类型字符串ーー这是一种不希望出现的行为。

实用性

与 WeakMap 实例相比,WeakSet 实例的实用性更为有限,但它们对于标记对象仍然很有作用。

参考下面的例子,使用了一个常规的 Set:

1
2
3
4
const disabledElements = new Set();
const loginButton = document.querySelector('#login');
// 添加进相关的set将节点标记为disabled
disabledElements.add(loginButton);

在这里,可以极快的通过查看 disabledElements 中元素来检查是否被禁用。但是,如果从 DOM 中删除该元素,该元素在 Set 中的存在将阻止垃圾回收器重新分配内存。

为了允许垃圾回收器重新分配元素的内存,可以使用 WeakSet 代替:

1
2
3
4
const disabledElements = new WeakSet();
const loginButton = document.querySelector('#login');
// 添加进相关的set将节点标记为disabled
disabledElements.add(loginButton);

现在,当从 DOM 中删除 WeakSet 中的任何元素时,垃圾回收器在进行垃圾收集时将忽略它在 WeakSet 中的引用。

迭代和扩展操作符

ECMAScript 6 引入了迭代器和扩展运算符,在集合引用类型上下文环境中特别有用。这些新工具允许简单的互操作性、克隆和修改集合类型。

注意:Iterators and Generators 章节提供了更多关于迭代器如何使用的内容。

如本章前文所示,四个原生集合引用类型定义了一个默认迭代器:

  • Array
  • All typed arrays
  • Map
  • Set

实际上,所有支持有序迭代的类型可以传递给 for..of 循环:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let iterableThings = [
Array.of(1, 2),
(typedArr = Int16Array.of(3, 4)),
new Map([
[5, 6],
[7, 8],
]),
new Set([9, 10]),
];
for (const iterableThing of iterableThings) {
for (const x of iterableThing) {
console.log(x);
}
}
// 1
// 2
// 3
// 4
// [5, 6]
// [7, 8]
// 9
// 10

所有上述类型都可以使用扩展操作。扩展运算符对迭代对象执行浅拷贝,使你可以用简洁的语法克隆整个对象:

1
2
3
4
5
let arr1 = [1, 2, 3];
let arr2 = [...arr1];
console.log(arr1); // [1, 2, 3]
console.log(arr2); // [1, 2, 3]
console.log(arr1 === arr2); // false

期望一个可迭代对象参数的构造函数可以通过一个可迭代实例参数进行克隆:

1
2
3
4
5
6
7
let map1 = new Map([
[1, 2],
[3, 4],
]);
let map2 = new Map(map1);
console.log(map1); // Map {1 => 2, 3 => 4}
console.log(map2); // Map {1 => 2, 3 => 4}

还允许进行数组一部分的构造:

1
2
3
let arr1 = [1, 2, 3];
let arr2 = [0, ...arr1, 4, 5];
console.log(arr2); // [0, 1, 2, 3, 4, 5]

浅拷贝机制意味着只复制对象的引用:

1
2
3
4
let arr1 = [{}];
let arr2 = [...arr1];
arr1[0].foo = ‘bar’;
console.log(arr2[0]); // { foo: ‘bar’ }

这些集合类型中的每一个都支持多种构造方法,比如 Array.of()和 Array.from()静态方法。当与扩展操作符结合时,这使得互操作性变得非常简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let arr1 = [1, 2, 3];
// 把一个数组复制到一个类型化数组
let typedArr1 = Int16Array.of(...arr1);
let typedArr2 = Int16Array.from(arr1);
console.log(typedArr1); // Int16Array [1, 2, 3]
console.log(typedArr2); // Int16Array [1, 2, 3]
// 把一个数组复制到Map实例中
let map = new Map(arr1.map((x) => [x, ‘val’ + x]));
console.log(map); // Map {1 => ‘val 1’, 2 => ‘val 2’, 3 => ‘val 3’}
// 把一个数组复制到set中
let set = new Set(typedArr2);
console.log(set); // Set {1, 2, 3}
// 把一个Set实例复制回数组
let arr2 = [...set];
console.log(arr2); // [1, 2, 3]
👆 全文结束,棒槌时间到 👇