Tweet

The `item` Method for Indexables

New at Stage 2, the item proposal will add an item method to strings, arrays, and typed arrays (collectively, "indexables". Let's take a look!

(Stage 2 proposals are works-in-progress. For example: just five hours or so after this was first posted, a commit updating the out-of-bounds behavior got merged; this post has been updated to match the improved behavior.)

The Basics

In its most basic form, the item method accepts an index and gives you the item at that index:

console.log("abc".item(1));                  // "b"
console.log(["zero", "one", "two"].item(1)); // "one"

So far, it's just like using []:

console.log("abc"[1]);                       // "b"
console.log(["zero", "one", "two"][1]);      // "one"

So what's the point? There are actually two points:

  1. Negative indexes! (And better out-of-bounds behavior)
  2. DOM collections

Let's look at them separately...

Negative Indexes

People have often wanted a last method or similar, not least for getting the last item in an array you get back from a function call, since as it stands today you have to save the return value somewhere so you can use length:

const result = theFunction();
const last = result[result.length - 1];

With item, there's no need for the result constant, and it's more concise as well:

const last = theFunction().item(-1);

Negative indexes are offsets from the end of the indexable, so you don't have to explicitly write the length part.

With the proposal as it is, the code above will work with almost all the indexables: strings, arrays, and typed arrays. (More about almost below.)

Out of Bounds Behavior

You might be wondering what happens if you go outside the indexable, for instance ["a", "b", "c"].item(-7) or "abc".item(8). The answer is: you get undefined. (In an earlier version of the proposal, going off the beginning got clamped to index 0 and going off the end worked like [], but that was changed.)

console.log(["a", "b", "c"].item(-7));  // undefined
console.log("abc".item(8));             // undefined

item's logic is (roughly):

// (Illustrative only; NOT a polyfill.)
function item(index) {
    const len = ToLength(this.length);      // (Clamps to the range 0..(2**53)-1, inclusive)
    const relativeIndex = ToInteger(index); // (Converts to number if needed, strips fractional portion off)
    const k = relativeIndex >= 0
        ? relativeIndex
        : len + relativeIndex;
    if (k < 0 || k >= len) {
        return undefined;
    }
    return this[k];
}

That's different from [], and less surprising. [] will happily go look up the property name you give it, because remember it's a property lookup, even on an array; in specification terms, arrays are objects with special treatment for properties whose names are numbers in standard form denoting the values 0 through (2**32)-2, inclusive. In the unlikely-but-all-too-real situation that you have a property on your array called "-4" (probably as the result of a bug), [] will give you the value of that property if you use a[-4], but item won't:

const a = ["zero", "one", "two"];
a[-4] = "negative four";
// Directly accessing the property `"-4"`:
console.log(a[-4]);         // "negative four"
// Using `item` to look for the value at index `-4` (`a.length - 7`):
console.log(a.item(-7));    // undefined

Similarly, if you have a property with a name that's a large positive number but not an array index (because it's greater than (2**32)-2), [] will return that property's value but item will give you undefined:

const a = ["zero", "one", "two"];
// Add a property to it with an index-looking (but invalid) name
const name = 1000000000000000000;
a[name] = "one quintillion";
console.log(a.length);      // 3, because the property isn't an array entry
// Directly accessing the property:
console.log(a[name]);       // "one quintillion"
// Using `item` to try to access the entry at index `1000000000000000000`
// (which is an invalid index)
console.log(a.item(name));  // undefined

Note that item supports the full integer index range, not just the array index range; indexes can be in the range 0 <= index < (2**53)-1. That makes sense; although arrays are limited to a 32-bit length (so valid array indexes are 0 <= index < (2**32)-1), other indexables like strings or other array-like objects aren't.

Making item only access within the range 0 <= index < length keeps it consistent with array methods that also ignore properties with names like "-4" and "1000000000000000000". For example, if you used map on the array in the last two examples, it would only visit three entries, and produce a new array without the "-4" or "1000000000000000000" properties.

String's item: Code Units, or Code Points?

TL;DR: it's code units, like [] and charAt.

You may remember that strings are, effectively, UTF-16 (hand-waves details; but see Chapter 10 if you're interested). Each element in a string is a Unicode code unit. Sometimes that single code unit defines a Unicode code point (very loosely, "character"), but other times it's just half of a surrogate pair that, taken together, defines a code point. "a" is a string with a single code unit defining a code point. "😄" is a string with two code units defining a single code point (U+1F604). Some recent additions to strings have worked with code points rather than code units — most notably iteration: when you iterate through a string, you go through its code points, not its code units like you would with a for loop increasing an index by 1 each time. (More on that in Chapter 10, too.)

Given that, it's reasonable to wonder whether item on strings works in code points or code units. It works in code units, like []. I haven't seen anything specifically addressing why, but we can make a couple of educated guesses:

  • It would be really confusing if str[x] and str.item(x) gave you different results when x is in range.
  • The indexes you supply to (and get from) all the other string methods (even codePointAt) are expressed in code units, not code points.
  • It would be costly in terms of performance. Since a code point in UTF-16 might take up one or two elements in a string, in order to get the nth code point of a string, you have to start at the beginning and iterate until you've found enough code points. .item(3) and .item(300000) would have radically different performance. Making item work at the code unit level keeps it linear like [] is.

DOM Collections

The second motivation for item is DOM collections. What you get from querySelectorAll or getElementsByTagName isn't an array; querySelectorAll returns a NodeList and getElementsByTagName returns an HTMLCollection. There's an ongoing effort to make it possible, at least in new APIs, for hosts like browsers to provide JavaScript code with arrays instead, but ones where the host can intercept assignments and such to do type checks, etc. But the goal is also, where possible, to extend that even to older APIs — to make querySelectorAll return an array rather than a NodeList, for instance. That's almost possible now, except that DOM collections have an item method that arrays don't have. Solution? Give arrays (and other indexables like strings) an item method! (If you want to read more on this, the section in the proposal explainer provides good detail.)

What Do You Mean "Almost" All Indexables?

Earlier I said that item was being added to "...almost all the indexables: strings, arrays, and typed arrays..." But there's a fourth indexable in JavaScript: arguments. You remember arguments, it's that automatic pseudo-array of a function's arguments:

function example() {
    console.log(arguments[0]);
}
example(1); // Shows 1

arguments objects aren't arrays. They don't inherit from Array.prototype so they don't have map and filter. They don't even have slice, which all the other indexables have.

And as the proposal stands currently, they won't have item, either. After all, arguments is basically deprecated by rest parameters, so there's almost never a need to use arguments in new code. The item proposal is about making the semantics/ergonomics of new code better. So...why add it to arguments?

There are pros and cons, and there's an ongoing discussion, but at least at the moment, it looks like arguments won't get item. And while I slightly prefer adding item to arguments for consistency, I also understand the reasons for leaving it out, especially the argument that new code should almost never use arguments.

Wrapup

item just arrived at Stage 2. Its various details could change further as it progresses, although it's quite simple and can only change in a few ways without losing its DOM-compatibility appeal. I won't be surprised to see it progress quickly, and I'm sure I'll be using it once I can. It's a nice, simple addition to the standard library that might even play a role in making working with the DOM better. And that's a worthy goal.

Happy Coding!

Have a question about or comment on this post? Tweet me!