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/proposal-collection-normalization.md.
  • 简体中文
  • collection normalization S2

    中文标题:集合归一化

    提案概览
    提案速览

    该提案为 Map 和 Set 构造函数引入可选的 coerceKeycoerceValue 钩子,允许在插入时自动归一化或验证键和值。它通过提供集中拦截点来解决样板代码和脆弱基类问题,并支持专用映射和检查键等用例。该提案处于早期阶段,寻求反馈和改进。

    Note

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

    集合 {coerceKey, coerceValue}

    本提案旨在为集合创建添加 coerceKeycoerceValue 参数。

    渲染规范

    动机

    各种数据结构在创建后使用时,预期对其键和/或值应用一致的约束或归一化。为了确保对这类数据类型进行归一化,需要在将数据插入数据结构之前应用归一化。当前的工作流程是手动确保所有插入数据的位置都能正确归一化数据。

    这导致各种问题,例如脆弱的样板代码问题,如:归一化未应用于所有插入位置,或归一化在不同插入位置之间不一致。

    本提案确实添加了围绕这些数据结构中存储值的新能力。目前,调用各种内置方法可以避开任何在插入时拦截和归一化数据的机制,例如在像 Map 这样的结构上调用 Map.prototype.set.call(mapOfStringsToStrings, number),这阻止了对 Map 包含一致数据保证的承诺。

    现实世界中有许多在插入时归一化数据的结构示例:

    将参数名和值都转换为字符串。

    验证并将头名称和值转换为字符串。

    将头名称和值转换为字符串。

    使用 append 将键限制为 length + 1

    对键执行验证。

    所有这些都通过不同的方式应用数据类型强制转换和验证插入的数据来实现类似效果。

    为了统一如何定义这些处理传入数据的通用操作,并避免样板问题和 FAQ 中提到的脆弱基类问题,提供一个拦截数据的钩子将缓解一些问题。

    使用案例

    专用映射

    给定一个包含用户对象的应用,可能希望基于用户名和电子邮件创建不同的集合。

    new Map(undefined, {
      coerceKey({email}) {
        return email;
      },
      coerceValue(state) {
        return state instanceof AccountState ? 
          state :
          new AccountState(state);
      }
    });
    new Set(undefined, {
      coerceValue({username}) {
        return username;
      }
    });

    检查键

    在对集合执行操作时检查类型是常见情况。这可以在键控过程中完成。

    new Map(undefined, {
      coerceKey(user) {
        if (user instanceof User !== true) {
          throw new TypeError('Expected User for key');
        }
        return user;
      }
    });

    DOM 示例:Headers..set

    1. 归一化值。
    1. 如果名称不是名称或值不是值,则抛出 TypeError。
    1. 如果此地的守卫是 "immutable",则抛出 TypeError。
    1. 否则,如果此地的守卫是 "request",且名称是禁止的头名称,则返回。
    1. 否则,如果此地的守卫是 "request-no-cors",且名称/值不是 no-CORS 安全列出的请求头,则返回。
    1. 否则,如果此地的守卫是 "response",且名称是禁止的响应头名称,则返回。
    1. 在此地的头列表设置名称/值。
    1. 如果此地的守卫是 "request-no-cors",则从此地移除特权 no-CORS 请求头。

    所有这些都是可交互的,如果钩子能在传入时拦截 namevalue。一般工作流程是将 namevalue 存储到后备的 header list 存储中。

    DOM 示例:SearchParams..has

    has(name) 方法步骤是,如果此地的列表中存在名为 name 的名称-值对,则返回 true,否则返回 false。

    注意,所有归一化都在此处 WebIDL 层完成以将 name 转换为 USVString,没有实际文本说明对 name 进行归一化。

    FAQ

    其他语言如何处理这种自定义键控?

    可以通过 此文档 找到一组参考文献。

    通常使用容器类型。如果你想按 person.email 创建一个 PeopleMap,你会实现一个包装类 PersonByEmail 作为键,以及用于其他方面键控的类。静态类型和编译器/语言强制转换可以缓解误用集合的问题,但在无法自动转换的动态类型场景中,包装和解包是手动的。

    本提案将提供一个钩子来执行这种手动包装和解包,而不要求集合用户在使用集合之前保持警惕地正确编组键。

    归一化步骤何时应用?

    归一化在数据传入以查找 [[MapData]] 中键位置的标识,以及将值放入 [[SetData]][[MapData]] 时应用。例如:

    const map = new Map([], {
      coerceKey: String
    });
    // stored using { [[Key]]: "1", [[Value]]: "one" } in map.[[MapData]]
    map.set(1, 'one');
    // looks for corresponding { [[Key]]: "1" } in map.[[MapData]]
    map.has(1); // true
    // functions directly exposing the underlying entry list are unaffected
    [...map.entries()]; // [["1", "one"]]
    
    const set = new Set([], {coerceValue: JSON.stringify});
    // stored using { [[Value]]: '{"path": "/foo"}' } in set.[[SetData]]
    set.add({path: '/foo'});
    // looks for corresponding { [[Value]]: '{"path": "/foo"}' } in set.[[SetData]]
    set.has({path: '/foo')};
    // functions directly exposing the underlying entry list are unaffected
    [...set]; // ['{"path": "/foo"}']

    归一化不会在迭代或返回内部数据时执行,仅对参数执行。

    为什么有人想对 Map 使用 coerceValue

    存在各种用例来规范化不同 API 中类似映射结构的值。即使它们不直接使用 Map,我们也能从现有 DOM API 中看到这种模式的实用性。

    • URLSearchParams
    • DomStringMap
    • Header

    Node 也有类似规范化值的 API,如 process.env

    规范化值还可以避免某些情况,例如通过验证错误、强制转换或其他方式将无效值放入 Map。现有类似映射的结构(如 require.cache)如果放入不正确的值可能会产生错误。规范化步骤允许映射在插入意外值时正确处理情况。

    为什么只给 Set 提供 coerceValue

    Set 是值的集合,没有从一个值到另一个值的映射操作。

    为什么不称之为 coerceKey 对于 Set?

    对其他语言的文档和 API 进行了审查,关于 Set。多数术语是 "elements";“keys” 和 “values” 也被使用。注意到每当使用 "keys" 时,"values" 也被使用,但使用 "values" 时并不总是同时使用 "keys"。为了匹配现有的 JS 术语用法,选择 "values" 作为此名称的基础。

    为什么不使用 value[Symbol.coerceKey]

    具有专门化的身份与每值类型具有多种专门化映射的想法冲突。当想要专门化基于原始值的键时也会引起冲突。

    为什么不鼓励扩展集合?

    1. 这将容易受到原型爬取的影响,例如:
    myCustomMap.__proto__.get.call(myCustomMap, key);

    这会在某种程度上使检查键类型的想法失效。

    1. 它避免了需要同步所有方法,这是一些样板代码和代码不同步的潜在位置。这也意味着即使 JS 标准库中添加了新方法,你的自定义实现也能正常工作:
    class MyMap extends Map {
      constructor([...entries]) {
        super(entries.map(...));
      }
      delete(k) { ... }
      get(k) { ... }
      has(k) { ... }
      set(k, v) { ... }
    }

    如果我们添加类似 emplace() 这样的功能,这份代码现在需要更新,否则如果人们期望它像标准 Map 一样工作,就会出错。

    这大致就是 脆弱的基类问题,其中 Map 是基类。

    1. 即使这是用户空间的解决方案,允许更容易地使用映射似乎是明智的。我们应该旨在减轻开发者的负担,而不要求所有新功能都有新的内核语义。我在此方面谈过关于 扩展标准库

    2. 组合,虽然扩展很好,但并不总是允许简单的链式和功能组合。如果我们引入 RekeyableMap 作为具体基类,它可能与其他可能引入的基类冲突,例如 InsertIfMissingMap。由于两者都是基类,它们就不允许轻松组合两个特性。