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/4/proposal-relative-indexing-method.md.
  • 简体中文
  • .at() S4

    提案概览
    提案速览

    该提案为 Array、String 和 TypedArray 添加一个 .at() 方法,支持类似于 Python 的负索引。该方法接受一个整数;负索引从末尾开始计数,越界访问返回 undefined。它最初提议命名为 .item(),但因库的鸭子类型判断而被发现与 Web 不兼容,因此更名为 .at()

    Note

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

    在所有内置可索引对象上添加 .at() 方法的提案

    一份 TC39 提案,旨在为所有基本可索引类(Array、String、TypedArray)添加一个 .at() 方法。

    阶段:4

    提案人:Tab Atkins, Shu-yu Guo

    提案规范文本: https://tc39.github.io/proposal-relative-indexing-method/

    目录

    1. 动机
      1. 现有方法
    2. 拟议修改
    3. Polyfill
    4. Web 不兼容性历史
      1. DOM 理由
        1. 可转换接口
      2. 可能的问题
        1. 可能的 DOM 兼容性问题

    动机

    多年来,程序员一直要求能够对 JS 数组进行“负索引”,就像在 Python 中那样。也就是说,希望能够编写 arr[-1] 而不是 arr[arr.length-1],其中负数从最后一个元素开始倒数。

    不幸的是,JS 的语言设计使得这不可能实现。[] 语法并非数组和字符串独有;它适用于所有对象。通过索引引用一个值,例如 arr[1],实际上只是引用了对象上键为 "1" 的属性,任何对象都可以拥有该属性。因此,arr[-1] 在今天代码中已经“有效”,但它返回的是对象“-1”属性的值,而不是从末尾开始计数的索引。

    已经有许多尝试来解决这个问题;最近的一个是限制性提案,旨在通过一个 .last 属性来更方便地访问数组的最后一个元素(https://github.com/tc39/proposal-array-last)。

    本提案采用了一种更常见的方法,建议为 Array、String 和 TypedArray 添加一个 .at() 方法,该方法接受一个整数值并返回该索引处的项,并具有上述的负数语义。

    这不仅以简单的方式解决了长期以来的需求,而且碰巧也解决了各种 DOM API 的单独问题,如下所述

    现有方法

    目前,要从可索引对象的末尾访问一个值,常见做法是编写 arr[arr.length - N],其中 N 是从末尾开始的第 N 个项(从 1 开始)。这需要两次命名可索引对象,另外增加了 7 个字符的 .length,并且对匿名值不友好;除非先将函数返回值存储在临时变量中,否则无法使用此技术来获取最后一项。

    另一种避免了其中一些缺点,但自身也有一些性能缺点的方法是 arr.slice(-N)[0]。这避免重复名称,因此也对匿名值友好。但是,拼写有些奇怪,特别是尾部的 [0](因为 .slice() 返回一个数组)。此外,会创建一个临时数组,包含从所需项到末尾的所有源内容,仅在检索第一项后立即丢弃。

    但请注意,.slice()(以及类似的方法,如 .splice())已经具有负索引的概念,并且完全按照期望的方式解析它们。

    可能的问题

    .at() 也可能由于尚未可知的原因导致 Web 不兼容。

    拟议修改

    https://tc39.github.io/proposal-relative-indexing-method/

    Polyfill

    (粗略的 polyfill;对于行为良好的对象能正确实现行为,但不能保证在边缘情况下(例如在 undefined 上调用方法)与规范行为完全一致。)

    function at(n) {
    	// ToInteger() 抽象操作
    	n = Math.trunc(n) || 0;
    	// 允许从末尾进行负索引
    	if (n < 0) n += this.length;
    	// 越界访问保证返回 undefined
    	if (n < 0 || n >= this.length) return undefined;
    	// 否则,这只是正常的属性访问
    	return this[n];
    }
    
    const TypedArray = Reflect.getPrototypeOf(Int8Array);
    for (const C of [Array, String, TypedArray]) {
        Object.defineProperty(C.prototype, "at",
                              { value: at,
                                writable: true,
                                enumerable: false,
                                configurable: true });
    }

    实现

    Web 不兼容性历史

    本提案的原始迭代提出了方法名为 .item()。不幸的是,这被发现是与 Web 不兼容的。一些库,特别是 YUI2 和 YUI3,正在基于 .item 属性的存在来对对象进行鸭子类型判断,将其视为 DOM 集合。请参阅 #28#31#32 了解更多详情。

    下面记录了选择 .item() 名称的原始动机和原始担忧。

    DOM 理由

    WebIDL 规范最近添加了 ObservableArray<>(感谢 @domenic!),它是 Array 上的代理,允许 Web API 公开某些对页面作者来说看起来完全像 Array 的内容,但仍然允许浏览器拦截索引属性的 get/set/delete 等操作,并像现在对命名属性那样执行类型检查和其他要求。

    我们计划开始将其用于大多数希望公开列表内容的 API,但我们还想在可能的情况下,升级较旧的 API 也使用它;许多旧 API 使用定制的接口,这些接口糟糕且不完整地复制了 Array 接口,这一直是 Web 作者不满意的根源。

    (例如,document.querySelectorAll() 返回的不是 Array,而是 NodeList,它支持索引属性和 .length,因此可以以基本方式视为 Array,但只有极少数功能的 Array 原型方法。像 .map() 这样的常用方法会缺失,要求作者编写类似 [...document.querySelectorAll("a")].map(foo) 的代码。)

    这种升级几乎可以原地进行,只需将各种定制接口替换为 ObservableArray,而不会破坏任何明确测试值的类型的代码。 只有一个例外:它们都有一个 .item() 方法,该方法返回传入索引处的值。

    (这是非常古老(1990 年代)观念的残余,即认为 Java 是在 Web 上使用的合理语言,因此 API 设计为一种“最低公分母”风格,以便在 JS 和 Java 中使用。当时,除非你实际上是 Java 数组,否则 Java 无法使用索引属性,因此 .item() 方法是一种折衷方案,可以在两种语言中以相同方式工作。)

    很可能存在依赖在这些接口上使用 .item() 的代码,我们不想冒险在那里破坏。

    我们可以通过子类化 ObservableArray 并在子类中添加 .item() 来解决这个问题。但是,这意味着这些值不是 Array 类型;社区中寻找 Array 的各种类型检查方法将会失败。

    或者,我们可以直接在 ObservableArray 本身中添加 .item(),因为它是围绕 Array 的代理包装器。然而,这会令人困惑和奇怪,使 Array 看起来有这样一个方法,即使它不在原型上。

    对我们来说,理想的解决方案是在 Array 原型本身上添加 .item(),并且为了完整性和一致性,也要在支持相同套件的与索引相关属性(如 .slice())的其他可索引类型上添加。

    因此,.item() 这个名称是本提案的要求;将其更改为其他名称仍然会帮助作者,但将无法满足 DOM 的需求。

    可转换接口

    假设本提案被采纳,以下遗留接口应可升级为 ObservableArray:

    (也许还有其他的,列表正在持续更新中)

    可能的问题

    与此类内置对象的任何添加一样,一个明显的迫在眉睫的问题是,.item() 这个名称可能已经被某个框架添加到了这些类的原型上,且定义不兼容,并且是使用某种脆弱的模式添加的,以避免覆盖内置名称,这样一来,依赖框架定义的代码将在获得新的内置定义时被破坏。

    我准备收回我的话,但我怀疑任何向 Array 或其他可索引对象添加 .item() 方法的库都会赋予它与此处概述的语义兼容或相同的语义;我想象不到这样一个方法名还能对应什么别的含义。

    有很好的证据表明我们在这里可能是安全的:MooTools、Prototype 或 Ext 都没有向 Array 添加 .item();这些通常是此类添加最危险的库(参见:smooshgate),所以如果我们在这些库上是安全的,那么我们在一般情况下很可能是安全的。

    可能的 DOM 兼容性问题

    所有上面列出的接口定义的 .item() 方法都有一个共同的结构:

    	SomeType? item(unsigned long index);

    如果你沿着 WebIDL 中的定义链走下去,最终会得到一个将 unsigned long 转换为内部数字的算法,该算法大部分与 JS 在 .slice() 中对索引的处理方式匹配,但在边缘有一些差异:

    • WebIDL 将 Infinity 和 -Infinity 视为 0,而 JS 则保持原样。
    • WebIDL 将值对 2^32 取模(使用 JS 的“模”数学运算符,因此负数变为正数),而 JS 则不是。
    • 如果索引超出范围,WebIDL 返回 null,而 JS 返回 undefined

    第一个意味着,意外将无穷大传递给 .item() 并依赖它返回索引 0 处的项的代码将会中断,因为它现在将获得 undefined。我认为这不太可能成为问题。

    第二个意味着,传递略大于 2^32 的巨大数字(因此取模会将它们带回到合理的索引)并依赖它从列表中返回某些内容的代码将会中断,因为它现在将获得 undefined。我同样认为这不太可能成为问题。

    第二个也意味着,依赖小的负数被取模到 40 亿附近从而返回 null 的代码将会中断,因为它现在将从列表末尾附近返回项。我认为这有轻微的可能性,并且相信我们将需要进行一些工具测试,以确保其低于我们的破坏阈值。(有一种很小的可能性是,此类代码期望从列表末尾接收值并且当前是有缺陷的,此更改将修复它。)

    第三个意味着,通过显式将返回值与 null 比较来测试项目是否存在的代码将会中断,因为它现在会收到 undefined 并认为有返回值。我也认为这有轻微的可能性。然而,根据我的经验,此类代码大多写为 == null 或者简单地使用返回值的真值性(因为有效索引总是返回一个对象,这是真值);这两种测试在此更改后都将继续有效。


    我们也许可以在试图完全接受此提案之前预览这些更改中的任何一个,这样我们就能知道进行此类升级是否现实,从而知道 .item() 名称是硬性要求还是可以自由讨论。

    特别是,测试负索引会相当简单,只需要修改 signed long 并在方法算法中增加一行。

    测试返回 undefined 也是可行的;虽然在 WebIDL 中表达起来仍然有点笨拙(需要将返回类型写成 any),但对方法算法的改动很小。