探索-JavaScript-ES2025-版--九-

探索 JavaScript(ES2025 版)(九)

原文:exploringjs.com/js/book/index.html

译者:飞龙

协议:CC BY-NC-SA 4.0

35 类型化数组:处理二进制数据 ES6 (高级)

原文:exploringjs.com/js/book/ch_typed-arrays.html

  1. 35.1 类型化数组 API:二进制数据的容器

    1. 35.1.1 类型化数组的用例

    2. 35.1.2 核心类:ArrayBuffer、类型化数组、DataView

    3. 35.1.3 SharedArrayBuffer (ES2017)

  2. 35.2 使用类型化数组

    1. 35.2.1 创建类型化数组

    2. 35.2.2 包装的 ArrayBuffer

    3. 35.2.3 获取和设置元素

    4. 35.2.4 连接类型化数组

    5. 35.2.5 类型化数组与普通数组

  3. 35.3 使用 DataView

  4. 35.4 元素类型

    1. 35.4.1 处理溢出和下溢

    2. 35.4.2 字节序

  5. 35.5 转换为和从类型化数组转换

    1. 35.5.1 静态方法 «ElementType»Array.from()

    2. 35.5.2 类型化数组是可迭代的

    3. 35.5.3 将类型化数组转换为普通数组以及反之

    4. 35.5.4 将 Uint8Array (UTF-8) 转换为字符串以及反之

  6. 35.6 调整 ArrayBuffer 的大小 (ES2024)

    1. 35.6.1 ArrayBuffer 的新特性

    2. 35.6.2 类型化数组如何响应 ArrayBuffer 大小的变化

    3. 35.6.3 ECMAScript 规范提供的指南

  7. 35.7 传输和分离 ArrayBuffer (ES2024)

    1. 35.7.1 准备:数据传输和分离

    2. 35.7.2 与传输和分离相关的方法

    3. 35.7.3 通过 structuredClone() 传输 ArrayBuffer

    4. 35.7.4 在同一代理内传输 ArrayBuffer

    5. 35.7.5 分离 ArrayBuffer 会如何影响其包装器?

    6. 35.7.6 ArrayBuffer.prototype.transferToFixedLength()

  8. 35.8 快速参考:索引与偏移量

  9. 35.9 快速参考:ArrayBuffer

    1. 35.9.1 new ArrayBuffer()

    2. 35.9.2 ArrayBuffer.*

    3. 35.9.3 ArrayBuffer.prototype.*: 获取和切片

    4. 35.9.4 ArrayBuffer.prototype.*: 调整大小

  10. 35.10 快速参考:Typed Arrays

    1. 35.10.1 TypedArray.*

    2. 35.10.2 TypedArray.prototype.*

    3. 35.10.3 new «ElementType»Array()

    4. 35.10.4 «ElementType»Array.*

    5. 35.10.5 «ElementType»Array.prototype.*

  11. 35.11 快速参考:DataViews

    1. 35.11.1 new DataView()

    2. 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 位浮点数。

  • 数据视图允许我们将数据解释为各种类型(Uint8Int16Float16等),我们可以在任何字节偏移量处读取和写入这些类型。

图 35.1 显示了 API 的类图。

图 35.1:类型数组 API 的类。

35.1.3   SharedArrayBuffer (ES2017)

共享数组缓冲区是一个数组缓冲区,其内存可以被多个代理(代理可以是主线程或 Web Worker)同时访问。

  • 在数组缓冲区可以在代理之间传输(移动,而不是复制)的情况下,共享数组缓冲区是不可传输的,必须进行克隆。然而,这仅克隆了它们的表层部分。数据存储本身是共享的。

  • 共享数组缓冲区可以调整大小,但它们只能增长而不能缩小,因为缩小共享内存太复杂了。

  • Atomics是一个全局命名空间,用于补充共享数组缓冲区的 API。ECMAScript 规范描述它为“在共享内存数组单元上操作不可分割(原子)的函数以及允许代理等待和调度原始事件的函数。当有纪律地使用时,Atomics 函数允许通过共享内存通信的多代理程序在并行 CPU 上以可理解的方式执行。”

有关SharedArrayBufferSharedArrayBuffer)和AtomicsAtomics)的更多信息,请参阅 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

