Free email newsletter: “ES.next News

2014-05-08

ECMAScript 6’s new array methods


Check out my book (free online): “Exploring ES6”. Updated version of this blog post: chapter “New Array features”.


This blog post explains what new array methods ECMAScript 6 will bring and how to use them in current browsers.

Note: I’m using the terms constructor and class interchangeably.

Class methods

Array has gained methods of its own.

Array.from(arrayLike, mapFunc?, thisArg?)

Array.from()’s basic functionality is to convert two kinds of objects to arrays:

  • Array-like objects, which have a property length and indexed elements. Examples include the results of DOM operations such as document.getElementsByClassName().
  • Iterable objects, whose contents can be retrieved one element at a time. Arrays are iterable, as are ECMAScript’s new data structures Map and Set.

The following code is an example of converting an array-like object to an array:

    let lis = document.querySelectorAll('ul.fancy li');
    Array.from(lis).forEach(function (li) {
        console.log(node);
    });

The result of querySelectorAll() is not an array and does not have a forEach() method, which is why we need to convert it to an array before we can use that method.

Mapping via Array.from()

Array.from() is also a convenient alternative to using map() generically:

    let spans = document.querySelectorAll('span.name');
    
    // map(), generically:
    let names1 = Array.prototype.map.call(spans, s => s.textContent);
    
    // Array.from():
    let names2 = Array.from(spans, s => s.textContent);

The second parameter of both methods is an arrow function.

In this example, the result of document.querySelectorAll() is again an array-like object, not an array, which is why we couldn’t invoke map() on it. Previously, we converted the array-like object to an array in order to call forEach(). Here, we skipped that intermediate step via a generic method call and via the two-parameter version of Array.from().

Holes

Array.from() ignores holes [3] in arrays, it treats them as if they were undefined elements:

    > Array.from([0,,2])
    [ 0, undefined, 2 ]

That means that you can use Array.from() to create and fill an array:

    > Array.from(new Array(5), () => 'a')
    [ 'a', 'a', 'a', 'a', 'a' ]
    > Array.from(new Array(5), (x,i) => i)
    [ 0, 1, 2, 3, 4 ]

If you want to fill an array with a fixed value (first one of the previous two examples) then Array.prototype.fill() (see below) is a better choice.

from() in subclasses of Array

Another use case for Array.from() is to convert an array-like or iterable object to an instance of a subclass of Array. For example, if you create a subclass MyArray of Array (subclassing arrays is explained in [1]) and want to convert such an object to an instance of MyArray, you simply use MyArray.from(). The reason that that works is because constructors inherit from each other in ECMAScript 6 (a super-constructor is the prototype of its sub-constructors).

    class MyArray extends Array {
        ...
    }
    let instanceOfMyArray = MyArray.from(anIterable);

You can also combine this functionality with mapping, to get a map operation where you control the result’s constructor:

    // from() – determine the result’s constructor via the receiver
    // (in this case, MyArray)
    let instanceOfMyArray = MyArray.from([1, 2, 3], x => x * x);
    
    // map(): the result is always an instance of Array
    let instanceOfArray   = [1, 2, 3].map(x => x * x);

Array.of(...items)

If you want to turn several values into an array, you should always use an array literal, especially since the array constructor doesn’t work properly if there is a single value that is a number (more information on this quirk):

    > new Array(3, 11, 8)
    [ 3, 11, 8 ]
    > new Array(3)
    [ , ,  ,]
    > new Array(3.1)
    RangeError: Invalid array length

But how are you supposed to turn values into an instance of a sub-constructor of Array then? This is where Array.of() helps (remember that sub-constructors of Array inherit all of Array’s methods, including of()).

    class MyArray extends Array {
        ...
    }
    console.log(MyArray.of(3, 11, 8) instanceof MyArray); // true
    console.log(MyArray.of(3).length === 1); // true

Array.of() is also handy as a function that doesn’t have Array()’s quirk related to wrapping values in arrays. However, be careful about an Array.prototype.map() pecularity that can trip you up here:

    > ['a', 'b'].map(Array.of)
    [ [ 'a', 0, [ 'a', 'b' ] ],
      [ 'b', 1, [ 'a', 'b' ] ] ]
    > ['a', 'b'].map(x => Array.of(x)) // better
    [ [ 'a' ], [ 'b' ] ]
    > ['a', 'b'].map(x => [x]) // best (in this case)
    [ [ 'a' ], [ 'b' ] ]

As you can see above, map() passes three parameters to its callback, the last two are simply often ignored (details).

Prototype methods

Several new methods are available for array instances.

Iterating over arrays

The following methods help with iterating over arrays:

  • Array.prototype.entries()
  • Array.prototype.keys()
  • Array.prototype.values()

The result of each of the aforementioned methods is a sequence of values, but they are not returned as an array; they are revealed one by one, via an iterator. Let’s look at an example (I’m using Array.from() to put the iterators’ contents into arrays):

    > Array.from([ 'a', 'b' ].keys())
    [ 0, 1 ]
    > Array.from([ 'a', 'b' ].values())
    [ 'a', 'b' ]
    > Array.from([ 'a', 'b' ].entries())
    [ [ 0, 'a' ],
      [ 1, 'b' ] ]

You can combine entries() with ECMAScript 6’s for-of loop [2] and destructuring to conveniently iterate over (index, element) pairs:

    for (let [index, elem] of ['a', 'b'].entries()) {
        console.log(index, elem);
    }

Note: this code already works in the current Firefox.

Searching for array elements

Array.prototype.find(predicate, thisArg?)
returns the first array element for which the callback predicate returns true. If there is no such element, it returns undefined. Example:

    > [6, -5, 8].find(x => x < 0)
    -5
    > [6, 5, 8].find(x => x < 0)
    undefined

Array.prototype.findIndex(predicate, thisArg?)
returns the index of the first element for which the callback predicate returns true. If there is no such element, it returns -1. Example:

    > [6, -5, 8].findIndex(x => x < 0)
    1
    > [6, 5, 8].findIndex(x => x < 0)
    -1

Both find* methods ignore holes [3]. The full signature of the callback predicate is:

    predicate(element, index, array)
Finding NaN via findIndex()

A well-known limitation of Array.prototype.indexOf() is that it can’t find NaN, because it searches for elements via ===:

    > [NaN].indexOf(NaN)
    -1

With findIndex(), you can use Object.is() [4] and will have no such problem:

    > [NaN].findIndex(y => Object.is(NaN, y))
    0

You can also adopt a more general approach, by creating a helper function elemIs():

    > function elemIs(x) { return Object.is.bind(Object, x) }
    > [NaN].findIndex(elemIs(NaN))
    0

Array.prototype.fill(value, start?, end?)

Fills an array with the given value:

    > ['a', 'b', 'c'].fill(7)
    [ 7, 7, 7 ]

Holes [3] get no special treatment:

    > new Array(3).fill(7)
    [ 7, 7, 7 ]

Optionally, you can restrict where the filling starts and ends:

    > ['a', 'b', 'c'].fill(7, 1, 2)
    [ 'a', 7, 'c' ]

When can I use the new array methods?

References

[1]: Subclassing builtins in ECMAScript 6 [2]: Iterators and generators in ECMAScript 6 [3]: “Holes in Arrays” (Speaking JavaScript) [4]: Stricter equality in JavaScript

No comments: