18. New Array features
Table of contents
Please support this book: buy it (PDF, EPUB, MOBI) or donate
(Ad, please don’t block.)

18. New Array features



18.1 Overview

New static Array methods:

New Array.prototype methods:

18.2 New static Array methods

The object Array has new methods.

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

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

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

const arrayLike = { length: 2, 0: 'a', 1: 'b' };

// for-of only works with iterable values
for (const x of arrayLike) { // TypeError
    console.log(x);
}

const arr = Array.from(arrayLike);
for (const x of arr) { // OK, iterable
    console.log(x);
}
// Output:
// a
// b
18.2.1.1 Mapping via Array.from()

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

const spans = document.querySelectorAll('span.name');

// map(), generically:
const names1 = Array.prototype.map.call(spans, s => s.textContent);

// Array.from():
const names2 = Array.from(spans, s => s.textContent);

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().

18.2.1.2 from() in subclasses of Array

Another use case for Array.from() is to convert an Array-like or iterable value to an instance of a subclass of Array. For example, if you create a subclass MyArray of Array 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 {
    ···
}
const 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)
const instanceOfMyArray = MyArray.from([1, 2, 3], x => x * x);

// map(): the result is always an instance of Array
const instanceOfArray   = [1, 2, 3].map(x => x * x);

The species pattern lets you configure what instances non-static built-in methods (such as slice(), filter() and map()) return. It is explained in Sect. “The species pattern” in Chap. “Classes”.

18.2.2 Array.of(...items)

Array.of(item_0, item_1, ···) creates an Array whose elements are item_0, item_1, etc.

18.2.2.1 Array.of() as an Array literal for subclasses of Array

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

18.3 New Array.prototype methods

Several new methods are available for Array instances.

18.3.1 Iterating over Arrays

The following methods help with iterating over Arrays:

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' ] ]

I could also have used the spread operator (...) to convert iterators to Arrays:

> [...['a', 'b'].keys()]
[ 0, 1 ]
18.3.1.1 Iterating over [index, element] pairs

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

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

18.3.2 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

The full signature of the callback predicate is:

predicate(element, index, array)
18.3.2.1 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() (explained in the chapter on OOP) 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

18.3.3 Array.prototype.copyWithin()

The signature of this method is:

Array.prototype.copyWithin(target : number,
    start : number, end = this.length) : This

It copies the elements whose indices are in the range [start,end) to index target and subsequent indices. If the two index ranges overlap, care is taken that all source elements are copied before they are overwritten.

Example:

> const arr = [0,1,2,3];
> arr.copyWithin(2, 0, 2)
[ 0, 1, 0, 1 ]
> arr
[ 0, 1, 0, 1 ]

18.3.4 Array.prototype.fill()

The signature of this method is:

Array.prototype.fill(value : any, start=0, end=this.length) : This

It fills an Array with the given value:

> const arr = ['a', 'b', 'c'];
> arr.fill(7)
[ 7, 7, 7 ]
> arr
[ 7, 7, 7 ]

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

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

18.4 ES6 and holes in Arrays

Holes are indices “inside” an Array that have no associated element. In other words: An Array arr is said to have a hole at index i if:

For example: The following Array has a hole at index 1.

> const arr = ['a',,'b']
'use strict'
> 0 in arr
true
> 1 in arr
false
> 2 in arr
true
> arr[1]
undefined

You’ll see lots of examples involving holes in this section. Should anything ever be unclear, you can consult Sect. “Holes in Arrays” in “Speaking JavaScript” for more information.

18.4.1 ECMAScript 6 treats holes like undefined elements

The general rule for Array methods that are new in ES6 is: each hole is treated as if it were the element undefined. Examples:

> Array.from(['a',,'b'])
[ 'a', undefined, 'b' ]
> [,'a'].findIndex(x => x === undefined)
0
> [...[,'a'].entries()]
[ [ 0, undefined ], [ 1, 'a' ] ]

The idea is to steer people away from holes and to simplify long-term. Unfortunately that means that things are even more inconsistent now.