它将现有的 typedArrayarrayLike 复制到接收器中的 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)。

  • 普通数组可以有空洞;类型化数组不能。

Icon “exercise”练习:字符串与 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),应避免使用。Uint8CUint8之间的唯一区别是它们处理溢出的方式(如下所述)。

类型数组和数组缓冲区使用数字和大整数来导入和导出值:

  • BigInt64BigUint64类型通过大整数处理。例如,设置器接受大整数,获取器返回大整数。

  • 所有其他元素类型都通过数字处理。

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代表Int8ArrayUint8Array等。ElementTypeInt8Uint8等。

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()单独映射相比,有两个优点:

  1. 不需要中间的数组或类型数组。

  2. 在将不同精度的类型数组之间进行转换时,出错的可能性更小。

继续阅读以了解第二个优势的解释。

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)转换为字符串以及从字符串转换回来

TextEncoderTextDecoder不是 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
)

如果lengthundefined,则 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?) ES6

    new ArrayBuffer(
      byteLength: number,
      options?: { // ES2024
        maxByteLength?: number
      }
    )
    
    

    通过 new 调用此构造函数会创建一个容量为 length 字节的实例。每个字节最初都是 0。

    如果提供了 options.maxByteLength,则 ArrayBuffer 可以调整大小。否则,它具有固定长度。

35.9.2 ArrayBuffer.*

  • ArrayBuffer.isView(arg) ES6

    如果argArrayBuffer的视图(即,如果它是类型化数组或DataView),则返回true

    > ArrayBuffer.isView(new Uint8Array())
    true
    > ArrayBuffer.isView(new DataView(new ArrayBuffer()))
    true
    
    

35.9.3 ArrayBuffer.prototype.*:获取和切片

  • get ArrayBuffer.prototype.byteLength ES6

    返回此ArrayBuffer的字节容量。

  • ArrayBuffer.prototype.slice(startIndex=0, endIndex=this.byteLength) ES6

    创建一个新的ArrayBuffer,它包含此ArrayBuffer中索引大于或等于startIndex且小于endIndex的字节。startendIndex可以是负数(参见“快速参考:索引与偏移量”(§35.8))。

35.9.4 ArrayBuffer.prototype.*:调整大小

  • ArrayBuffer.prototype.resize(newByteLength) ES2024

    改变此ArrayBuffer的大小。更多信息,请参见“调整ArrayBuffer大小”(§35.6)。

  • get ArrayBuffer.prototype.resizable ES2024

    如果此ArrayBuffer可调整大小,则返回true;如果不可以,则返回false

  • get ArrayBuffer.prototype.maxByteLength ES2024

    如果在构造函数中提供了options.maxByteLength,则返回options.maxByteLength。否则,返回this.byteLength

35.10 快速参考:类型化数组

类型化数组对象的属性分两步介绍:

  1. TypedArray:首先,我们来看所有类型化数组类的抽象超类(在本书开头的类图中已展示)。这个超类称为TypedArray,但在 JavaScript 中没有全局名称:

    > Object.getPrototypeOf(Uint8Array).name
    'TypedArray'
    
    
  2. «ElementType»Array:具体的类型化数组类称为Uint8ArrayInt16ArrayFloat16Array等。这些是我们通过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.buffer ES6

    返回支持此 Typed Array 的 ArrayBuffer。

  • 获取 TypedArray.prototype.length ES6

    返回此 Typed Array 缓冲区的元素长度。

    > new Uint32Array(new ArrayBuffer(4)).length
    1
    
    
  • 获取 TypedArray.prototype.byteLength ES6

    返回此 Typed Array 缓冲区的字节大小。

    > new Uint32Array(new ArrayBuffer(4)).byteLength
    4
    
    
  • 获取 TypedArray.prototype.byteOffset ES6

    返回此 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.buffer ES6

    返回此 DataView 的 ArrayBuffer。

  • get DataView.prototype.byteLength ES6

    返回此 DataView 可以访问的字节数。

  • get DataView.prototype.byteOffset ES6

    返回此 DataView 从其缓冲区开始访问字节的偏移量。

  • DataView.prototype.get«ElementType»(byteOffset, littleEndian=false) ES6

    返回:

    • BigInt64, BigUint64: bigint

    • 所有其他元素类型:number

    从此 DataView 的缓冲区中读取一个值。

  • DataView.prototype.set«ElementType»(byteOffset, value, littleEndian=false) ES6

    value 的类型:

    • BigInt64, BigUint64: bigint

    • 所有其他元素类型:number

    value 写入此 DataView 的缓冲区。

