For AI agents: the complete documentation index is available at /tc39-atlas/llms.txt, the full documentation bundle is available at /tc39-atlas/llms-full.txt, and this page is available as Markdown at /tc39-atlas/proposals/stage/2.7/proposal-immutable-arraybuffer.md.
  • 简体中文
  • Immutable ArrayBuffers S2.7

    中文标题:不可变 ArrayBuffer

    提案概览
    提案速览

    该提案增加了创建不可变 ArrayBuffer 的方式,使其内容不能被更改、调整大小或分离。它在 ArrayBuffer.prototype 上引入了 immutable 访问器以及 transferToImmutablesliceToImmutable 方法。这支持可被冻结并放入 ROM 的数据,也可避免接受缓冲区的 API 进行防御性拷贝。

    Note

    以下 README 来自上游仓库,其中的阶段或状态标注可能滞后;当前信息以提案概览为准。

    不可变 ArrayBuffer

    一项关于不可变 ArrayBuffer 的 TC39 提案。

    状态

    TC39 流程

    阶段:2.7

    提案人

    • Mark S. Miller (@erights)
    • Peter Hoddie (@phoddie)
    • Richard Gibson (@gibson042)
    • Jack-Works (@Jack-Works)

    规范https://tc39.es/proposal-immutable-arraybuffer/

    演讲历史

    背景

    此前的提案 In-Place Resizable and Growable ArrayBuffersArrayBuffer.prototype.transfer and friends 都已达到第 4 阶段,因此现在是 JavaScript 的正式组成部分。总的来说,ArrayBuffer.prototype 现在具有以下方法:

    • transfer(newByteLength?: number) :ArrayBuffer -- 将原缓冲区的内容移动到新缓冲区,分离原缓冲区,并返回新缓冲区。新缓冲区将和原缓冲区一样可调整大小。
    • transferToFixedLength(newByteLength?: number) :ArrayBuffer -- 类似 transfer,但新缓冲区不可调整大小。
    • resize(newByteLength: number) :void -- 如果可能,更改此缓冲区的大小,否则抛出异常。
    • slice(start?: number, end?: number) :ArrayBuffer -- 返回一个新缓冲区,其初始内容是原缓冲区该区域的副本。原缓冲区未被修改。

    以及以下只读访问器属性

    • detached: boolean -- 此缓冲区是否已分离,或者其内容是否仍可从此缓冲区对象访问?
    • resizable: boolean -- 此缓冲区是否可以调整大小,还是固定长度?
    • byteLength: number -- 此缓冲区当前内容有多大?
    • maxByteLength: number -- 此缓冲区最大可调整到多大?

    上述操作均无法创建不可变缓冲区,即一个未分离且其内容不能被更改、调整大小或分离的缓冲区。

    DataView 对象和 TypedArray 对象都是对缓冲区后备存储的视图。对于 TypedArray 对象,后备存储的内容会作为 TypeArray 对象的索引数据属性出现,并反映此后备存储的当前内容。目前,由于无法阻止后备存储的内容被更改,TypedArray 无法被冻结。

    动机

    一些 JavaScript 实现(例如 Moddable XS)将 JavaScript 带到嵌入式系统(例如设备控制器)中,在这些系统里 ROM 比 RAM 更充裕且更便宜。这些系统需要将大量固定数据放入 ROM,目前是通过官方 JavaScript 标准之外的语义来实现的。

    接受 ArrayBuffer 和/或由它们支持的对象的 API 也可以受益于性能改进:当输入缓冲区不可变时,可以避免防御性拷贝(参见 通用零拷贝 ArrayBuffer 用法,了解 Web 平台中针对此问题提出的替代解决方案)。

    OCapN 网络协议将字符串和字节数组视为要通过拷贝传输的两种不同形式的批量数据。在支持 OCapN 的 JavaScript 端点(例如 @endo/pass-style + @endo/marshal)上,JavaScript 字符串表示 OCapN 字符串。JavaScript 语言中字符串的不可变性反映了它们在协议中的按拷贝性质。同样,为了将 OCapN 字节数组良好地反映到 JavaScript 语言中,需要一个不可变批量二进制数据的容器。目前没有这样的容器,但不可变 ArrayBuffer 恰好能提供必要的底层机制。

    目标重叠的先前提案或议题

    Limited ArrayBuffer,特别是 issue #16

    Readonly Collections,特别是 issue #10

    wasm 零拷贝 issue #1162 comment

    w3c TPAC 演讲 Web 上的零拷贝操作

    web-bluetooth 只读 ArrayBuffer,特别是 issue #300

    gpuweb issue #2072issue #747SharedValueTable 提案

    webidl 冻结数组

    webcodecs issue #80issue #104issue #212

    web transport issue #131

    whatwg streams issue #495

    • 不太可能,因为,嗯,它们是流,不是缓冲区。

    w3c machine learning workshop issue #93

    Deno 打算支持

    Proposal Import Buffer 依赖不可变 ArrayBuffer

    解决方案

    本提案向 ArrayBuffer.prototype 引入了额外的方法和只读访问器属性,它们自然地融入上述内容。正如缓冲区可以可调整大小或不可调整大小,以及已分离或未分离一样,本提案使缓冲区可以不可变或可变。正如 transferToFixedSize 将原始缓冲区的内容移动到新创建的非可调整大小缓冲区中一样,本提案提供了一种转移操作,将原始原始缓冲区的内容移动到新创建的不可变缓冲区中。总的来说,本提案仅向 ArrayBuffer.prototype 添加一个只读访问器

    • immutable: boolean -- 此缓冲区是否不可变,或者其内容是否可以被更改?

    以及两个方法

    • transferToImmutable(newByteLength?: number) :ArrayBuffer -- 将原缓冲区的内容移动到一个新的不可变缓冲区,分离原缓冲区,并返回新缓冲区。
    • sliceToImmutable(start?: number, end?: number) :ArrayBuffer -- 从原缓冲区内容的某个范围创建一个新的不可变缓冲区,这种方式允许实现轻松地减少、有时甚至消除对它们的拷贝。

    不可变缓冲区不能被分离、调整大小或进一步转移。它的 maxByteLength 与其 byteLength 相同。使用不可变缓冲区作为后备存储的 DataViewTypedArray 可以被冻结且不可变。被冻结且不可变的 ArrayBuffers、DataViews 和 TypedArrays 可以放入 ROM,而不超出 JavaScript 的官方语义。

    ArrayBuffer slice 方法和会创建新 ArrayBuffer 的 TypedArray 方法(filtermapslicetoReversed 等)不会尝试保留不可变性,就像它们不会尝试保留可调整大小性一样(尽管这些方法中使用 SpeciesConstructor 意味着对于后者,结果中可调整大小性/不可变性的 缺失 无法得到保证)。

    不可变缓冲区也直观地与 HTML structuredClone 集成——它们不可转移,但克隆会同时保留不可变性和底层数据块。

    用例

    将任意二进制数据表示为不可变的 netstring

    const consumeIntoNetstring = data => {
      // Transfer to a new ArrayBuffer with room for the netstring framing.
      // https://en.wikipedia.org/wiki/Netstring
      const prefix = new TextEncoder().encode(`${data.length}:`);
      const buf = data.buffer.transfer(prefix.length + data.length + 1);
    
      // Frame the data.
      const tmpArr = new Uint8Array(buf);
      tmpArr.copyWithin(prefix.length, 0);
      tmpArr.set(prefix);
      tmpArr[tmpArr.length - 1] = 0x2C;
    
      // Transfer to an immutable ArrayBuffer backing a frozen Uint8Array.
      const frozenNetstring = Object.freeze(new Uint8Array(buf.transferToImmutable()));
      assert(buf.detached);
      return frozenNetstring;
    };
    
    const input = new TextEncoder().encode('hello world!');
    const result = consumeIntoNetstring(input);
    assert(Object.isFrozen(result));
    try { result[0] = 0; } catch (_err) {}
    try { new Uint8Array(result.buffer)[0] = 1; } catch (_err) {}
    try { result.buffer.transferToImmutable(); } catch (_err) {}
    assert(String.fromCharCode(...result) === '12:hello world!,');

    实现

    Polyfill/转译器实现

    原生实现

    待添加的跟踪议题:

    问答

    为什么不可变 ArrayBuffer 不能被分离/转移?

    因为那会导致由它支持的任何 TypedArray 或 DataView 发生可观察的变化。

    由不可变 ArrayBuffer 支持的 TypedArray 的索引属性是否应可配置且可写?

    不,TypedArray 索引属性应继续跟踪底层缓冲区的状态,而无需逐个记录。

    ArrayBuffer 是否应支持零拷贝切片(例如 arrayBuffer.sliceToImmutable())? https://github.com/tc39/proposal-immutable-arraybuffer/issues/9

    是的。正如 12 月 TC39 全会所商定的,我们不会规定实现必须是零拷贝。但提供此操作 使 某些实现能够轻松地将其实现为零拷贝。

    新的 getter 应命名为 immutable 还是 mutablehttps://github.com/tc39/proposal-immutable-arraybuffer/issues/10

    immutable。正如 12 月 TC39 全会所商定的,遵循默认为 false 的原则,诸如 if (buf.immutable) { 这样的特性检测在尚未实现此提案的引擎上将返回假值。

    操作顺序,何时抛出异常或静默不执行? https://github.com/tc39/proposal-immutable-arraybuffer/issues/16

    我们将根据实现者的反馈来推动此问题的解决。但当这一点本身不是决定性因素时,我们倾向于失败时抛出异常,而不是静默处理。现有的 XS 实现遵循这一原则。