18.4.2 Array operations and holes

18.4.2.1 Iteration

The iterator created by Array.prototype[Symbol.iterator] treats each hole as if it were the element undefined. Take, for example, the following iterator iter:

> var arr = [, 'a'];
> var iter = arr[Symbol.iterator]();

If we invoke next() twice, we get the hole at index 0 and the element 'a' at index 1. As you can see, the former produces undefined:

> iter.next()
{ value: undefined, done: false }
> iter.next()
{ value: 'a', done: false }

Among others, two operations are based on the iteration protocol. Therefore, these operations also treat holes as undefined elements.

First, the spread operator (...):

> [...[, 'a']]
[ undefined, 'a' ]

Second, the for-of loop:

for (const x of [, 'a']) {
  console.log(x);
}
// Output:
// undefined
// a

Note that the Array prototype methods (filter() etc.) do not use the iteration protocol.

18.4.2.2 Array.from()

If its argument is iterable, Array.from() uses iteration to convert it to an Array. Then it works exactly like the spread operator:

> Array.from([, 'a'])
[ undefined, 'a' ]

But Array.from() can also convert Array-like objects to Arrays. Then holes become undefined, too:

> Array.from({1: 'a', length: 2})
[ undefined, 'a' ]

With a second argument, Array.from() works mostly like Array.prototype.map().

However, Array.from() treats holes as undefined:

> Array.from([,'a'], x => x)
[ undefined, 'a' ]
> Array.from([,'a'], (x,i) => i)
[ 0, 1 ]

Array.prototype.map() skips them, but preserves them:

> [,'a'].map(x => x)
[ , 'a' ]
> [,'a'].map((x,i) => i)
[ , 1 ]
18.4.2.3 Array.prototype methods

In ECMAScript 5, behavior already varied slightly. For example:

ECMAScript 6 adds new kinds of behaviors:

The following table describes how Array.prototype methods handle holes.

Method Holes are  
concat Preserved ['a',,'b'].concat(['c',,'d']) → ['a',,'b','c',,'d']
copyWithinES6 Preserved [,'a','b',,].copyWithin(2,0) → [,'a',,'a']
entriesES6 Elements [...[,'a'].entries()] → [[0,undefined], [1,'a']]
every Ignored [,'a'].every(x => x==='a') → true
fillES6 Filled new Array(3).fill('a') → ['a','a','a']
filter Removed ['a',,'b'].filter(x => true) → ['a','b']
findES6 Elements [,'a'].find(x => true) → undefined
findIndexES6 Elements [,'a'].findIndex(x => true) → 0
forEach Ignored [,'a'].forEach((x,i) => log(i)); → 1
indexOf Ignored [,'a'].indexOf(undefined) → -1
join Elements [,'a',undefined,null].join('#') → '#a##'
keysES6 Elements [...[,'a'].keys()] → [0,1]
lastIndexOf Ignored [,'a'].lastIndexOf(undefined) → -1
map Preserved [,'a'].map(x => 1) → [,1]
pop Elements ['a',,].pop() → undefined
push Preserved new Array(1).push('a') → 2
reduce Ignored ['#',,undefined].reduce((x,y)=>x+y) → '#undefined'
reduceRight Ignored ['#',,undefined].reduceRight((x,y)=>x+y) → 'undefined#'
reverse Preserved ['a',,'b'].reverse() → ['b',,'a']
shift Elements [,'a'].shift() → undefined
slice Preserved [,'a'].slice(0,1) → [,]
some Ignored [,'a'].some(x => x !== 'a') → false
sort Preserved [,undefined,'a'].sort() → ['a',undefined,,]
splice Preserved ['a',,].splice(1,1) → [,]
toString Elements [,'a',undefined,null].toString() → ',a,,'
unshift Preserved [,'a'].unshift('b') → 3
valuesES6 Elements [...[,'a'].values()] → [undefined,'a']

Notes:

18.4.3 Creating Arrays filled with values

Holes being treated as undefined elements by the new ES6 operations helps with creating Arrays that are filled with values.

18.4.3.1 Filling with a fixed value