36 Maps (Map) ES6

原文:exploringjs.com/js/book/ch_maps.html

  1. 36.1 使用 Maps

    1. 36.1.1 创建 Maps

    2. 36.1.2 处理单个条目

    3. 36.1.3 确定 Maps 的大小并清除它

    4. 36.1.4 示例:计数字符

  2. 36.2 在 Map 上迭代

    1. 36.2.1 在 Map 上循环

    2. 36.2.2 按插入顺序列出:条目、键、值

  3. 36.3 从 Maps 转换到和从 Maps 转换

    1. 36.3.1 将键、值、条目转换为 Arrays

    2. 36.3.2 在 Maps 和 Objects 之间转换

  4. 36.4 处理 Maps

    1. 36.4.1 复制 Maps

    2. 36.4.2 将多个 Maps 合并成一个单一 Map

    3. 36.4.3 通过迭代器方法映射和过滤 Maps(ES2025)

  5. 36.5 关于 Maps 键的一些更多细节(高级)

    1. 36.5.1 哪些键被认为是相等的?
  6. 36.6 快速参考:Map

    1. 36.6.1 new Map()

    2. 36.6.2 Map.*

    3. 36.6.3 Map.prototype.*:处理单个条目

    4. 36.6.4 Map.prototype:处理所有条目

    5. 36.6.5 Map.prototype:迭代和循环

  7. 36.7 FAQ: Maps

    1. 36.7.1 应该何时使用 Map,何时使用对象?

    2. 36.7.2 当何时在 Map 中使用对象作为键?

    3. 36.7.3 为什么 Maps 保留条目的插入顺序?

    4. 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')
;

要合并 map1map2,我们创建一个新的数组,并将 map1map2 的条目(键值对)展开到其中(通过迭代)。然后我们将数组转换回 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 没有这样的操作。解决方案是:

  1. 将 Map 转换为 [键,值] 对的迭代器。

  2. Map 或过滤迭代器。

  3. 将结果转换回 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?) ES6

    new 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) ES2024

    Map.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.size ES6

    返回此 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?) ES6

    Map.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 (高级)

原文:exploringjs.com/js/book/ch_weakmaps.html

  1. 37.1 WeakMap 与 Map 有何不同?

  2. 37.2 WeakMap 是黑盒

  3. 37.3 WeakMap 的键是弱持有

    1. 37.3.1 WeakMap 中可以作为键的值有哪些?

    2. 37.3.2 为什么符号作为 WeakMap 键有趣?^(ES2023)

  4. 37.4 WeakMap 的用例:将值附加到对象上

    1. 37.4.1 示例:缓存计算结果

    2. 37.4.2 示例:在 WeakMap 中保持对象私有数据

  5. 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()创建)

所有的键有一个共同点——它们有身份语义

  1. 当通过===比较时,如果两个键具有相同的身份,则认为它们相等——它们不是通过比较它们的内容(它们的值)来比较的。这意味着永远不会有两个或更多不同的键(“不同”意味着“在内存中的不同位置”)都被认为是相等的。每个键都是唯一的。

  2. 它们被垃圾回收了。

这两个条件都很重要,这样 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 功能的重要问题:

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

