探索-JavaScript-ES2025-版--九-
探索 JavaScript(ES2025 版)(九)
原文:
exploringjs.com/js/book/index.html译者:飞龙
35 类型化数组:处理二进制数据 ES6 (高级)
-
35.1 类型化数组 API:二进制数据的容器
-
35.1.1 类型化数组的用例
-
35.1.2 核心类:
ArrayBuffer、类型化数组、DataView -
35.1.3
SharedArrayBuffer(ES2017)
-
-
35.2 使用类型化数组
-
35.2.1 创建类型化数组
-
35.2.2 包装的 ArrayBuffer
-
35.2.3 获取和设置元素
-
35.2.4 连接类型化数组
-
35.2.5 类型化数组与普通数组
-
-
35.3 使用 DataView
-
35.4 元素类型
-
35.4.1 处理溢出和下溢
-
35.4.2 字节序
-
-
35.5 转换为和从类型化数组转换
-
35.5.1 静态方法
«ElementType»Array.from() -
35.5.2 类型化数组是可迭代的
-
35.5.3 将类型化数组转换为普通数组以及反之
-
35.5.4 将
Uint8Array(UTF-8) 转换为字符串以及反之
-
-
35.6 调整 ArrayBuffer 的大小 (ES2024)
-
35.6.1 ArrayBuffer 的新特性
-
35.6.2 类型化数组如何响应 ArrayBuffer 大小的变化
-
35.6.3 ECMAScript 规范提供的指南
-
-
35.7 传输和分离 ArrayBuffer (ES2024)
-
35.7.1 准备:数据传输和分离
-
35.7.2 与传输和分离相关的方法
-
35.7.3 通过
structuredClone()传输 ArrayBuffer -
35.7.4 在同一代理内传输 ArrayBuffer
-
35.7.5 分离 ArrayBuffer 会如何影响其包装器?
-
35.7.6
ArrayBuffer.prototype.transferToFixedLength()
-
-
35.8 快速参考:索引与偏移量
-
35.9 快速参考:ArrayBuffer
-
35.9.1
new ArrayBuffer() -
35.9.2
ArrayBuffer.* -
35.9.3
ArrayBuffer.prototype.*: 获取和切片 -
35.9.4
ArrayBuffer.prototype.*: 调整大小
-
-
35.10 快速参考:Typed Arrays
-
35.10.1
TypedArray.* -
35.10.2
TypedArray.prototype.* -
35.10.3
new «ElementType»Array() -
35.10.4
«ElementType»Array.* -
35.10.5
«ElementType»Array.prototype.*
-
-
35.11 快速参考:DataViews
-
35.11.1
new DataView() -
35.11.2
DataView.prototype.*
-
35.1 The Typed Array API: containers for binary data
网络上的大量数据是文本:JSON 文件、HTML 文件、CSS 文件、JavaScript 代码等。JavaScript 通过其内置的字符串很好地处理这类数据。
然而,在 2011 年之前,它并没有很好地处理二进制数据。Typed Array 规范 1.0于 2011 年 2 月 8 日推出,为处理二进制数据提供了工具。随着 ECMAScript 6 的推出,Typed Arrays 被添加到核心语言中,并获得了之前仅适用于普通数组的方法(.map()、.filter()等)。
35.1.1 Use cases for Typed Arrays
Typed Arrays 的主要用途包括:
-
处理二进制数据:管理图像数据、操作二进制文件、处理二进制网络协议等。
-
与原生 API 交互:原生 API 通常以二进制格式接收和返回数据,在 ES6 之前的 JavaScript 中,我们无法很好地存储或操作这些数据。这意味着每次与这样的 API 通信时,数据都必须在每次调用时从 JavaScript 转换为二进制格式,然后再转换回来。Typed Arrays 消除了这个瓶颈。例如包括:
-
WebGL,"基于 OpenGL ES 的低级 3D 图形 API,通过 HTML5 Canvas 元素暴露给 ECMAScript"。Typed Arrays 最初是为 WebGL 创建的。文章“Typed Arrays: Binary Data in the Browser”(由 Ilmari Heikkinen 为 HTML5 Rocks 撰写)的“Typed Arrays 的历程”部分提供了更多信息。
-
WebGPU,"一个用于在图形处理单元上执行操作(如渲染和计算)的 API"。例如,WebGPU 使用 ArrayBuffer 作为后备存储的包装器。
-
WebAssembly(简称“Wasm”),"一种基于栈的虚拟机的二进制指令格式。Wasm 被设计为编程语言的便携式编译目标,使得客户端和服务器应用程序能够在网络上部署。"例如,WebAssembly 代码的内存存储在 ArrayBuffer 或 SharedArrayBuffer 中(详情)。
-
35.1.2 核心类:ArrayBuffer、类型数组、DataView
类型数组 API 将二进制数据存储在ArrayBuffer的实例中:
const buf = new ArrayBuffer(4); // length in bytes
// buf is initialized with zeros
数组缓冲区本身是一个黑盒:如果我们想访问其数据,我们必须将其包装在另一个对象中——一个视图对象。有两种类型的视图对象可用:
-
类型数组与普通数组的工作方式相似,并允许我们将数据作为具有相同类型的索引元素序列访问。例如包括:
-
Uint8Array:元素是无符号的 8 位整数。无符号意味着它们的范围从零开始。 -
Int16Array:元素是带符号的 16 位整数。带符号意味着它们有符号,可以是负数、零或正数。 -
Float16Array:元素是 16 位浮点数。
-
-
数据视图允许我们将数据解释为各种类型(
Uint8、Int16、Float16等),我们可以在任何字节偏移量处读取和写入这些类型。
图 35.1 显示了 API 的类图。