Array.prototype.fill() replaces all Array elements (incl. holes) with a fixed value:

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

new Array(3) creates an Array with three holes and fill() replaces each hole with the value 7.

18.4.3.2 Filling with ascending numbers

Array.prototype.keys() reports keys even if an Array only has holes. It returns an iterable, which you can convert to an Array via the spread operator:

> [...new Array(3).keys()]
[ 0, 1, 2 ]
18.4.3.3 Filling with computed values

The mapping function in the second parameter of Array.from() is notified of holes. Therefore, you can use Array.from() for more sophisticated filling:

> Array.from(new Array(5), (x,i) => i*2)
[ 0, 2, 4, 6, 8 ]
18.4.3.4 Filling with undefined

If you need an Array that is filled with undefined, you can use the fact that iteration (as triggered by the spread operator) converts holes to undefineds:

> [...new Array(3)]
[ undefined, undefined, undefined ]

18.4.4 Removing holes from Arrays

The ES5 method filter() lets you remove holes:

> ['a',,'c'].filter(() => true)
[ 'a', 'c' ]

ES6 iteration (triggered via the spread operator) lets you convert holes to undefined elements:

> [...['a',,'c']]
[ 'a', undefined, 'c' ]

18.5 Configuring which objects are spread by concat() (Symbol.isConcatSpreadable)

You can configure how Array.prototype.concat() treats objects by adding an (own or inherited) property whose key is the well-known symbol Symbol.isConcatSpreadable and whose value is a boolean.

18.5.1 Default for Arrays: spreading

By default, Array.prototype.concat() spreads Arrays into its result: their indexed elements become elements of the result:

const arr1 = ['c', 'd'];
['a', 'b'].concat(arr1, 'e');
    // ['a', 'b', 'c', 'd', 'e']

With Symbol.isConcatSpreadable, you can override the default and avoid spreading for Arrays:

const arr2 = ['c', 'd'];
arr2[Symbol.isConcatSpreadable] = false;
['a', 'b'].concat(arr2, 'e');
    // ['a', 'b', ['c','d'], 'e']

18.5.2 Default for non-Arrays: no spreading

For non-Arrays, the default is not to spread:

const arrayLike = {length: 2, 0: 'c', 1: 'd'};

console.log(['a', 'b'].concat(arrayLike, 'e'));
    // ['a', 'b', arrayLike, 'e']

console.log(Array.prototype.concat.call(
    arrayLike, ['e','f'], 'g'));
    // [arrayLike, 'e', 'f', 'g']

You can use Symbol.isConcatSpreadable to force spreading:

arrayLike[Symbol.isConcatSpreadable] = true;

console.log(['a', 'b'].concat(arrayLike, 'e'));
    // ['a', 'b', 'c', 'd', 'e']

console.log(Array.prototype.concat.call(
    arrayLike, ['e','f'], 'g'));
    // ['c', 'd', 'e', 'f', 'g']

18.5.3 Detecting Arrays

How does concat() determine if a parameter is an Array? It uses the same algorithm as Array.isArray(). Whether or not Array.prototype is in the prototype chain makes no difference for that algorithm. That is important, because, in ES5 and earlier, hacks were used to subclass Array and those must continue to work (see the section on __proto__ in this book):

> const arr = [];
> Array.isArray(arr)
true

> Object.setPrototypeOf(arr, null);
> Array.isArray(arr)
true

18.5.4 Symbol.isConcatSpreadable in the standard library

No object in the ES6 standard library has a property with the key Symbol.isConcatSpreadable. This mechanism therefore exists purely for browser APIs and user code.

Consequences:

18.6 The numeric range of Array indices

For Arrays, ES6 still has the same rules as ES5:

Strings and Typed Arrays have a larger range of indices: 0 ≤ i < 253−1. The upper bound of that range is due to 253−1 being the largest integer that JavaScript’s floating point numbers can represent safely. For details, see Sect. “Safe integers”.

The only reason for the smaller index range of normal Arrays is backward compatibility.

Next: 19. Maps and Sets