原文:exploringjs.com/js/book/ch_sets.html

  1. 38.1 基本集合操作

    1. 38.1.1 创建集合

    2. 38.1.2 添加、删除、检查成员资格

    3. 38.1.3 确定集合的大小并清空它

  2. 38.2 组合集合:并集、交集、差集、对称差集 (ES2025)

    1. 38.2.1 并集:set.union(other) (ES2025)

    2. 38.2.2 交集:set.intersection(other) (ES2025)

    3. 38.2.3 差集:set.difference(other) (ES2025)

    4. 38.2.4 对称差集:set.symmetricDifference(other) (ES2025)

  3. 38.3 检查集合关系:子集、超集、不相交 (ES2025)

    1. 38.3.1 子集:set.isSubsetOf(other) (ES2025)

    2. 38.3.2 超集:set.isSupersetOf(other) (ES2025)

    3. 38.3.3 不相交:set.isDisjointFrom(other) (ES2025)

  4. 38.4 类似集合的对象(高级)

    1. 38.4.1 示例:有限类似集合对象

    2. 38.4.2 示例:无限类似集合数据

    3. 38.4.3 常见问题解答:类似集合对象

  5. 38.5 遍历集合

    1. 38.5.1 将集合转换为数组

    2. 38.5.2 通过迭代方法映射和过滤集合

    3. 38.5.3 示例:通过迭代组合集合 (ES2025)

    4. 38.5.4 对集合元素进行分组 (ES2024)

  6. 38.6 使用集合的示例

    1. 38.6.1 从数组中删除重复项

    2. 38.6.2 创建一个包含 Unicode 字符(代码点)的集合

  7. 38.7 集合 API 的详细信息(高级)

    1. 38.7.1 哪些集合元素被认为是相等的?

    2. 38.7.2 常见问题解答:集合 API

  8. 38.8 快速参考:Set

    1. 38.8.1 new Set()

    2. 38.8.2 Set.prototype.*: 与单个集合元素一起工作

    3. 38.8.3 Set.prototype.*: 与所有集合元素一起工作

    4. 38.8.4 Set.prototype.*: 迭代和循环

    5. 38.8.5 Set.prototype.*: 合并两个集合 (ES2025)

    6. 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) 的结果是包含 setother 的值的集合:

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) 的结果是包含 setother 共同拥有的值的集合:

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'])
);

对称差是什么意思?以下是对称差的等效定义:

  • (setother) ∪ (otherset)

    • 只存在于 set 或只存在于 other 的元素。该公式清楚地说明了对称差既是对称的,又是差异。
  • (setother) − (setother)

    • setother 的元素——除了同时存在于两个集合中的元素。
  • setother

    • 排他或——直观上: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 如果 setother 没有共同元素:

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)中给出。
  • 为什么有些方法名是动词,而有些是名词?这是一个粗略的一般规则:

    • 动词方法会修改 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.size ES6

    返回这个集合中有多少个元素。

    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?) ES6

    forEach(
      callback: (value: T, key: T, theSet: Set<T>) => void,
      thisArg?: any
    ): void
    
    

    将这个集合的每个元素传递给 callback()valuekey 都包含当前元素。这种冗余是为了使这个 callbackMap.prototype.forEach()callback 具有相同的类型签名。

    我们可以通过 thisArg 指定 callbackthis。如果我们省略它,则 thisundefined

    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) ES2025

    Set<T>.prototype.union(other: SetLike<T>): Set<T>
    
    

    此方法返回一个集合,它是 thisother 的并集。如果它在 thisother 中,则包含该值。

    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) ES2025

    Set<T>.prototype.intersection(other: SetLike<T>): Set<T>
    
    

    此方法返回一个集合,它是 thisother 的交集。如果它在 thisother 中,则包含该值。

    assert.deepEqual(
      new Set(['a', 'b']).intersection(new Set(['b', 'c'])),
      new Set(['b'])
    );
    
    

    other 不必是集合,它只需要是 类似集合 并具有属性 .size 和方法 .has(key) 以及 .keys()。集合和映射都满足这些要求。

  • Set.prototype.difference(other) ES2025

    Set<T>.prototype.difference(other: SetLike<T>): Set<T>
    
    

    此方法返回一个集合,它是thisother的差集。如果该值在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) ES2025

    Set<T>.prototype.symmetricDifference(other: SetLike<T>): Set<T>
    
    

    此方法返回一个集合,它是thisother的对称差集。如果该值仅在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) ES2025

    Set<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) ES2025

    Set<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) ES2025

    Set<T>.prototype.isDisjointFrom(other: SetLike<T>): boolean
    
    

    如果thisother没有共同元素,则返回true

    assert.deepEqual(
      new Set(['a', 'b', 'c']).isDisjointFrom(new Set(['x'])),
      true
    );
    
    
posted @ 2025-12-12 18:01  绝不原创的飞龙  阅读(2)  评论(0)    收藏  举报