ECMAScript 提案:Array, 类数组和字符串的 .item() 方法

ECMAScript 提案:Array, 类数组和字符串的 .item() 方法

由 Shu-yu Guo 和 Tab Atkins 提出的 ECMAScript 提案“.item()” 为可索引对象(Array, 类数组,string)推出的方法。给定一个索引,该方法返回符合的值。该方法的关键好处是索引可以是负值(-1 获取最后一个元素,-2 对应倒数第二个元素,等等)。

2020 年 11 月更新:名为 .item() 的方法已经结束支持。实验性的新名称是 .at()

可索引类的方法 item()

方法 .item() 在数组中有以下用法:

const arr = ['a', 'b', 'c', 'd'];
assert.equal(arr.item(1), 'b');
assert.equal(arr.item(0), 'a');
assert.equal(arr.item(-1), 'd');

于是,下面的两个表达式是等价的:

arr.item(-1);
arr[arr.length - 1];

前面两行展示了 .item() 的关键好处:我们可以使用负索引值来访问一个 Array 末端的元素。其它的 Array 方法如 .slice() 已经支持负指数(而方括号操作符 [] 不支持)。

['a', 'b', 'c', 'd'].slice(1, -1);
// -> [ 'b', 'c' ]
['a', 'b', 'c', 'd'].slice(-1);
// -> [ 'd' ]

索引溢出

如果索引溢出,使用方括号操作符访问溢出索引值属性的值:

// Set up an Array
const arr = [];
arr['4294967296'] = 'abc';

// `arr` has no indexed properties
assert.equal(arr.length, 0);

// Index 4294967296 is out of bounds
assert.equal(arr[4294967296], 'abc');

数组索引范围 是 [0, 2^32 − 1)(最大数组长度 2^32 − 1 不包含在内)。

相比之下,.item() 在上面的用例中返回 undefined

assert.equal(arr.item(4294967296), undefined);

这使得程序索引安全一些。

拥有方法 .item() 的类

提案是添加方法 .item()可索引类和 ECMAScript 原始类型:

  • Array
  • 所有的类数组:Uint8Array
  • string

此外,几个 DOM 类同样拥有该方法:

  • HTMLCollection(动态的,由 .getElementsByClassName().getElementsByTagName() 等返回)。
  • NodeList(静态的,由 .querySelectorAll() 返回)。
  • DOMTokenList(静态的,.classList 的值)。
  • 其它情况

访问数组末端元素 - 可用于替换 .item()

当想要访问负值索引,有一些替换 .item() 的方法,但是笨拙且难用:

const arr = ['a', 'b', 'c', 'd'];
const N = -2;

const element1 = arr[arr.length + N];
assert.equal(element1, 'c');

const element2 = arr.slice(N)[0];
assert.equal(element2, 'c');

const { length, [length + N]: element3 } = arr;
assert.equal(element3, 'c');

当我们需要获取数组的末端元素,并且不介意删除它的时候,也可以使用 .pop()

const lastElement = arr.pop();
assert.equal(lastElement, 'd');

.item() polyfill

这有我们可以填充使用 .item()(一份频繁编辑版本 code shown in the proposal):

function item(n) {
  // ToInteger() abstract operation
  n = Math.trunc(n) || 0;

  // Allow negative indexing from the end
  if (n < 0) n += this.length;

  // Out-of-bounds access is guaranteed to return undefined
  if (n < 0 || n >= this.length) return undefined;

  // Otherwise, this is just normal property access
  return this[n];
}

// Other TypedArray constructors omitted for brevity.
for (const C of [Array, String, Uint8Array]) {
  Object.defineProperty(C.prototype, 'item', {
    value: item,
    writable: true,
    enumerable: false,
    configurable: true,
  });
}

npm 包填充器

下面两个 npm 包提供支持:

.item() 和升级中的可索引 DOM 集合

目前 DOM 的一个计划是将现有的和即将到来的可索引 DOM 集合(如 HTMLCollectionNodeList)基于 ObservableArray。该类的实例是数组的代理,因此具有 .map() 等方法,而这些方法目前在可索引 DOM 数据结构中是不可用的。那么在使用这些方法之前,将不再需要将这些集合转换为数组:

// Old:
[...document.querySelectorAll('img')].map((img) => img.src);

// New:
document.querySelectorAll('img').map((img) => img.src);

所有可索引的 DOM 数据结构都有 .item() 这个方法。由于各种原因,使 ObservableArray 与它们兼容的最简单的方法,是将这个方法添加到 Array 中。

例子:与 .replace() 回调函数一起使用

下面的代码显示 .item() 是有用的:

const result = 'first=jane, last=doe'.replace(
  /(?<key>[a-z]+)=(?<value>[a-z]+)/g,
  (...args) => {
    const groups = args.item(-1); // (A)    const { key, value } = groups;
    return key.toUpperCase() + '=' + value.toUpperCase();
  },
);
assert.equal(result, 'FIRST=JANE, LAST=DOE');

groups 一直是 .replace() 回调函数的最后一个参数。如果在行 A 中 .item() 不可用,同样有效的是:

const groups = args.pop();

问题解释 FAQ

为什么不允许在括号中使用负值索引?

不幸的是,JavaScript 不会改变以允许在方括号中使用负值索引。问题是因为这么做会破环现有代码。

我们来看一些代码。我不推荐使用这些技术,但是相似的代码存在于网络上。如果负值索引被允许在方括号中使用,每种情况都会被破坏。

第一个例子:

const english = ['hello', 'world'];
const german = ['hallo', 'Welt'];

function translate(word) {
  return german[english.indexOf(word)];
}

assert.equal(translate('world'), 'Welt');
assert.equal(translate('universe'), undefined);

第二个例子:

const arr = ['fee', 'fi', 'fo', 'fum'];
arr['-1'] = 'Englishman';

// Current behavior:
assert.equal(arr[-1], 'Englishman');

第三个例子:

const numbers = [1, 2, 3];
const reversed = [];

let i = numbers.length - 1;
while (numbers[i]) {
  reversed.push(numbers[i--]);
}

assert.deepEqual(reversed, [3, 2, 1]);

为什么不是 .getItem().setItem()

正如在章节“.item() 和升级中的可索引 DOM 集合”中解释过,方法名 .item() 有助于升级 DOM。这就是为什么这个名字比其它名字更受欢迎。

什么是 ECMAScript 提案 .last

也有 stage 1 提案 a getter/setter .last。然而,引用该提案:

其它提案(Array.prototype.itemArray Slice Notation)也充分解决这个问题,并在标准轨道上更快地推进。如果其中一个提案进入 stage 3,这个提案将被放弃。

资源

这篇文章引用的资源有:

本文作者 Axel Rauschmayer,转载请注明来源链接:

原文链接:https://2ality.com/2020/09/item-method.html

本文链接:https://tie.pub/2020/10/item-method/