图 35.1:类型数组 API 的类。
35.1.3 SharedArrayBuffer (ES2017)
共享数组缓冲区是一个数组缓冲区,其内存可以被多个代理(代理可以是主线程或 Web Worker)同时访问。
-
在数组缓冲区可以在代理之间传输(移动,而不是复制)的情况下,共享数组缓冲区是不可传输的,必须进行克隆。然而,这仅克隆了它们的表层部分。数据存储本身是共享的。
-
共享数组缓冲区可以调整大小,但它们只能增长而不能缩小,因为缩小共享内存太复杂了。
-
Atomics是一个全局命名空间,用于补充共享数组缓冲区的 API。ECMAScript 规范描述它为“在共享内存数组单元上操作不可分割(原子)的函数以及允许代理等待和调度原始事件的函数。当有纪律地使用时,Atomics 函数允许通过共享内存通信的多代理程序在并行 CPU 上以可理解的方式执行。”
有关SharedArrayBuffer(SharedArrayBuffer)和Atomics(Atomics)的更多信息,请参阅 MDN Web 文档。
35.2 使用类型数组
类型数组的使用方式与普通数组非常相似。
35.2.1 创建类型数组
以下代码展示了创建相同类型数组的三种不同方法:
// Argument: Typed Array or Array-like object
const ta1 = new Uint8Array([0, 1, 2]);
const ta2 = Uint8Array.of(0, 1, 2);
assert.deepEqual(ta2, ta1);
const ta3 = Uint8Array.from([0, 1, 2]);
assert.deepEqual(ta3, ta1);
const ta4 = new Uint8Array(3); // length of Typed Array
ta4[0] = 0;
ta4[1] = 1;
ta4[2] = 2;
assert.deepEqual(ta4, ta1);
35.2.2 包装的ArrayBuffer
const typedArray = new Int16Array(2); // 2 elements
assert.equal(typedArray.length, 2);
assert.deepEqual(
typedArray.buffer, new ArrayBuffer(4)); // 4 bytes
35.2.3 获取和设置元素
const typedArray = new Int16Array(2);
assert.equal(typedArray[1], 0); // initialized with 0
typedArray[1] = 72;
assert.equal(typedArray[1], 72);
35.2.4 连接类型数组
类型化数组没有像普通数组那样的 .concat() 方法。解决方案是使用它们的重载方法 .set():
.set(typedArray: TypedArray, offset=0): void
.set(arrayLike: ArrayLike<number>, offset=0): void
它将现有的 typedArray 或 arrayLike 复制到接收器中的 offset 索引处。TypedArray 是所有具体类型化数组类的内部抽象超类(实际上没有全局名称)。
以下函数使用该方法将零个或多个类型化数组(或类似数组的对象)复制到 resultConstructor 的一个实例中:
function concatenate(resultConstructor, ...arrays) {
let totalLength = 0;
for (const arr of arrays) {
totalLength += arr.length;
}
const result = new resultConstructor(totalLength);
let offset = 0;
for (const arr of arrays) {
result.set(arr, offset);
offset += arr.length;
}
return result;
}
assert.deepEqual(
concatenate(Uint8Array, Uint8Array.of(1, 2), [3, 4]),
Uint8Array.of(1, 2, 3, 4));
35.2.5 类型化数组与普通数组
类型化数组与普通数组非常相似:它们都有一个 .length,可以通过中括号运算符 [] 访问元素,并且具有大多数标准数组方法。它们与普通数组在以下方面有所不同:
-
类型化数组有缓冲区。类型化数组
ta的元素不存储在ta中,而是存储在关联的 ArrayBuffer 中,可以通过ta.buffer访问:const ta = new Uint16Array(2); // 2 elements assert.deepEqual( ta.buffer, new ArrayBuffer(4)); // 4 bytes -
类型化数组以零初始化:
-
new Array(4)创建一个没有任何元素的普通数组。它只有四个空洞(小于.length的索引没有关联的元素)。 -
new Uint8Array(4)创建一个类型化数组,其四个元素都是 0。
assert.deepEqual(new Uint8Array(4), Uint8Array.of(0, 0, 0, 0)); -
-
类型化数组的所有元素都具有相同的类型:
-
设置元素会将值转换为该类型。
const ta = new Uint8Array(1); ta[0] = 257; assert.equal(ta[0], 1); // 257 % 256 (overflow) ta[0] = '2'; assert.equal(ta[0], 2); -
获取元素返回数字或大整数。
const ta = new Uint8Array(1); assert.equal(ta[0], 0); assert.equal(typeof ta[0], 'number');
-
-
类型化数组的
.length从其 ArrayBuffer 中派生,永远不会改变(除非我们切换到不同的 ArrayBuffer)。 -
普通数组可以有空洞;类型化数组不能。
练习:字符串与 UTF-16 之间的转换
exercises/typed-arrays/utf-16-conversion_test.mjs
35.3 使用 DataViews
这就是如何使用 DataViews:
const dataView = new DataView(new ArrayBuffer(4));
assert.equal(dataView.getInt16(0), 0);
assert.equal(dataView.getUint8(0), 0);
dataView.setUint8(0, 5);
35.4 元素类型
| 元素 | 类型化数组 | 字节 | 描述 | 获取/设置 | |
|---|---|---|---|---|---|
Int8 |
Int8Array |
1 | 8-bit signed integer | number |
ES6 |
Uint8 |
Uint8Array |
1 | 8-bit unsigned int | number |
ES6 |
(Uint8C) |
Uint8ClampedArray |
1 | 8-bit unsigned int | number |
ES6 |
Int16 |
Int16Array |
2 | 16-bit signed int | number |
ES6 |
Uint16 |
Uint16Array |
2 | 16-bit unsigned int | number |
ES6 |
Int32 |
Int32Array |
4 | 32-bit signed int | number |
ES6 |
Uint32 |
Uint32Array |
4 | 32-bit unsigned int | number |
ES6 |
BigInt64 |
BigInt64Array |
8 | 64-bit signed int | bigint |
ES2020 |
BigUint64 |
BigUint64Array |
8 | 64-bit unsigned int | bigint |
ES2020 |
Float16 |
Float16Array |
2 | 16-bit floating point | number |
ES2025 |
Float32 |
Float32Array |
4 | 32-bit floating point | number |
ES6 |
Float64 |
Float64Array |
8 | 64-bit floating point | number |
ES6 |
表 35.1:类型化数组 API 支持的元素类型。
表 35.1 列出了可用的元素类型。这些类型(例如,Int32)出现在两个位置:
-
在类型数组中,它们指定了元素的类型。例如,
Int32Array的所有元素都具有Int32类型。元素类型是类型数组唯一不同的方面。 -
在数据视图中,它们是我们使用
.getInt32()和.setInt32()等方法访问其 ArrayBuffers 时的透镜。
元素类型Uint8C是特殊的:它不被DataView支持,仅存在以启用Uint8ClampedArray。此类型数组由canvas元素使用(其中它取代了CanvasPixelArray),应避免使用。Uint8C和Uint8之间的唯一区别是它们处理溢出的方式(如下所述)。
类型数组和数组缓冲区使用数字和大整数来导入和导出值:
-
BigInt64和BigUint64类型通过大整数处理。例如,设置器接受大整数,获取器返回大整数。 -
所有其他元素类型都通过数字处理。
35.4.1 处理溢出和下溢
35.4.1.1 处理整数的溢出
通常,当值超出元素类型范围时,使用模运算将其转换为范围内的值。对于有符号和无符号整数,这意味着:
-
最高值加一转换为最低值(对于无符号整数来说是 0)。
-
最低值减一转换为最高值。
以下函数有助于说明转换是如何工作的:
function setAndGet(typedArray, value) {
typedArray[0] = value;
return typedArray[0];
}
无符号 8 位整数的模转换:
const uint8 = new Uint8Array(1);
// Highest value of range
assert.equal(setAndGet(uint8, 255), 255);
// Positive overflow
assert.equal(setAndGet(uint8, 256), 0);
// Lowest value of range
assert.equal(setAndGet(uint8, 0), 0);
// Negative overflow
assert.equal(setAndGet(uint8, -1), 255);
有符号 8 位整数的模转换:
const int8 = new Int8Array(1);
// Highest value of range
assert.equal(setAndGet(int8, 127), 127);
// Positive overflow
assert.equal(setAndGet(int8, 128), -128);
// Lowest value of range
assert.equal(setAndGet(int8, -128), -128);
// Negative overflow
assert.equal(setAndGet(int8, -129), 127);
限制转换不同:
-
所有负溢出值都转换为最低值。
-
所有正溢出值都转换为最高值。
const uint8c = new Uint8ClampedArray(1);
// Highest value of range
assert.equal(setAndGet(uint8c, 255), 255);
// Positive overflow
assert.equal(setAndGet(uint8c, 256), 255);
// Lowest value of range
assert.equal(setAndGet(uint8c, 0), 0);
// Negative overflow
assert.equal(setAndGet(uint8c, -1), 0);
35.4.1.2 处理浮点数的溢出和下溢
const float16 = new Float16Array(1);
function setAndGet(typedArray, value) {
typedArray[0] = value;
return typedArray[0];
}
如果发生正溢出(正数离零太远),结果是正无穷大:
assert.equal(
setAndGet(float16, 2**15),
32768
);
assert.equal(
setAndGet(float16, 2**16),
Infinity
);
assert.equal(
2**16,
65536 // float64
);
如果发生负溢出(负数离零太远),结果是负无穷大:
assert.equal(
setAndGet(float16, -(2**15)),
-32768
);
assert.equal(
setAndGet(float16, -(2**16)),
-Infinity
);
assert.equal(
-(2**16),
-65536 // float64
);
算术下溢意味着一个数字在二进制小数点后有太多数字(它太接近整数)。如果发生这种情况,无法表示的数字将被省略:
assert.equal(
setAndGet(float16, 2**-24),
5.960464477539063e-8
);
assert.equal(
setAndGet(float16, 2**-25),
0,
);
assert.equal(
2**-25,
2.9802322387695312e-8 // float64
);
有用的相关函数:Math.f16round(x) 将x四舍五入到 16 位(在 64 位浮点数内)。
35.4.2 字节序
当一个类型(如Uint16)作为多个字节的序列存储时,字节序很重要:
-
大端序:最高有效字节最先。例如,
Uint16值 0x4321 存储为两个字节——首先是 0x43,然后是 0x21。 -
小端序:最低有效字节最先。例如,
Uint16值 0x4321 存储为两个字节——首先是 0x21,然后是 0x43。
字节序通常在每个 CPU 架构中是固定的,并且与原生 API 保持一致。类型数组用于与这些 API 通信,这就是为什么它们的字节序遵循平台的字节序,并且不能更改。
另一方面,协议和二进制文件的字节序可能不同,但每个格式在平台间是固定的。因此,我们必须能够以任意的字节序访问数据。DataViews 用于满足这一需求,并允许我们在获取或设置值时指定字节序。
-
大端表示法是数据网络中最常见的约定;在互联网协议套件的协议中,如 IPv4、IPv6、TCP 和 UDP,字段以大端顺序传输。因此,大端字节序也被称为网络字节序。
-
小端存储在微处理器中很受欢迎,部分原因是英特尔公司对微处理器设计产生了重大历史影响。
其他排序也是可能的。这些通常被称为中间端序或混合端序。
35.5 将数据转换为和从类型数组转换
在本节中,「ElementType」Array代表Int8Array、Uint8Array等。ElementType是Int8、Uint8等。
35.5.1 静态方法「ElementType」Array.from()
此方法具有以下类型签名:
.from<S>(
source: Iterable<S>|ArrayLike<S>,
mapfn?: S => ElementType, thisArg?: any)
: «ElementType»Array
.from()将source转换为this(一个类型数组)的实例。
例如,普通数组是可迭代的,并且可以使用此方法进行转换:
assert.deepEqual(
Uint16Array.from([0, 1, 2]),
Uint16Array.of(0, 1, 2));
类型数组也是可迭代的:
assert.deepEqual(
Uint16Array.from(Uint8Array.of(0, 1, 2)),
Uint16Array.of(0, 1, 2));
source也可以是一个类似数组的对象:
assert.deepEqual(
Uint16Array.from({0:0, 1:1, 2:2, length: 3}),
Uint16Array.of(0, 1, 2));
可选的mapfn允许我们在source的元素成为结果元素之前对其进行转换。为什么一次性执行两个步骤映射和转换?与通过.map()单独映射相比,有两个优点:
-
不需要中间的数组或类型数组。
-
在将不同精度的类型数组之间进行转换时,出错的可能性更小。
继续阅读以了解第二个优势的解释。
35.5.1.1 陷阱:在转换类型数组时映射
.from()静态方法可以选择性地在类型数组之间进行映射和转换。如果我们使用该方法,出错的可能性会更小。
为了了解为什么是这样,让我们首先将类型数组转换为具有更高精度的类型数组。如果我们使用.from()进行映射,结果将自动正确。否则,我们必须先转换然后映射。
const typedArray = Int8Array.of(127, 126, 125);
assert.deepEqual(
Int16Array.from(typedArray, x => x * 2),
Int16Array.of(254, 252, 250));
assert.deepEqual(
Int16Array.from(typedArray).map(x => x * 2),
Int16Array.of(254, 252, 250)); // OK
assert.deepEqual(
Int16Array.from(typedArray.map(x => x * 2)),
Int16Array.of(-2, -4, -6)); // wrong
如果我们从类型数组转换为具有较低精度的类型数组,通过.from()进行映射会产生正确的结果。否则,我们必须先映射然后转换。
assert.deepEqual(
Int8Array.from(Int16Array.of(254, 252, 250), x => x / 2),
Int8Array.of(127, 126, 125));
assert.deepEqual(
Int8Array.from(Int16Array.of(254, 252, 250).map(x => x / 2)),
Int8Array.of(127, 126, 125)); // OK
assert.deepEqual(
Int8Array.from(Int16Array.of(254, 252, 250)).map(x => x / 2),
Int8Array.of(-1, -2, -3)); // wrong
问题在于,如果我们通过.map()进行映射,则输入类型和输出类型是相同的。相比之下,.from()从任意输入类型映射到我们通过其接收器指定的输出类型。
35.5.2 Typed Arrays 是可迭代的
Typed Arrays 是可迭代的。这意味着我们可以使用for-of循环和其他基于迭代的机制:
const ui8 = Uint8Array.of(0, 1, 2);
for (const byte of ui8) {
console.log(byte);
}
输出:
0
1
2
ArrayBuffer 和 DataViews 是不可迭代的。
35.5.3 将 Typed Arrays 转换为普通数组以及从普通数组转换回来
要将普通数组转换为 Typed Array,我们将其传递给:
-
Typed Array 构造函数接受 Typed Arrays、可迭代值和类似数组的对象。
-
«ElementType»Array.from()接受可迭代值和类似数组的值。
例如:
const ta1 = new Uint8Array([0, 1, 2]);
const ta2 = Uint8Array.from([0, 1, 2]);
assert.deepEqual(ta1, ta2);
要将 Typed Array 转换为普通数组,我们可以使用Array.from()或展开(因为 Typed Arrays 是可迭代的):
assert.deepEqual(
[...Uint8Array.of(0, 1, 2)], [0, 1, 2]
);
assert.deepEqual(
Array.from(Uint8Array.of(0, 1, 2)), [0, 1, 2]
);
35.5.4 将Uint8Array(UTF-8)转换为字符串以及从字符串转换回来
类TextEncoder和TextDecoder不是 ECMAScript 本身的一部分,但所有主要的 JavaScript 平台(浏览器、Node.js 等)都支持它们。因此,我们可以使用它们在Uint8Array和字符串之间进行转换。
将字符串转换为 UTF-8 编码的字节:
const textEncoder = new TextEncoder();
function stringToUtf8(str) {
return textEncoder.encode(str);
}
assert.deepEqual(
stringToUtf8('abc'),
Uint8Array.of(97, 98, 99)
);
将 UTF-8 编码的字节转换为字符串:
const textDecoder = new TextDecoder();
function utf8ToString(bytes) {
return textDecoder.decode(bytes);
}
assert.deepEqual(
utf8ToString(Uint8Array.of(97, 98, 99)),
'abc'
);
35.6 调整 ArrayBuffers(ES2024)
在 ArrayBuffers 变得可调整大小之前,它们具有固定的大小。如果我们想要一个增长或缩小,我们必须分配一个新的,并将旧的复制过来。这会花费时间,并且可以在 32 位系统上破坏地址空间。
35.6.1 ArrayBuffer 的新特性
这些是调整大小引入的变化:
-
现有的构造函数多了一个参数:
new ArrayBuffer(byteLength: number, options?: {maxByteLength?: number}) -
有一个新方法和两个新属性:
-
ArrayBuffer.prototype.resize(newByteLength: number)- 调整 ArrayBuffer 的大小。
-
get ArrayBuffer.prototype.resizable- 返回一个布尔值,表示此 ArrayBuffer 是否可调整大小。
-
get ArrayBuffer.prototype.maxByteLength- 如果在构造函数中提供了
options.maxByteLength,则返回options.maxByteLength。否则,返回this.byteLength。
- 如果在构造函数中提供了
-
-
现有的
.slice()方法始终返回不可调整大小的 ArrayBuffer。
构造函数的options对象决定了 ArrayBuffer 是否可调整大小:
const resizableArrayBuffer = new ArrayBuffer(16, {maxByteLength: 32});
assert.equal(
resizableArrayBuffer.resizable, true
);
const fixedArrayBuffer = new ArrayBuffer(16);
assert.equal(
fixedArrayBuffer.resizable, false
);
35.6.2 如何处理 Typed Arrays 对 ArrayBuffer 大小的变化
Typed Array 的构造函数看起来是这样的:
new «TypedArray»(
buffer: ArrayBuffer | SharedArrayBuffer,
byteOffset?: number,
length?: number
)
如果length是undefined,则 Typed Array 实例的.length和.byteLength会自动跟踪可调整大小的buffer的长度:
const buf = new ArrayBuffer(2, {maxByteLength: 4});
// `tarr1` starts at offset 0 (`length` is undefined)
const tarr1 = new Uint8Array(buf);
// `tarr2` starts at offset 2 (`length` is undefined)
const tarr2 = new Uint8Array(buf, 2);
assert.equal(
tarr1.length, 2
);
assert.equal(
tarr2.length, 0
);
buf.resize(4);
assert.equal(
tarr1.length, 4
);
assert.equal(
tarr2.length, 2
);
如果 ArrayBuffer 被调整大小,则具有固定长度的包装器可能会超出范围:包装器的范围不再由 ArrayBuffer 覆盖。JavaScript 将其视为如果 ArrayBuffer 被断开连接:
-
.length、.byteLength和.byteOffset为零。 -
获取元素返回
undefined。 -
设置元素会被静默忽略。
-
所有与元素相关的方法都会抛出错误。
const buf = new ArrayBuffer(4, {maxByteLength: 4});
const tarr = new Uint8Array(buf, 2, 2);
assert.equal(
tarr.length, 2
);
buf.resize(3);
// `tarr` is now partially out of bounds
assert.equal(
tarr.length, 0
);
assert.equal(
tarr.byteLength, 0
);
assert.equal(
tarr.byteOffset, 0
);
assert.equal(
tarr[0], undefined
);
assert.throws(
() => tarr.at(0),
{
name: 'TypeError',
message: 'Cannot perform %TypedArray%.prototype.at '
+ 'on a detached ArrayBuffer',
}
);
35.6.3 ECMAScript 规范提供的指南
ECMAScript 规范为使用可调整大小的 ArrayBuffer 提供了以下指南:
-
我们建议尽可能在部署环境中测试程序。不同硬件设备之间的可用物理内存差异很大。同样,虚拟内存子系统在硬件设备和操作系统之间也存在很大差异。一个在 64 位桌面网络浏览器上运行且没有内存不足错误的程序,可能在 32 位移动网络浏览器上耗尽内存。
-
当为可调整大小的 ArrayBuffer 选择
maxByteLength选项的值时,我们建议选择应用程序可能的最小大小。我们建议maxByteLength不要超过 1,073,741,824 (2³⁰ 字节或 1 GiB)。 -
请注意,成功构造特定最大大小的可调整大小 ArrayBuffer 并不保证未来的调整大小操作将成功。
35.7 转移和断开 ArrayBuffer^(ES2024)
35.7.1 准备:转移数据和断开连接
网络 API(不是 ECMAScript 标准)长期以来一直支持结构化克隆,以安全地在领域之间移动值(globalThis、iframe、web workers 等)。某些对象也可以被[转移]:在克隆之后,原始对象变为断开连接(不可访问),所有权从原始对象转移到克隆对象。转移通常比复制更快,尤其是涉及大量内存时。这些是最常见的可转移对象类别:
-
ArrayBuffer -
流:
-
ReadableStream -
TransformStream -
WritableStream
-
-
与 DOM 相关的数据:
-
ImageBitmap -
OffscreenCanvas
-
-
杂项通信:
-
MessagePort -
RTCDataChannel
-
35.7.2 与转移和断开连接相关的方法
-
两个方法让我们可以显式地将一个 ArrayBuffer 转移到新对象(我们很快就会看到为什么这很有用):
-
ArrayBuffer.prototype.transfer(newLength?: number) -
ArrayBuffer.prototype.transferToFixedLength(newLength?: number)
-
-
一个获取器告诉我们一个 ArrayBuffer 是否已断开连接:
get ArrayBuffer.prototype.detached
35.7.3 通过 structuredClone() 转移 ArrayBuffer
广泛支持的structuredClone()也允许我们转移(因此断开)ArrayBuffer:
const original = new ArrayBuffer(16);
const clone = structuredClone(original, {transfer: [original]});
assert.equal(
original.byteLength, 0
);
assert.equal(
clone.byteLength, 16
);
assert.equal(
original.detached, true
);
assert.equal(
clone.detached, false
);
ArrayBuffer 的 .transfer() 方法仅仅提供了一个更简洁的方式来断开 ArrayBuffer:
const original = new ArrayBuffer(16);
const transferred = original.transfer();
assert.equal(
original.detached, true
);
assert.equal(
transferred.detached, false
);
35.7.4 在同一代理内传输 ArrayBuffer
转移通常用于两个 代理(代理可以是主线程或 Web Worker)之间。然而,在同一代理内进行转移也是有意义的:如果函数将(可能共享的)ArrayBuffer 作为参数传递,则可以将其转移,这样外部代码就无法干扰其操作。示例(取自 ECMAScript 提案 并稍作编辑):
async function validateAndWriteSafeAndFast(arrayBuffer) {
const owned = arrayBuffer.transfer();
// We have `owned` and no one can access its data via
// `arrayBuffer` now because the latter is detached:
assert.equal(
arrayBuffer.detached, true
);
// `await` pauses this function – which gives external
// code the opportunity to access `arrayBuffer`.
await validate(owned);
await fs.writeFile("data.bin", owned);
}
35.7.5 解除 ArrayBuffer 的连接会如何影响其包装器?
35.7.5.1 解除连接的 ArrayBuffer 的类型化数组
准备:
> const arrayBuffer = new ArrayBuffer(16);
> const typedArray = new Uint8Array(arrayBuffer);
> arrayBuffer.transfer();
长度和偏移量都是零:
> typedArray.length
0
> typedArray.byteLength
0
> typedArray.byteOffset
0
获取元素返回 undefined;设置元素会静默失败:
> typedArray[0]
undefined
> typedArray[0] = 128
128
所有与元素相关的都会抛出异常:
> typedArray.at(0)
TypeError: Cannot perform %TypedArray%.prototype.at
on a detached ArrayBuffer
35.7.5.2 解除连接的 ArrayBuffer 的 DataView
DataView 的所有数据相关方法都会抛出:
> const arrayBuffer = new ArrayBuffer(16);
> const dataView = new DataView(arrayBuffer);
> arrayBuffer.transfer();
> dataView.byteLength
TypeError: Cannot perform get DataView.prototype.byteLength
on a detached ArrayBuffer
> dataView.getUint8(0)
TypeError: Cannot perform DataView.prototype.getUint8
on a detached ArrayBuffer
35.7.5.3 我们无法使用解除连接的 ArrayBuffer 创建新的包装器
> const arrayBuffer = new ArrayBuffer(16);
> arrayBuffer.transfer();
> new Uint8Array(arrayBuffer)
TypeError: Cannot perform Construct on a detached ArrayBuffer
> new DataView(arrayBuffer)
TypeError: Cannot perform DataView constructor on a detached ArrayBuffer
35.7.6 ArrayBuffer.prototype.transferToFixedLength()
此方法完善了 API:它将可调整大小的 ArrayBuffer 转换为固定长度的 ArrayBuffer。这可能会释放为增长而保留的内存。
35.8 快速参考:索引与偏移量
在准备 ArrayBuffer、类型化数组和 DataView 的快速参考之前,我们需要了解索引和偏移量之间的区别:
-
方括号运算符
[ ]的索引:我们只能使用非负索引(从 0 开始)。在正常数组中,写入负索引会创建属性:
const arr = [6, 7]; arr[-1] = 5; assert.deepEqual( Object.keys(arr), ['0', '1', '-1']);在类型化数组中,写入负索引会被忽略:
const tarr = Uint8Array.of(6, 7); tarr[-1] = 5; assert.deepEqual( Object.keys(tarr), ['0', '1']); -
ArrayBuffer、类型化数组和 DataView 方法的索引:每个索引都可以是负数。如果是,则将其添加到实体的长度以产生实际索引。因此,
-1指的是最后一个元素,-2指的是倒数第二个,等等。正常数组的方法也是这样工作的。const ui8 = Uint8Array.of(0, 1, 2); assert.deepEqual(ui8.slice(-1), Uint8Array.of(2)); -
传递给类型化数组和 DataView 方法的偏移量:必须是非负数——例如:
const dataView = new DataView(new ArrayBuffer(4)); assert.throws( () => dataView.getUint8(-1), { name: 'RangeError', message: 'Offset is outside the bounds of the DataView', });
是否是索引或偏移量只能通过查看文档来确定;没有简单的规则。
35.9 快速参考:ArrayBuffer
ArrayBuffer 存储二进制数据,这些数据应通过类型化数组和 DataView 访问。
35.9.1 new ArrayBuffer()
-
new ArrayBuffer(byteLength, options?)ES6new ArrayBuffer( byteLength: number, options?: { // ES2024 maxByteLength?: number } )通过
new调用此构造函数会创建一个容量为length字节的实例。每个字节最初都是 0。如果提供了
options.maxByteLength,则 ArrayBuffer 可以调整大小。否则,它具有固定长度。
35.9.2 ArrayBuffer.*
-
ArrayBuffer.isView(arg)ES6如果
arg是ArrayBuffer的视图(即,如果它是类型化数组或DataView),则返回true。> ArrayBuffer.isView(new Uint8Array()) true > ArrayBuffer.isView(new DataView(new ArrayBuffer())) true
35.9.3 ArrayBuffer.prototype.*:获取和切片
-
get ArrayBuffer.prototype.byteLengthES6返回此
ArrayBuffer的字节容量。 -
ArrayBuffer.prototype.slice(startIndex=0, endIndex=this.byteLength)ES6创建一个新的
ArrayBuffer,它包含此ArrayBuffer中索引大于或等于startIndex且小于endIndex的字节。start和endIndex可以是负数(参见“快速参考:索引与偏移量”(§35.8))。
35.9.4 ArrayBuffer.prototype.*:调整大小
-
ArrayBuffer.prototype.resize(newByteLength)ES2024改变此
ArrayBuffer的大小。更多信息,请参见“调整ArrayBuffer大小”(§35.6)。 -
get ArrayBuffer.prototype.resizableES2024如果此
ArrayBuffer可调整大小,则返回true;如果不可以,则返回false。 -
get ArrayBuffer.prototype.maxByteLengthES2024如果在构造函数中提供了
options.maxByteLength,则返回options.maxByteLength。否则,返回this.byteLength。
35.10 快速参考:类型化数组
类型化数组对象的属性分两步介绍:
-
TypedArray:首先,我们来看所有类型化数组类的抽象超类(在本书开头的类图中已展示)。这个超类称为TypedArray,但在 JavaScript 中没有全局名称:> Object.getPrototypeOf(Uint8Array).name 'TypedArray' -
«ElementType»Array:具体的类型化数组类称为Uint8Array、Int16Array、Float16Array等。这些是我们通过new、.of和.from()使用的类。
35.10.1 TypedArray.*
静态TypedArray方法由其子类(Uint8Array等)继承。因此,我们可以通过子类使用这些方法,这些子类是具体的,可以直接实例化。
-
TypedArray.from(iterableOrArrayLike, mapFunc?)ES6// BigInt64Array: bigint instead of number TypedArray.from<T>( iterableOrArrayLike: Iterable<number> | ArrayLike<number> ): TypedArray<T> TypedArray.from<S, T>( iterableOrArrayLike: Iterable<S> | ArrayLike<S>, mapFunc: (v: S, k: number) => T, thisArg?: any ): TypedArray<T>将可迭代对象(包括数组和类型化数组)或类似数组的对象转换为类型化数组类的实例。
assert.deepEqual( Uint16Array.from([0, 1, 2]), Uint16Array.of(0, 1, 2));可选的
mapFunc允许我们在source元素成为结果元素之前对其进行转换。assert.deepEqual( Int16Array.from(Int8Array.of(127, 126, 125), x => x * 2), Int16Array.of(254, 252, 250)); -
TypedArray.of(...items)ES6// BigInt64Array: bigint instead of number TypedArray.of<T>( ...items: Array<number> ): TypedArray<T>创建一个类型化数组类的新实例,其元素是
items(强制转换为元素类型)。assert.deepEqual( Int16Array.of(-1234, 5, 67), new Int16Array([-1234, 5, 67]) );
35.10.2 TypedArray.prototype.*
类型化数组方法接受的索引可以是负数(它们以这种方式像传统的数组方法一样工作)。偏移量必须是正数。有关详细信息,请参见“快速参考:索引与偏移量”(§35.8)。
35.10.2.1 特定于类型化数组的属性
以下属性特定于类型化数组;普通数组没有这些属性:
-
get TypedArray.prototype.bufferES6返回支持此 Typed Array 的 ArrayBuffer。
-
获取 TypedArray.prototype.lengthES6返回此 Typed Array 缓冲区的元素长度。
> new Uint32Array(new ArrayBuffer(4)).length 1 -
获取 TypedArray.prototype.byteLengthES6返回此 Typed Array 缓冲区的字节大小。
> new Uint32Array(new ArrayBuffer(4)).byteLength 4 -
获取 TypedArray.prototype.byteOffsetES6返回此 Typed Array 在其 ArrayBuffer 中的起始偏移量。
-
TypedArray.prototype.set(typedArrayOrArrayLike, offset=0)(ES6)将第一个参数的所有元素复制到这个 Typed Array 中。参数的索引 0 处的元素写入此 Typed Array 的
offset索引(等等)。有关类似数组的更多信息,请参阅“类似数组的对象”(§34.5)。 -
TypedArray.prototype.subarray(startIndex=0, endIndex=this.length)ES6返回一个新的 Typed Array,它具有与这个 Typed Array 相同的缓冲区,但范围(通常)更小。如果
startIndex为非负数,则结果 Typed Array 的第一个元素是this[startIndex],第二个是this[startIndex+1](等等)。如果startIndex为负数,则相应地转换。
35.10.2.2 数组方法
以下方法基本上与普通数组的 方法相同(ECMAScript 版本指定了方法何时添加到数组中 - 在 ES6 之前,Typed Arrays 不存在于 ECMAScript 中):
-
TypedArray.prototype.at(index)(ES2022, R) -
TypedArray.prototype.copyWithin(target, start, end=this.length)(ES6, W) -
TypedArray.prototype.entries()(ES6, R) -
TypedArray.prototype.every(predicate, thisArg?)(ES5, R) -
TypedArray.prototype.fill(start=0, end=this.length)(ES6, W) -
TypedArray.prototype.filter(predicate, thisArg?)(ES5, R) -
TypedArray.prototype.find(predicate, thisArg?)(ES6, R) -
TypedArray.prototype.findIndex(predicate, thisArg?)(ES6, R) -
TypedArray.prototype.findLast(predicate, thisArg?)(ES2023, R) -
TypedArray.prototype.findLastIndex(predicate, thisArg?)(ES2023, R) -
TypedArray.prototype.forEach(callback)(ES5, R) -
TypedArray.prototype.includes(searchElement, fromIndex)(ES2016, R) -
TypedArray.prototype.indexOf(searchElement, fromIndex)(ES5, R) -
TypedArray.prototype.join(separator = ',')(ES1, R) -
TypedArray.prototype.keys()(ES6, R) -
TypedArray.prototype.lastIndexOf(searchElement, fromIndex)(ES5, R) -
TypedArray.prototype.map(callback, thisArg?)(ES5, R) -
TypedArray.prototype.reduce(callback, initialValue?)(ES5, R) -
TypedArray.prototype.reduceRight(callback, initialValue?)(ES5, R) -
TypedArray.prototype.reverse()(ES1, W) -
TypedArray.prototype.slice(start?, end?)(ES3, R) -
TypedArray.prototype.some(predicate, thisArg?)(ES5, R) -
TypedArray.prototype.sort(compareFunc?)(ES1, W) -
TypedArray.prototype.toLocaleString()(ES3, R) -
TypedArray.prototype.toReversed()(ES2023, R) -
TypedArray.prototype.toSorted(compareFunc?)(ES2023, R) -
TypedArray.prototype.toSpliced(start?, deleteCount?, ...items)(ES2023, R) -
TypedArray.prototype.toString()(ES1, R) -
TypedArray.prototype.values()^ (ES6, R) -
TypedArray.prototype.with(index, value)^ (ES2023, R)
关于这些方法如何工作的详细信息,请参阅“快速参考:Array” (§34.18)。
35.10.3 new «ElementType»Array()
每个类型化数组构造函数都有一个名称,遵循 «ElementType»Array 的模式,其中 «ElementType» 是 表 35.1 中列出的元素类型之一。这意味着有 12 个类型化数组构造函数:
-
Int8Array,Uint8Array,Uint8ClampedArray -
Int16Array,Uint16Array -
Int32Array,Uint32Array -
BigInt64Array,BigUint64Array -
Float16Array,Float32Array,Float64Array
每个构造函数都有几个 重载 版本 – 它的行为取决于它接收到的参数数量和它们的类型:
-
new «ElementType»Array(length=0)创建一个新的
«ElementType»Array,具有给定的length和适当的缓冲区。缓冲区的大小(以字节为单位):length * «ElementType»Array.BYTES_PER_ELEMENT -
new «ElementType»Array(source: TypedArray)创建一个新的
«ElementType»Array实例,其元素与source的元素具有相同的值,但被强制转换为ElementType。 -
new «ElementType»Array(source: Iterable<number>)-
BigInt64Array,BigUint64Array: 使用bigint而不是number -
创建一个新的
«ElementType»Array实例,其元素与source的项具有相同的值,但被强制转换为ElementType。有关迭代器的更多信息,请参阅“同步迭代 (ES6)” (§32)。
-
-
new «ElementType»Array(source: ArrayLike<number>)-
BigInt64Array,BigUint64Array: 使用bigint而不是number -
创建一个新的
«ElementType»Array实例,其元素与source的元素具有相同的值,但被强制转换为ElementType。有关类似数组的更多信息,请参阅“类似数组的对象” (§34.5)。
-
-
new «ElementType»Array(buffer: ArrayBuffer, byteOffset=0, length=0)创建一个新的
«ElementType»Array,其缓冲区为buffer。它从给定的byteOffset开始访问缓冲区,并将具有给定的length。注意,length计算的是类型化数组(每个元素 1-8 字节)的元素数量,而不是字节数。
35.10.4 «ElementType»Array.*
-
«ElementType»Array.BYTES_PER_ELEMENT: number计算存储单个元素所需的字节数:
> Uint8Array.BYTES_PER_ELEMENT 1 > Int16Array.BYTES_PER_ELEMENT 2 > Float64Array.BYTES_PER_ELEMENT 8
35.10.5 «ElementType»Array.prototype.*
-
«ElementType»Array.prototype.BYTES_PER_ELEMENT: number与
«ElementType»Array.BYTES_PER_ELEMENT相同。
35.11 快速参考:DataViews
35.11.1 new DataView()
-
new DataView(arrayBuffer, byteOffset?, byteLength?)ES6创建一个新的
DataView,其数据存储在buffer的 ArrayBuffer 中。默认情况下,新的DataView可以访问buffer的全部内容。最后两个参数允许我们更改这一点。
35.11.2 DataView.prototype.*
在本节的剩余部分,«ElementType» 指的是以下两种类型之一:
-
Int8,Uint8 -
Int16,Uint16 -
Int32,Uint32 -
BigInt64,BigUint64 -
Float16,Float32,Float64
这些是 DataView.prototype 的属性:
-
get DataView.prototype.bufferES6返回此 DataView 的 ArrayBuffer。
-
get DataView.prototype.byteLengthES6返回此 DataView 可以访问的字节数。
-
get DataView.prototype.byteOffsetES6返回此 DataView 从其缓冲区开始访问字节的偏移量。
-
DataView.prototype.get«ElementType»(byteOffset, littleEndian=false)ES6返回:
-
BigInt64,BigUint64:bigint -
所有其他元素类型:
number
从此 DataView 的缓冲区中读取一个值。
-
-
DataView.prototype.set«ElementType»(byteOffset, value, littleEndian=false)ES6value的类型:-
BigInt64,BigUint64:bigint -
所有其他元素类型:
number
将
value写入此 DataView 的缓冲区。 -
36 Maps (Map) ES6
-
36.1 使用 Maps
-
36.1.1 创建 Maps
-
36.1.2 处理单个条目
-
36.1.3 确定 Maps 的大小并清除它
-
36.1.4 示例:计数字符
-
-
36.2 在 Map 上迭代
-
36.2.1 在 Map 上循环
-
36.2.2 按插入顺序列出:条目、键、值
-
-
36.3 从 Maps 转换到和从 Maps 转换
-
36.3.1 将键、值、条目转换为 Arrays
-
36.3.2 在 Maps 和 Objects 之间转换
-
-
36.4 处理 Maps
-
36.4.1 复制 Maps
-
36.4.2 将多个 Maps 合并成一个单一 Map
-
36.4.3 通过迭代器方法映射和过滤 Maps(ES2025)
-
-
36.5 关于 Maps 键的一些更多细节(高级)
- 36.5.1 哪些键被认为是相等的?
-
36.6 快速参考:
Map-
36.6.1
new Map() -
36.6.2
Map.* -
36.6.3
Map.prototype.*:处理单个条目 -
36.6.4
Map.prototype:处理所有条目 -
36.6.5
Map.prototype:迭代和循环
-
-
36.7 FAQ: Maps
-
36.7.1 应该何时使用 Map,何时使用对象?
-
36.7.2 当何时在 Map 中使用对象作为键?
-
36.7.3 为什么 Maps 保留条目的插入顺序?
-
36.7.4 为什么 Maps 有
.size,而 Arrays 有.length?
-
在 ES6 之前,JavaScript 没有字典数据结构,而是将对象从字符串到任意值作为字典(滥用)使用。ES6 引入了 Maps,它是一种从任意值到任意值的字典。
36.1 使用 Maps
Map 的一个实例将键映射到值。单个键值映射称为 条目。
36.1.1 创建 Maps
创建 Maps 有三种常见方式。
首先,我们可以使用无参数的构造函数来创建一个空的 Map:
const emptyMap = new Map();
assert.equal(emptyMap.size, 0);
其次,我们可以将可迭代的(例如,一个数组)传递给构造函数,作为键值“对”(包含两个元素的数组):
const map = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three'], // trailing comma is ignored
]);
第三,.set() 方法向 Map 添加条目,并且是可链式的:
const map = new Map()
.set(1, 'one')
.set(2, 'two')
.set(3, 'three')
;
36.1.2 处理单个条目
map.set() 和 map.get() 用于写入和读取值(给定键)。
const map = new Map();
map.set('foo', 123);
assert.equal(map.get('foo'), 123);
// Unknown key:
assert.equal(map.get('bar'), undefined);
// Use the default value '' if an entry is missing:
assert.equal(map.get('bar') ?? '', '');
map.has() 检查 Map 是否包含具有给定键的条目。map.delete() 删除条目。
const map = new Map([['foo', 123]]);
assert.equal(map.has('foo'), true);
assert.equal(map.delete('foo'), true)
assert.equal(map.has('foo'), false)
36.1.3 确定 Map 的大小和清除它
map.size 包含 Map 中的条目数量。map.clear() 删除 Map 中的所有条目。
const map = new Map()
.set('foo', true)
.set('bar', false)
;
assert.equal(map.size, 2)
map.clear();
assert.equal(map.size, 0)
36.1.4 示例:计数字符
countChars() 返回一个将字符映射到出现次数的 Map。
function countChars(chars) {
const charCounts = new Map();
for (let ch of chars) {
ch = ch.toLowerCase();
const prevCount = charCounts.get(ch) ?? 0;
charCounts.set(ch, prevCount+1);
}
return charCounts;
}
const result = countChars('AaBccc');
assert.deepEqual(
countChars('AaBccc'),
new Map([
['a', 2],
['b', 1],
['c', 3],
])
);
36.2 遍历 Map
这就是 Map 如何支持迭代的方式:
-
new Map()接受一个键值对的可迭代。这将在我们查看处理 Map 和从 Map 转换时很有用。 -
以下方法返回可迭代的迭代器——其方法将在我们查看处理 Map 时很有用。
-
map.keys():map的键 -
map.values():map的值 -
map.entries():map的条目(键值对) -
map[Symbol.iterator](): 与map.entries()相同- 此方法使
Map的实例可迭代。
- 此方法使
-
36.2.1 遍历 Map
Map 可以通过键值对进行迭代。这是遍历它们的一种常见方式:
const map = new Map()
.set(false, 'no')
.set(true, 'yes')
;
for (const [key, value] of map) { // (A)
console.log(JSON.stringify(key) + ' = ' + JSON.stringify(value));
}
在行 A 中,我们使用解构来访问迭代器返回的键值对的组件。
输出:
false = "no"
true = "yes"
以下示例演示了如何遍历由方法 .keys() 返回的可迭代迭代器:
for (const key of map.keys()) {
console.log(JSON.stringify(key));
}
输出:
false
true
36.2.2 按插入顺序列出:条目、键、值
Map 记录条目创建的顺序,并在列出键、值或条目时尊重该顺序:
const map1 = new Map([
['a', 1],
['b', 2],
]);
for (const key of map1.keys()) {
console.log(JSON.stringify(key));
}
输出:
"a"
"b"
const map2 = new Map([
['b', 2],
['a', 1],
]);
for (const key of map2.keys()) {
console.log(JSON.stringify(key));
}
输出:
"b"
"a"
按插入顺序列出内容的 Map 有两个好处:
-
当插入顺序很重要时,我们可以使用 Map。
-
在测试中,结果变得更加确定,更容易检查。
36.3 从和到 Map 的转换
36.3.1 将键、值、条目转换为数组
一方面,.keys()、.values() 和 .entries() 返回的值是可迭代的——这使我们能够使用 Array.from():
const map = new Map()
.set(false, 'no')
.set(true, 'yes')
;
assert.deepEqual(
Array.from(map.keys()),
[false, true]
);
assert.deepEqual(
Array.from(map.values()),
['no', 'yes']
);
assert.deepEqual(
Array.from(map.entries()),
[
[false, 'no'],
[true, 'yes'],
]
);
另一方面,.keys()、.values() 和 .entries() 返回的值也是迭代器——这使我们能够使用迭代器方法 .toArray():
const map = new Map()
.set(false, 'no')
.set(true, 'yes')
;
assert.deepEqual(
map.keys().toArray(),
[false, true]
);
assert.deepEqual(
map.values().toArray(),
['no', 'yes']
);
assert.deepEqual(
map.entries().toArray(),
[
[false, 'no'],
[true, 'yes'],
]
);
36.3.2 在 Map 和对象之间转换
只要 Map 只使用字符串和符号作为键,我们就可以将其转换为对象(通过 Object.fromEntries()):
const map = new Map([
['a', 1],
['b', 2],
]);
const obj = Object.fromEntries(map);
assert.deepEqual(
obj, {a: 1, b: 2}
);
我们还可以通过 Object.entries() 将对象转换为具有字符串或符号键的 Map:
const obj = {
a: 1,
b: 2,
};
const map = new Map(Object.entries(obj));
assert.deepEqual(
map, new Map([['a', 1], ['b', 2]])
);
36.4 处理 Map
36.4.1 复制 Map
正如我们所见,Map 可以通过键值对进行迭代。因此,我们可以使用构造函数来创建一个 Map 的副本。这个副本是浅拷贝:键和值是相同的;它们自身没有被复制/克隆。
const original = new Map()
.set(false, 'no')
.set(true, 'yes')
;
const copy = new Map(original);
assert.deepEqual(original, copy);
36.4.2 将多个 Map 合并为单个 Map
没有合并 Maps 的方法,这就是为什么我们必须使用一种解决方案。让我们合并以下两个 Maps:
const map1 = new Map()
.set(1, '1a')
.set(2, '1b')
.set(3, '1c')
;
const map2 = new Map()
.set(2, '2b')
.set(3, '2c')
.set(4, '2d')
;
要合并 map1 和 map2,我们创建一个新的数组,并将 map1 和 map2 的条目(键值对)展开到其中(通过迭代)。然后我们将数组转换回 Map。所有这些都在行 A 中完成:
const combinedMap = new Map([...map1, ...map2]); // (A)
assert.deepEqual(
Array.from(combinedMap), // convert to Array for comparison
[
[ 1, '1a' ],
[ 2, '2b' ],
[ 3, '2c' ],
[ 4, '2d' ],
]
);
练习:合并 Maps
exercises/maps/combine_maps_test.mjs
36.4.3 通过迭代器方法映射和过滤 Maps (ES2025)
我们可以对数组进行 .map() 和 .filter() 操作,但对于 Map 没有这样的操作。解决方案是:
-
将 Map 转换为 [键,值] 对的迭代器。
-
Map 或过滤迭代器。
-
将结果转换回 Map。
我们将使用以下 Map 来探索它是如何工作的。
const originalMap = new Map()
.set(1, 'a')
.set(2, 'b')
.set(3, 'c')
;
映射 originalMap:
const mappedMap = new Map( // step 3
originalMap.entries() // step 1
.map(([k, v]) => [k * 2, '_' + v]) // step 2
);
assert.deepEqual(
mappedMap,
new Map([[2,'_a'], [4,'_b'], [6,'_c']])
);
过滤 originalMap:
const filteredMap = new Map( // step 3
originalMap.entries() // step 1
.filter(([k, v]) => k < 3) // step 2
);
assert.deepEqual(
filteredMap,
new Map([[1,'a'], [2,'b']])
);
如果我们不能使用迭代器方法怎么办?那么我们可以通过
-
替换
originalMap.entries() -
使用
Array.from(originalMap)。
36.5 关于 Maps 键的一些更多细节(高级)
任何值都可以作为键,甚至是一个对象:
const map = new Map();
const KEY1 = {};
const KEY2 = {};
map.set(KEY1, 'hello');
map.set(KEY2, 'world');
assert.equal(map.get(KEY1), 'hello');
assert.equal(map.get(KEY2), 'world');
36.5.1 哪些键被认为是相等的?
大多数 Map 操作都需要检查一个值是否等于键之一。它们通过内部操作 SameValueZero 来这样做,它的工作方式类似于 ===,但将 NaN 视为等于自身。
因此,我们可以像任何其他值一样在 Maps 中使用 NaN 作为键:
> const map = new Map();
> map.set(NaN, 123);
> map.get(NaN)
123
不同的对象始终被认为是不同的。这是无法改变的事情(尚无法改变 - 配置键相等性是 TC39 长期路线图上的一个项目)。
> new Map().set({}, 1).set({}, 2).size
2
36.6 快速参考:Map
注意:为了简洁起见,我假设所有键都具有相同的类型 K,并且所有值都具有相同的类型 V。
36.6.1 new Map()
-
new Map(entries?)ES6new Map<K, V>( entries?: Iterable<[K, V]> )如果我们不提供参数
entries,则创建一个空 Map。如果我们提供 [键,值] 对的可迭代对象,则将这些对作为条目添加到 Map 中。例如:const map = new Map([ [ 1, 'one' ], [ 2, 'two' ], [ 3, 'three' ], // trailing comma is ignored ]);
36.6.2 Map.*
-
Map.groupBy(items, computeGroupKey)ES2024Map.groupBy<K, T>( items: Iterable<T>, computeGroupKey: (item: T, index: number) => K, ): Map<K, Array<T>>;-
回调
computeGroupKey为每个items返回一个 分组键。 -
Map.groupBy()的结果是包含以下内容的 Map:-
每个条目的键是一个分组键,
-
其值是一个包含所有具有该分组键的项的数组。
-
assert.deepEqual( Map.groupBy( ['orange', 'apricot', 'banana', 'apple', 'blueberry'], (str) => str[0] // compute group key ), new Map() .set('o', ['orange']) .set('a', ['apricot', 'apple']) .set('b', ['banana', 'blueberry']) ); -
36.6.3 Map.prototype.*:处理单个条目
-
Map.prototype.get(key)ES6返回
key在此 Map 中映射到的value。如果没有在此 Map 中找到键key,则返回undefined。const map = new Map([[1, 'one'], [2, 'two']]); assert.equal(map.get(1), 'one'); assert.equal(map.get(5), undefined); -
Map.prototype.set(key, value)ES6-
将给定的键映射到给定的值。
-
如果已经存在一个键为
key的条目,则它将被更新。否则,将创建一个新的条目。 -
此方法返回
this,这意味着我们可以将其链接。
const map = new Map([[1, 'one'], [2, 'two']]); map.set(1, 'ONE!') // update an existing entry .set(3, 'THREE!') // create a new entry ; assert.deepEqual( Array.from(map.entries()), [[1, 'ONE!'], [2, 'two'], [3, 'THREE!']] ); -
-
Map.prototype.has(key)ES6返回给定的键是否存在于此 Map 中。
const map = new Map([[1, 'one'], [2, 'two']]); assert.equal(map.has(1), true); // key exists assert.equal(map.has(5), false); // key does not exist -
Map.prototype.delete(key)ES6如果存在一个键为
key的条目,则该条目被移除并返回true。否则,不发生任何操作并返回false。const map = new Map([[1, 'one'], [2, 'two']]); assert.equal(map.delete(1), true); assert.equal(map.delete(5), false); // nothing happens assert.deepEqual( Array.from(map.entries()), [[2, 'two']] );
36.6.4 Map.prototype:处理所有条目
-
get Map.prototype.sizeES6返回此 Map 有多少条目。
const map = new Map([[1, 'one'], [2, 'two']]); assert.equal(map.size, 2); -
Map.prototype.clear()ES6从此 Map 中移除所有条目。
const map = new Map([[1, 'one'], [2, 'two']]); assert.equal(map.size, 2); map.clear(); assert.equal(map.size, 0);
36.6.5 Map.prototype:迭代和循环
在 Map 中迭代和循环发生时,顺序与条目添加到 Map 中的顺序一致。
-
Map.prototype.entries()ES6返回一个包含此 Map 中每个条目的一个 [键,值] 对的可迭代对象。对是长度为 2 的数组。
const map = new Map([[1, 'one'], [2, 'two']]); for (const entry of map.entries()) { console.log(entry); }输出:
[ 1, 'one' ] [ 2, 'two' ] -
Map.prototype.forEach(callback, thisArg?)ES6Map.prototype.forEach( callback: (value: V, key: K, theMap: Map<K,V>) => void, thisArg?: any ): void-
第一个参数是一个回调函数,它为 Map 中的每个条目调用一次。
-
如果提供了
thisArg,则对于每次调用,this被设置为它。否则,this被设置为undefined。
const map = new Map([[1, 'one'], [2, 'two']]); map.forEach((value, key) => console.log(value, key));输出:
one 1 two 2 -
-
Map.prototype.keys()ES6返回一个遍历此 Map 中所有键的可迭代对象。
const map = new Map([[1, 'one'], [2, 'two']]); for (const key of map.keys()) { console.log(key); }输出:
1 2 -
Map.prototype.values()ES6返回一个遍历此 Map 中所有值的可迭代对象。
const map = new Map([[1, 'one'], [2, 'two']]); for (const value of map.values()) { console.log(value); }输出:
one two -
Map.prototype[Symbol.iterator]()ES6遍历 Maps 的默认方式。与
map.entries()相同。const map = new Map([[1, 'one'], [2, 'two']]); for (const [key, value] of map) { console.log(key, value); }输出:
1 one 2 two
36.7 FAQ: Maps
36.7.1 我应该在何时使用 Map,何时使用对象?
如果我们需要一个类似字典的数据结构,其键既不是字符串也不是符号,我们别无选择:我们必须使用 Map。
然而,如果我们的键是字符串或符号,我们必须决定是否使用对象。一个粗略的一般性指南是:
-
是否有一个固定的键集合(在开发时已知)?
然后使用一个对象
obj并通过固定的键访问值:const value = obj.key; -
键的集合在运行时可以改变吗?
然后使用一个 Map
map并通过存储在变量中的键访问值:const theKey = 123; map.get(theKey);
36.7.2 当我应该使用对象作为 Map 的键时?
我们通常希望 Map 键通过值进行比较(如果两个键的内容相同,则认为它们相等)。这排除了对象。然而,对象作为键有一个用例:将数据外部附加到对象上。但这个用例更适合 WeakMaps,其中条目不会阻止键被垃圾回收(有关详细信息,请参阅下一章)。
36.7.3 为什么 Maps 保留条目的插入顺序?
原则上,Map 条目是无序的。排序条目的主要原因是为了使列出条目、键或值的操作是确定的。这有助于,例如,测试。
36.7.4 为什么 Maps 有 .size,而 Arrays 有 .length?
在 JavaScript 中,可索引序列(如数组和字符串)有 .length,而不索引集合(如 Maps 和 Sets)有 .size:
-
.length基于索引;它始终是最高索引加一。 -
.size计算集合中元素的数量。
37 WeakMap (WeakMap) ES6 (高级)
-
37.1 WeakMap 与 Map 有何不同?
-
37.2 WeakMap 是黑盒
-
37.3 WeakMap 的键是弱持有的
-
37.3.1 WeakMap 中可以作为键的值有哪些?
-
37.3.2 为什么符号作为 WeakMap 键有趣?^(ES2023)
-
-
37.4 WeakMap 的用例:将值附加到对象上
-
37.4.1 示例:缓存计算结果
-
37.4.2 示例:在 WeakMap 中保持对象私有数据
-
-
37.5 快速参考:
WeakMap
37.1 WeakMap 与 Map 有何不同?
WeakMap 与 Map 类似,但有以下区别:
-
它们是黑盒,只有当我们同时拥有 WeakMap 和键时,才能访问值。
-
WeakMap 的键是弱持有的:如果一个值是 WeakMap 的键,它仍然可以被垃圾回收。这使两个重要的用例成为可能:
-
我们可以将数据附加到我们拥有的值上——例如,缓存计算结果。
-
我们可以通过不公开包含该部分的 WeakMap 来保持部分值的私有性。
-
下两个部分将更详细地探讨这意味着什么。
37.2 WeakMap 是黑盒
无法检查 WeakMap 内部的内容:
-
例如,我们无法对键、值或条目进行迭代或循环。我们也不能计算大小。
-
此外,我们也不能清除 WeakMap,我们必须创建一个新的实例。
这些限制使得具有安全性属性。引用 Mark Miller:
只有同时拥有 weakmap 和键的人才能观察或影响 weakmap/key 对值映射。使用
clear(),只有 WeakMap 的人原本能够影响 WeakMap 和键到值的映射。
37.3 WeakMap 的键是弱持有的
WeakMap 的键被称为弱持有:通常情况下,如果一个对象引用了另一个对象,那么后一个对象只要前一个对象存在,就不能被垃圾回收。使用 WeakMap,情况就不同了:如果一个对象是键并且没有其他引用,它可以在 WeakMap 存在的情况下被垃圾回收。这也导致相应的条目被移除(但无法观察到这一点)。
37.3.1 WeakMap 中可以作为键的值有哪些?
WeakMap 中可以作为键的值在 ECMAScript 规范中有记录,通过规范函数 CanBeHeldWeakly() 实现(tc39.es/ecma262/#sec-canbeheldweakly):
-
对象 (ES6)
-
符号^(ES2023)——只要它们没有被注册(通过
Symbol.for()创建)
所有的键有一个共同点——它们有身份语义:
-
当通过
===比较时,如果两个键具有相同的身份,则认为它们相等——它们不是通过比较它们的内容(它们的值)来比较的。这意味着永远不会有两个或更多不同的键(“不同”意味着“在内存中的不同位置”)都被认为是相等的。每个键都是唯一的。 -
它们被垃圾回收了。
这两个条件都很重要,这样 WeakMaps 才能在键消失时销毁条目,避免内存泄漏。
让我们看看例子:
-
非注册符号可以用作 WeakMap 键:它们是原始类型,但它们通过身份比较,并且会被垃圾回收。
-
以下两种类型的值不能用作 WeakMap 键:
// Get a symbol from the registry const mySymbol = Symbol.for('key-in-symbol-registry'); assert.equal( // Retrieve that symbol again Symbol.for('key-in-symbol-registry'), mySymbol );
37.3.2 为什么将符号作为 WeakMap 键有趣?^(ES2023)
符号作为 WeakMap 键解决了即将到来的 JavaScript 功能的重要问题:
-
我们可以在记录和元组中放置对象的引用。
-
我们可以在ShadowRealms中传递对象的引用。
37.4 WeakMaps 的使用场景:附加值到对象
我们可以使用 WeakMaps 来外部附加值到对象上——例如:
const externalId = new WeakMap();
{
const obj = {};
externalId.set(obj, 'x3cdw5am'); // (A)
assert.equal(
externalId.get(obj), // (B)
'x3cdw5am'
);
}
// (C)
-
在行 A 中,我们为
obj设置了一个外部 ID。 -
在行 B 中,我们获取
obj的外部 ID。 -
在行 C 中,即使
obj是数据结构externalId中的键,它也可以被垃圾回收。
从某种意义上说,我们为obj创建了一个属性,但将其外部存储。如果externalId是一个属性,则之前的代码将如下所示:
{
const obj = {};
obj.externalId = 'x3cdw5am';
assert.equal(
obj.externalId,
'x3cdw5am'
);
}
37.4.1 示例:缓存计算结果
使用 WeakMaps,我们可以将之前计算的结果与对象关联起来,而无需担心内存管理。以下函数countOwnKeys()是一个例子:它在 WeakMap cache中缓存了之前的结果。
const cache = new WeakMap();
function countOwnKeys(obj) {
if (cache.has(obj)) {
return [cache.get(obj), 'cached'];
} else {
const count = Object.keys(obj).length;
cache.set(obj, count);
return [count, 'computed'];
}
}
如果我们使用这个函数与对象obj一起使用,我们可以看到结果只对第一次调用进行了计算,而第二次调用则使用了缓存的值:
> const obj = { foo: 1, bar: 2};
> countOwnKeys(obj)
[2, 'computed']
> countOwnKeys(obj)
[2, 'cached']
37.4.2 示例:在 WeakMaps 中保持对象数据私有
在以下代码中,WeakMaps _counter和_action被用来存储Countdown实例的虚拟属性值。
const _counter = new WeakMap();
const _action = new WeakMap();
class Countdown {
constructor(counter, action) {
_counter.set(this, counter);
_action.set(this, action);
}
dec() {
let counter = _counter.get(this);
counter--;
_counter.set(this, counter);
if (counter === 0) {
_action.get(this)();
}
}
}
// The two pseudo-properties are truly private:
assert.deepEqual(
Object.keys(new Countdown()),
[]);
这就是Countdown的使用方式:
let invoked = false;
const cd = new Countdown(3, () => invoked = true);
cd.dec(); assert.equal(invoked, false);
cd.dec(); assert.equal(invoked, false);
cd.dec(); assert.equal(invoked, true);
练习:WeakMaps 用于私有数据
exercises/weakmaps/weakmaps_private_data_test.mjs
37.5 快速参考:WeakMap
构造函数和 WeakMap 的四个方法与它们的 Map 等价物工作方式相同:
-
new WeakMap<K, V>(entries?: Iterable<[K, V]>)(ES6) -
WeakMap.prototype.delete(key: K) : boolean(ES6) -
WeakMap.prototype.get(key: K) : V(ES6) -
WeakMap.prototype.has(key: K) : boolean(ES6) -
WeakMap.prototype.set(key: K, value: V) : this(ES6)
38 集合(Set)ES6
-
38.1 基本集合操作
-
38.1.1 创建集合
-
38.1.2 添加、删除、检查成员资格
-
38.1.3 确定集合的大小并清空它
-
-
38.2 组合集合:并集、交集、差集、对称差集 (ES2025)
-
38.2.1 并集:
set.union(other)(ES2025) -
38.2.2 交集:
set.intersection(other)(ES2025) -
38.2.3 差集:
set.difference(other)(ES2025) -
38.2.4 对称差集:
set.symmetricDifference(other)(ES2025)
-
-
38.3 检查集合关系:子集、超集、不相交 (ES2025)
-
38.3.1 子集:
set.isSubsetOf(other)(ES2025) -
38.3.2 超集:
set.isSupersetOf(other)(ES2025) -
38.3.3 不相交:
set.isDisjointFrom(other)(ES2025)
-
-
38.4 类似集合的对象(高级)
-
38.4.1 示例:有限类似集合对象
-
38.4.2 示例:无限类似集合数据
-
38.4.3 常见问题解答:类似集合对象
-
-
38.5 遍历集合
-
38.5.1 将集合转换为数组
-
38.5.2 通过迭代方法映射和过滤集合
-
38.5.3 示例:通过迭代组合集合 (ES2025)
-
38.5.4 对集合元素进行分组 (ES2024)
-
-
38.6 使用集合的示例
-
38.6.1 从数组中删除重复项
-
38.6.2 创建一个包含 Unicode 字符(代码点)的集合
-
-
38.7 集合 API 的详细信息(高级)
-
38.7.1 哪些集合元素被认为是相等的?
-
38.7.2 常见问题解答:集合 API
-
-
38.8 快速参考:
Set-
38.8.1
new Set() -
38.8.2
Set.prototype.*: 与单个集合元素一起工作 -
38.8.3
Set.prototype.*: 与所有集合元素一起工作 -
38.8.4
Set.prototype.*: 迭代和循环 -
38.8.5
Set.prototype.*: 合并两个集合 (ES2025) -
38.8.6
Set.prototype.*: 检查集合关系 (ES2025)
-
数据结构 Set 管理一个无重复值的集合,并提供快速的成员资格检查等功能。
38.1 基本集合操作
38.1.1 创建集合
有三种常见的方法来创建集合。
首先,我们可以使用无参数的构造函数来创建一个空集合:
const emptySet = new Set();
assert.equal(emptySet.size, 0);
第二,我们可以将可迭代对象(例如,一个数组)传递给构造函数。迭代的值成为新集合的元素:
const set = new Set(['red', 'green', 'blue']);
第三,.add() 方法向集合中添加元素,并且是可链式的:
const set = new Set()
.add('red')
.add('green')
.add('blue');
38.1.2 添加、删除、检查成员资格
.add() 向集合中添加一个元素。
const set = new Set();
set.add('red');
.has() 检查一个元素是否是集合的成员。
assert.equal(set.has('red'), true);
.delete() 从集合中删除一个元素。
assert.equal(set.delete('red'), true); // there was a deletion
assert.equal(set.has('red'), false);
38.1.3 确定集合的大小和清除集合
.size 包含集合中的元素数量。
const set = new Set()
.add('foo')
.add('bar');
assert.equal(set.size, 2)
.clear() 移除集合中的所有元素。
set.clear();
assert.equal(set.size, 0)
38.2 组合集合:并集、交集、差集、对称差 (ES2025)
有四种方法可以组合两个集合。
38.2.1 并集:set.union(other) (ES2025)

set.union(other) 的结果是包含 set 和 other 的值的集合:
assert.deepEqual(
new Set(['a', 'b']).union(new Set(['b', 'c'])),
new Set(['a', 'b', 'c'])
);
38.2.2 交集:set.intersection(other) (ES2025)

set.intersection(other) 的结果是包含 set 和 other 共同拥有的值的集合:
assert.deepEqual(
new Set(['a', 'b']).intersection(new Set(['b', 'c'])),
new Set(['b'])
);
38.2.3 差集:set.difference(other) (ES2025)

set.difference(other) 的结果是包含只存在于 set 的值的集合:
assert.deepEqual(
new Set(['a', 'b']).difference(new Set(['b', 'c'])),
new Set(['a'])
);
练习:实现 set.union()、set.intersection() 和 set.difference()
exercises/sets/set-union-intersection-difference_test.mjs
38.2.4 对称差:set.symmetricDifference(other) (ES2025)

set.symmetricDifference(other) 的结果是包含只存在于 set 或只存在于 other 的值的集合:
assert.deepEqual(
new Set(['a', 'b']).symmetricDifference(new Set(['b', 'c'])),
new Set(['a', 'c'])
);
assert.deepEqual(
new Set(['a', 'b']).symmetricDifference(new Set(['c', 'd'])),
new Set(['a', 'b', 'c', 'd'])
);
对称差是什么意思?以下是对称差的等效定义:
-
(
set−other) ∪ (other−set)- 只存在于
set或只存在于other的元素。该公式清楚地说明了对称差既是对称的,又是差异。
- 只存在于
-
(
set∪other) − (set∩other)set和other的元素——除了同时存在于两个集合中的元素。
-
set与other- 排他或——直观上:
set区域内的所有内容都被反转。set区域外的所有内容保持不变。
- 排他或——直观上:
练习:实现 set.symmetricDifference()
exercises/sets/set-symmetric-difference_test.mjs
38.3 检查集合关系:子集、超集、不相交 (ES2025)
有三种方法用于检查两个集合之间的关系。
38.3.1 子集:set.isSubsetOf(other) (ES2025)
set.isSubsetOf(other) 返回 true 如果 set 的所有元素都在 other 中:
assert.deepEqual(
new Set(['a', 'b']).isSubsetOf(new Set(['a', 'b', 'c'])),
true
);
38.3.2 超集:set.isSupersetOf(other) (ES2025)
set.isSupersetOf(other) 返回 true 如果 set 包含 other 的所有元素:
assert.deepEqual(
new Set(['a', 'b', 'c']).isSupersetOf(new Set(['a', 'b'])),
true
);
38.3.3 不相交:set.isDisjointFrom(other) (ES2025)
set.isDisjointFrom(other) 返回 true 如果 set 和 other 没有共同元素:
assert.deepEqual(
new Set(['a', 'b', 'c']).isDisjointFrom(new Set(['x'])),
true
);
Set 关系方法练习
-
实现 Set 相等性:
exercises/sets/set-is-equal-to_test.mjs -
实现
set.isSubsetOf():exercises/sets/is-subset-of_test.mjs -
实现
set.isDisjointFrom():exercises/sets/is-disjoint-from_test.mjs
38.4 Set-like 对象(高级)
一个参数为另一个 Set other 的 Set 方法不需要 other 真的是 Set;它只需要是 Set-like 并具有以下方法:
interface SetLike<T> {
/** Can be `Infinity` (see next section). */
size: number;
has(key: T): boolean;
/** Returns an iterator for the elements in `this`. */
keys(): Iterator<T>; // only method `.next()` is required
}
38.4.1 示例:有限 Set-like 对象
让我们实现一个简单的 Set-like 对象,并使用具有“其他 Set”参数的方法:
const setLike = {
size: 1,
has(x) { return x === 'b' },
* keys() { yield 'b' },
};
assert.deepEqual(
new Set(['a', 'b', 'c']).difference(setLike),
new Set(['a', 'c']),
);
assert.deepEqual(
new Set(['a', 'b', 'c']).difference(setLike),
new Set(['a', 'c']),
);
assert.equal(
new Set(['a', 'b', 'c']).isSupersetOf(setLike),
true,
);
assert.equal(
new Set(['b']).isSubsetOf(setLike),
true,
);
Maps 也是 Set-like:
const setLike = new Map([['b', true]]);
assert.deepEqual(
new Set(['a', 'b', 'c']).difference(setLike),
new Set(['a', 'c']),
);
assert.equal(
new Set(['a', 'b', 'c']).isSupersetOf(setLike),
true,
);
38.4.2 示例:无限 Set-like 数据
other 的 .size 可以是 Infinity。这意味着我们可以处理无限 Set:
const evenNumbers = {
has(elem) {
return (elem % 2) === 0;
},
size: Infinity,
keys() {
throw new TypeError();
}
};
assert.deepEqual(
new Set([0, 1, 2, 3]).difference(evenNumbers),
new Set([1, 3])
);
assert.deepEqual(
new Set([0, 1, 2, 3]).intersection(evenNumbers),
new Set([0, 2])
);
这之所以有效,是因为这些方法仅在 other.size 小于 this.size 时调用 other.keys()。
只有两种方法不支持 other 是无限 Set:
-
union -
symmetricDifference
38.4.3 常见问题解答:Set-like 对象
-
为什么使用
SetLike接口来处理other?- 由于接口,
other可以是一个不是 Set 的数据结构。它被选为只接受 Set 和所有可迭代对象的折衷方案。
- 由于接口,
-
为什么 JavaScript 总是强制执行
other的完整接口SetLike并在属性缺失或动态类型错误时抛出异常?- 这使得 API 更简单,并隐藏了实现细节。
-
为什么选择
.keys()方法来遍历元素?-
这是因为与标准库中当前唯一的 Set-like 数据结构
Map兼容。另外两种 Set 方法会更好,但不能与 Maps 一起使用:-
Map 方法
.[Symbol.iterator]()返回键值对。 -
Map 方法
.values()与 Map 方法.has()(接受键而不是值)不兼容。
-
-
来源:TC39 提案
38.5 遍历 Set
Set 是可迭代的,for-of 循环按预期工作:
const set = new Set(['red', 'green', 'blue']);
for (const x of set) {
console.log(x);
}
输出:
red
green
blue
如我们所见,Sets 保留 插入顺序。也就是说,元素总是按它们被添加的顺序迭代。
38.5.1 将 Set 转换为数组
集合是可迭代的,这就是为什么我们可以使用 Array.from() 将集合转换为数组:
const set = new Set(['red', 'green', 'blue']);
assert.deepEqual(
Array.from(set),
['red', 'green', 'blue']
);
我们也可以通过迭代器执行转换:
assert.deepEqual(
set.values().toArray(),
['red', 'green', 'blue']
);
-
set.values()返回set的值的迭代器。 -
迭代器方法
.toArray()创建一个包含迭代值的数组。
38.5.2 通过迭代器方法映射和过滤集合
集合没有 .map() 方法。但我们可以借用迭代器拥有的那个方法:
const set = new Set([1, 2, 3]);
const mappedSet = new Set( // (A)
set.values().map(x => x * 2) // (B)
);
assert.deepEqual(
mappedSet,
new Set([2, 4, 6])
);
之前的代码展示了使用集合的迭代器方法的常见模式:
-
set.values()返回set的迭代器(行 B)。 -
.map()是一个迭代器方法,它返回一个迭代器(行 B)。 -
上一步的结果是一个可迭代的迭代器。它被传递给接受任何可迭代对象作为参数的构造函数
Set,并使用它填充新的集合。
过滤集合的工作方式相同:
const set = new Set([1, 2, 3, 4, 5]);
const filteredSet = new Set(
set.values().filter(x => (x % 2) === 0)
);
assert.deepEqual(
filteredSet,
new Set([2, 4])
);
如果我们不能使用迭代器方法怎么办?那么我们可以切换到数组方法:
-
我们使用
Array.from(set) -
而不是
set.values()。
38.5.3 示例:通过迭代组合集合(ES2025)
我们也可以使用迭代来代替集合方法结合集合:
const a = new Set([1, 2, 3]);
const b = new Set([2, 3, 4]);
// Union
assert.deepEqual(
new Set([...a, ...b]),
new Set([1, 2, 3, 4])
);
// Intersection
assert.deepEqual(
new Set(a.values().filter(x => b.has(x))),
new Set([2, 3])
);
// Difference
assert.deepEqual(
new Set(a.values().filter(x => !b.has(x))),
new Set([1])
);
38.5.4 分组集合元素(ES2024)
通过 Object.groupBy() 和 Map.groupBy() 进行分组适用于任何可迭代对象,因此也适用于集合:
assert.deepEqual(
Object.groupBy(
new Set([0, -5, 3, -4, 8, 9]),
x => Math.sign(x)
),
{
'0': [0],
'-1': [-5,-4],
'1': [3,8,9],
__proto__: null,
}
);
更多信息:“分组可迭代对象(ES2024)” (§32.8)
38.6 使用集合的示例
38.6.1 从数组中删除重复项
将数组转换为集合再转换回来,会从数组中删除重复项:
const arr = [1, 2, 1, 2, 3, 3, 3];
const noDuplicates = Array.from(new Set(arr));
assert.deepEqual(
noDuplicates, [1, 2, 3]
);
38.6.2 创建 Unicode 字符集(代码点)
字符串是可迭代的,因此可以用作 new Set() 的参数:
assert.deepEqual(
new Set('abc'),
new Set(['a', 'b', 'c'])
);
38.7 集合 API 的详细信息(高级)
38.7.1 哪些集合元素被认为是相等的?
与 Map 键类似,集合元素与 === 进行类似比较,除了 NaN 等于自身。
> const set = new Set([NaN, NaN, NaN]);
> set.size
1
> set.has(NaN)
true
与 === 类似,两个不同的对象永远不会被认为是相等的(目前也没有办法改变这一点):
> const set = new Set();
> set.add({});
> set.size
1
> set.add({});
> set.size
2
38.7.2 常见问题解答:集合 API
-
为什么集合有
.size,而数组有.length?- 这个问题的答案在“为什么 Map 有
.size,而数组有.length?”(§36.7.4)中给出。
- 这个问题的答案在“为什么 Map 有
-
为什么有些方法名是动词,而有些是名词?这是一个粗略的一般规则:
-
动词方法会修改
this– 例如:set.add()和set.clear() -
名词方法返回新数据 – 例如:
set.values()和set.union()
-
38.8 快速参考:Set
38.8.1 new Set()
-
new Set(iterable)ES6-
如果我们不提供参数
values,则创建一个空集合。 -
如果我们这样做,那么迭代的值将被添加为集合的元素。
const set = new Set(['red', 'green', 'blue']); -
38.8.2 Set.prototype.*: 处理单个集合元素
-
Set.prototype.add(value)ES6-
向这个集合添加
value。 -
此方法返回
this,这意味着它可以被链式调用。
const set = new Set(['red']); set.add('green').add('blue'); assert.deepEqual( Array.from(set), ['red', 'green', 'blue'] ); -
-
Set.prototype.delete(value)ES6-
从这个集合中移除
value。 -
如果有东西被删除,则返回
true,否则返回false。
const set = new Set(['red', 'green', 'blue']); assert.equal(set.delete('red'), true); // there was a deletion assert.deepEqual( Array.from(set), ['green', 'blue'] ); -
-
Set.prototype.has(value)ES6如果
value在这个集合中,则返回true,否则返回false。const set = new Set(['red', 'green']); assert.equal(set.has('red'), true); assert.equal(set.has('blue'), false);
38.8.3 Set.prototype.*: 处理所有集合元素
-
get Set.prototype.sizeES6返回这个集合中有多少个元素。
const set = new Set(['red', 'green', 'blue']); assert.equal(set.size, 3); -
Set.prototype.clear()ES6从这个集合中移除所有元素。
const set = new Set(['red', 'green', 'blue']); assert.equal(set.size, 3); set.clear(); assert.equal(set.size, 0);
38.8.4 Set.prototype.*: 遍历和循环
-
Set.prototype.values()ES6返回一个遍历这个集合所有元素的迭代器。
const set = new Set(['red', 'green']); for (const x of set.values()) { console.log(x); }输出:
red green -
Set.prototype[Symbol.iterator]()ES6遍历集合的默认方式。与
.values()相同。const set = new Set(['red', 'green']); for (const x of set) { console.log(x); }输出:
red green -
Set.prototype.forEach(callback, thisArg?)ES6forEach( callback: (value: T, key: T, theSet: Set<T>) => void, thisArg?: any ): void将这个集合的每个元素传递给
callback()。value和key都包含当前元素。这种冗余是为了使这个callback与Map.prototype.forEach()的callback具有相同的类型签名。我们可以通过
thisArg指定callback的this。如果我们省略它,则this是undefined。const set = new Set(['red', 'green']); set.forEach(x => console.log(x));输出:
red green
38.8.4.1 与 Map 的对称性
以下方法使 Set 的接口与 Map 的接口对称。
-
Set.prototype.entries(): Iterable<[T,T]>ES6主要存在是为了使集合和映射具有相似的接口:每个集合元素被视为一个键值对,其键和值都是该元素:
> new Set(['a', 'b', 'c']).entries().toArray() [ [ 'a', 'a' ], [ 'b', 'b' ], [ 'c', 'c' ] ].entries()允许我们将集合转换为映射:const set = new Set(['a', 'b', 'c']); const map = new Map(set.entries()); assert.deepEqual( Array.from(map.entries()), [['a','a'], ['b','b'], ['c','c']] ); -
Set.prototype.keys(): Iterable<T>ES6主要存在是为了使集合和映射具有相似的接口:每个集合元素被视为一个键值对,其键和值都是该元素。因此
.keys()的结果与.values()的结果相同:> new Set(['a', 'b', 'c']).keys().toArray() [ 'a', 'b', 'c' ]
38.8.5 Set.prototype.*: 合并两个集合 (ES2025)
-
Set.prototype.union(other)ES2025Set<T>.prototype.union(other: SetLike<T>): Set<T>此方法返回一个集合,它是
this和other的并集。如果它在this或other中,则包含该值。assert.deepEqual( new Set(['a', 'b']).union(new Set(['b', 'c'])), new Set(['a', 'b', 'c']) );other不必是集合,它只需要是 类似集合 并具有属性.size和方法.has(key)以及.keys()。集合和映射都满足这些要求。 -
Set.prototype.intersection(other)ES2025Set<T>.prototype.intersection(other: SetLike<T>): Set<T>此方法返回一个集合,它是
this和other的交集。如果它在this或other中,则包含该值。assert.deepEqual( new Set(['a', 'b']).intersection(new Set(['b', 'c'])), new Set(['b']) );other不必是集合,它只需要是 类似集合 并具有属性.size和方法.has(key)以及.keys()。集合和映射都满足这些要求。 -
Set.prototype.difference(other)ES2025Set<T>.prototype.difference(other: SetLike<T>): Set<T>此方法返回一个集合,它是
this和other的差集。如果该值在this中但不在other中,则包含该值。assert.deepEqual( new Set(['a', 'b']).difference(new Set(['b', 'c'])), new Set(['a']) );other不必是集合,它只需要是类似集合的对象,并具有.size属性以及.has(key)和.keys()方法。集合和映射都满足这些要求。 -
Set.prototype.symmetricDifference(other)ES2025Set<T>.prototype.symmetricDifference(other: SetLike<T>): Set<T>此方法返回一个集合,它是
this和other的对称差集。如果该值仅在this或仅在other中,则包含该值。assert.deepEqual( new Set(['a', 'b']).symmetricDifference(new Set(['b', 'c'])), new Set(['a', 'c']) );other不必是集合,它只需要是类似集合的对象,并具有.size属性以及.has(key)和.keys()方法。集合和映射都满足这些要求。关于此方法的更多信息,请参阅本章中的相关部分。
38.8.6 Set.prototype.*: 检查集合关系^(ES2025)
-
Set.prototype.isSubsetOf(other)ES2025Set<T>.prototype.isSubsetOf(other: SetLike<T>): boolean如果
this的所有元素都在other中,则返回true:assert.deepEqual( new Set(['a', 'b']).isSubsetOf(new Set(['a', 'b', 'c'])), true ); -
Set.prototype.isSupersetOf(other)ES2025Set<T>.prototype.isSupersetOf(other: SetLike<T>): boolean如果
this包含other的所有元素,则返回true:assert.deepEqual( new Set(['a', 'b', 'c']).isSupersetOf(new Set(['a', 'b'])), true ); -
Set.prototype.isDisjointFrom(other)ES2025Set<T>.prototype.isDisjointFrom(other: SetLike<T>): boolean如果
this和other没有共同元素,则返回true:assert.deepEqual( new Set(['a', 'b', 'c']).isDisjointFrom(new Set(['x'])), true );


浙公网安备 33010602011771号