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

    提案概览
    提案速览

    该提案旨在在 JavaScript 中引入不可变 ArrayBuffer,解决对不可修改、不可调整大小、不可分离的只读二进制数据容器的需求。它增加了 immutable getter 和两个方法 transferToImmutablesliceToImmutable,允许创建不可变缓冲区,同时在某些实现中允许高效的零拷贝切片。7,先前的阶段已获得 TC39 批准,并已获得 Moddable XS 等实现者的早期支持。

    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 对象,后备存储的内容表现为 TypedArray 对象的索引数据属性,反映后备存储的当前内容。目前,由于无法阻止后备存储内容被更改,因此 TypedArray 不能被冻结。

    动机

    一些 JavaScript 实现,如 Moddable XS,将 JavaScript 引入嵌入式系统,例如设备控制器,其中 ROM 比 RAM 更丰富且更便宜。这些系统需要将大量固定数据放入 ROM 中,目前使用官方 JavaScript 标准之外的语义来实现。

    接受 ArrayBuffer 及其支持对象的 API 也可以通过避免输入缓冲区不可变时的防御性复制来提高性能(有关 Web 平台中提出的替代解决方案,请参阅 Generic zero-copy ArrayBuffer usage)。

    OCapN 网络协议将字符串和字节数组视为通过复制传输的不同类型的批量数据。在讲 OCapN 的 JavaScript 端点,如 @endo/pass-style + @endo/marshal,JavaScript 字符串代表 OCapN 字符串。JavaScript 语言中字符串的不可变性反映了协议中的按复制特性。同样,要很好地将 OCapN 字节数组反映到 JavaScript 语言中,需要一种不可变批量二进制数据的容器。目前不存在这样的容器,但不可变的 ArrayBuffer 将提供必要的基础机制。

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

    Limited ArrayBuffer,尤其是 issue #16

    Readonly Collections,尤其是 issue #10

    wasm zero copy issue #1162 comment

    w3c TPAC talk Zero-copy operations on the web

    web-bluetooth read-only ArrayBuffer,尤其是 issue #300

    gpuweb issue #2072, issue #747SharedValueTable proposal

    webidl Frozen Array

    webcodecs issue #80, issue #104issue #212

    web transport issue #131

    whatwg streams issue #495

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

    w3c machine learning workshop issue #93

    Deno 打算支持

    Proposal Import Buffer 依赖于 Immutable ArrayBuffer

    解决方案

    本提案在 ArrayBuffer.prototype 上引入了额外的方法和只读访问器属性,与上述内容自然契合。正如缓冲区可以调整大小或不可调整,以及分离或未分离,本提案也允许缓冲区是不可变的或可变的。正如 transferToFixedSize 将原始缓冲区的内容移动到新创建的不调整大小的缓冲区,本提案提供了一种转移操作,将原始缓冲区的内容转移到新创建的不可变缓冲区。总之,本提案仅在 ArrayBuffer.prototype 上添加一个只读访问器:

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

    以及两个方法:

    • transferToImmutable(newByteLength?: number) :ArrayBuffer -- 将原始缓冲区的内容移动到新的不可变缓冲区,分离原始缓冲区,并返回新缓冲区。
    • sliceToImmutable(start?: number, end?: number) :ArrayBuffer -- 根据原始缓冲区内容的一部分创建新的不可变缓冲区,以允许实现轻松最小化甚至有时消除复制。

    不可变缓冲区不能被分离、调整大小或进一步转移。其 maxByteLength 与其 byteLength 相同。使用不可变缓冲区作为后备存储的 DataViewTypedArray 可以被冻结并不可变。冻结且不可变的 ArrayBufferDataViewTypedArray 可以放置在 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/transpiler 实现

    原生实现

    跟踪问题待添加:

    • JavaScriptCore
    • SpiderMonkey
    • XS
    • V8

    问答

    为什么不可变 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 全体会议所同意的,遵循默认假原则,诸如 if (buf.immutable) { 之类的特性测试在尚未实现此提案的引擎上将返回 falsy。

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

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