ECMAScript proposal: Method .item() for Arrays, Typed Arrays, and strings

[2020-09-23] dev, javascript, es proposal
(Ad, please don’t block)

The ECMAScript proposal “.item() (by Shu-yu Guo and Tab Atkins) introduces the mentioned method for indexable values (Arrays, Typed Arrays, strings). Given an index, the method returns the corresponding element. Its key benefit is that indices can be negative (-1 gets the last element, -2 the second last, etc.). This blog post examines .item() in detail.

Update November 2020: The method name .item() ended up not being web-compatible. The tentative new name is .at().

Method item() for indexable classes  

Method .item() works as follows for Arrays:

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

That is, the following two expressions are roughly equivalent:

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

The previous two lines demonstrates the key benefit of .item(): We can use negative indices that access the elements at the end of an Array. Other Array methods such as .slice() already support negative indices (while the square bracket operator [] doesn’t):

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

Out-of-bounds indices  

If an index is out of bounds, the square bracket operator accesses the value of a non-index property:

// 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');

The range of Array indices is [0, 2^32^−1) (the maximum Array length 2^32^−1 is excluded).

In contrast, .item() returns undefined in such a case:

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

That makes working with indices a little bit safer.

Classes that have method .item()  

The proposal is to add method .item() to all indexable classes and primitive types of ECMAScript:

  • Array
  • All Typed Array classes: Uint8Array etc.
  • string

Additionally, several DOM classes already have that method:

  • HTMLCollection (dynamic, returned by .getElementsByClassName(), .getElementsByTagName(), etc.)
  • NodeList (static, returned by .querySelectorAll())
  • DOMTokenList (static, value of .classList)
  • And others

Accessing the elements at the end of an Array – alternatives to .item()  

When it comes to negative indices, there are alternatives to .item(), but they are clumsy and/or inefficient:

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');

If we need to get the last element of an Array and don’t mind that element being removed in the process, then we can also use .pop():

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

A polyfill for .item()  

This is how we could polyfill .item() (a slightly edited version of 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 packages with polyfills  

The following two npm packages provide polyfills:

.item() and upgrading indexable DOM collections  

A current plan for the DOM is to base existing and upcoming indexable DOM data structures such as HTMLCollection and NodeList on ObservableArray. Instances of that class are proxies for Arrays and therefore have methods such as .map() that are currently not available in indexable DOM data structures. Then it will no longer be necessary to convert these data structures to Arrays before using these methods:

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

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

All indexable DOM data structures have the method .item(). For various reasons, the easiest way to make ObservableArray compatible with them, was to add this method to Array.

Example: .replace() with callback  

The following code shows where .item() is useful:

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 is always the last parameter of the .replace() callback. This is a workaround if .item() isn’t available in line A:

const groups = args.pop();

FAQ  

Why not allow negative indices in brackets?  

Unfortunately, JavaScript can’t be changed to allow negative indices in square brackets. The problem is that such a change would break existing code.

Let’s look at a few examples. I don’t recommend the techniques they are using, but similar code does exist on the web. Each one would break if negative indices were allowed in brackets.

First example:

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);

Second example:

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

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

Third example:

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]);

Why not .getItem() and .setItem()?  

As explained in Sect. “.item() and upgrading indexable DOM collections”, the method name .item() helps with upgrading the DOM. That’s why this name was preferred over alternatives.

What about the ECMAScript proposal for .last?  

There is also a stage 1 proposal for a getter/setter .last. However, quoting that proposal:

Other proposals (Array.prototype.item and Array Slice Notation) also sufficiently solve this problem, and are advancing quicker through the standards track. Should one of these proposals advance to Stage 3, this proposal will be dropped.

Sources and acknowledgements  

Sources of this blog post:

The following people contributed to this blog post via discussions on Twitter: