Array
)
JavaScript Arrays are a very flexible data structure and used as lists, stacks, queues, tuples (e.g. pairs), and more.
Some Array-related operations destructively change Arrays. Others non-destructively produce new Arrays with the changes applied to a copy of the original content.
Creating an Array, reading and writing elements:
// Creating an Array
const arr = ['a', 'b', 'c']; // Array literal
assert.deepEqual(
arr,
[ // Array literal
'a',
'b',
'c', // trailing commas are ignored
]
);
// Reading elements
assert.equal(
arr[0], 'a' // negative indices don’t work
);
assert.equal(
arr.at(-1), 'c' // negative indices work
);
// Writing an element
arr[0] = 'x';
assert.deepEqual(
arr, ['x', 'b', 'c']
);
The length of an Array:
const arr = ['a', 'b', 'c'];
assert.equal(
arr.length, 3 // number of elements
);
arr.length = 1; // removing elements
assert.deepEqual(
arr, ['a']
);
arr[arr.length] = 'b'; // adding an element
assert.deepEqual(
arr, ['a', 'b']
);
Adding elements destructively via .push()
:
const arr = ['a', 'b'];
arr.push('c'); // adding an element
assert.deepEqual(
arr, ['a', 'b', 'c']
);
// Pushing Arrays (used as arguments via spreading (...)):
arr.push(...['d', 'e']);
assert.deepEqual(
arr, ['a', 'b', 'c', 'd', 'e']
);
Adding elements non-destructively via spreading (...
):
const arr1 = ['a', 'b'];
const arr2 = ['c'];
assert.deepEqual(
[...arr1, ...arr2, 'd', 'e'],
['a', 'b', 'c', 'd', 'e']
);
Looping over elements:
const arr = ['a', 'b', 'c'];
for (const value of arr) {
console.log(value);
}
Output:
a
b
c
Looping over index-value pairs:
const arr = ['a', 'b', 'c'];
for (const [index, value] of arr.entries()) {
console.log(index, value);
}
Output:
0 a
1 b
2 c
This section demonstrates a few common Array methods. There is a more comprehensive quick reference at the end of this chapter.
Destructively adding or removing an Array element at the start or the end:
// Adding and removing at the start
const arr1 = ['■', '●'];
arr1.unshift('▲');
assert.deepEqual(
arr1, ['▲', '■', '●']
);
arr1.shift();
assert.deepEqual(
arr1, ['■', '●']
);
// Adding and removing at the end
const arr2 = ['■', '●'];
arr2.push('▲');
assert.deepEqual(
arr2, ['■', '●', '▲']
);
arr2.pop();
assert.deepEqual(
arr2, ['■', '●']
);
Finding Array elements:
> ['■', '●', '■'].includes('■')
true
> ['■', '●', '■'].indexOf('■')
0
> ['■', '●', '■'].lastIndexOf('■')
2
> ['●', '', '▲'].find(x => x.length > 0)
'●'
> ['●', '', '▲'].findLast(x => x.length > 0)
'▲'
> ['●', '', '▲'].findIndex(x => x.length > 0)
0
> ['●', '', '▲'].findLastIndex(x => x.length > 0)
2
Transforming Arrays (creating new ones without changing the originals):
> ['▲', '●'].map(x => x+x)
['▲▲', '●●']
> ['■', '●', '■'].filter(x => x === '■')
['■', '■']
> ['▲', '●'].flatMap(x => [x,x])
['▲', '▲', '●', '●']
Copying parts of an Array:
> ['■', '●', '▲'].slice(1, 3)
['●', '▲']
> ['■', '●', '▲'].slice() // complete copy
['■', '●', '▲']
Concatenating the strings in an Array:
> ['■','●','▲'].join('-')
'■-●-▲'
> ['■','●','▲'].join('')
'■●▲'
.sort()
sorts its receiver and returns it (if we don’t want to change the receiver, we can use .toSorted()
):
// By default, string representations of the Array elements
// are sorted lexicographically:
const arr = [200, 3, 10];
arr.sort();
assert.deepEqual(
arr, [10, 200, 3]
);
// Sorting can be customized via a callback:
assert.deepEqual(
[200, 3, 10].sort((a, z) => a - z), // sort numerically
[3, 10, 200]
);
There are two ways of using Arrays in JavaScript:
Normally, an Array is used in either of these ways. But we can also mix the two approaches.
As an example of the difference between the two ways, consider the Array returned by Object.entries()
:
> Object.entries({a:1, b:2, c:3})
[
[ 'a', 1 ],
[ 'b', 2 ],
[ 'c', 3 ],
]
It is a sequence of pairs – fixed-layout Arrays with a length of two.
Note that the difference between fixed layout and sequence may not
Sequence Arrays are very flexible that we can use them as (traditional) arrays, stacks, and queues. We’ll see how later.
The best way to create an Array is via an Array literal:
const arr = ['a', 'b', 'c'];
The Array literal starts and ends with square brackets []
. It creates an Array with three elements: 'a'
, 'b'
, and 'c'
.
Trailing commas are allowed and ignored in Array literals:
const arr = [
'a',
'b',
'c',
];
To read an Array element, we put an index in square brackets (indices start at zero):
const arr = ['a', 'b', 'c'];
assert.equal(arr[0], 'a');
To change an Array element, we assign to an Array with an index:
const arr = ['a', 'b', 'c'];
arr[0] = 'x';
assert.deepEqual(arr, ['x', 'b', 'c']);
The range of Array indices is 32 bits (excluding the maximum length): [0, 232−1)
.length
of an ArrayEvery Array has a property .length
that can be used to both read and change(!) the number of elements in an Array.
The length of an Array is always the highest index plus one:
> const arr = ['a', 'b'];
> arr.length
2
If we write to the Array at the index of the length, we append an element:
> arr[arr.length] = 'c';
> arr
[ 'a', 'b', 'c' ]
> arr.length
3
Another way of (destructively) appending an element is via the Array method .push()
:
> arr.push('d');
> arr
[ 'a', 'b', 'c', 'd' ]
If we set .length
, we are pruning the Array by removing elements:
> arr.length = 1;
> arr
[ 'a' ]
Exercise: Removing empty lines via .push()
exercises/arrays/remove_empty_lines_push_test.mjs
Most Array methods support negative indices. If an index is negative, it is added to the length of an Array to produce a usable index. Therefore, the following two invocations of .slice()
are equivalent: They both copy arr
starting at the last element.
> const arr = ['a', 'b', 'c'];
> arr.slice(-1)
[ 'c' ]
> arr.slice(arr.length - 1)
[ 'c' ]
.at()
: reading single elements (supports negative indices) [ES2022]The Array method .at()
returns the element at a given index. It supports positive and negative indices (-1
refers to the last element, -2
refers to the second-last element, etc.):
> ['a', 'b', 'c'].at(0)
'a'
> ['a', 'b', 'c'].at(-1)
'c'
In contrast, the bracket operator []
does not support negative indices (and can’t be changed because that would break existing code). It interprets them as keys of non-element properties:
const arr = ['a', 'b', 'c'];
arr[-1] = 'non-element property';
// The Array elements didn’t change:
assert.deepEqual(
Array.from(arr), // copy just the Array elements
['a', 'b', 'c']
);
assert.equal(
arr[-1], 'non-element property'
);
To clear (empty) an Array, we can either set its .length
to zero:
const arr = ['a', 'b', 'c'];
arr.length = 0;
assert.deepEqual(arr, []);
or we can assign a new empty Array to the variable storing the Array:
let arr = ['a', 'b', 'c'];
arr = [];
assert.deepEqual(arr, []);
The latter approach has the advantage of not affecting other locations that point to the same Array. If, however, we do want to reset a shared Array for everyone, then we need the former approach.
Inside an Array literal, a spread element consists of three dots (...
) followed by an expression. It results in the expression being evaluated and then iterated over. Each iterated value becomes an additional Array element – for example:
> const iterable = ['b', 'c'];
> ['a', ...iterable, 'd']
[ 'a', 'b', 'c', 'd' ]
That means that we can use spreading to create a copy of an Array and to convert an iterable to an Array:
const original = ['a', 'b', 'c'];
const copy = [...original];
const iterable = original.keys();
assert.deepEqual(
[...iterable], [0, 1, 2]
);
However, for both previous use cases, I find Array.from()
more self-descriptive and prefer it:
const copy2 = Array.from(original);
assert.deepEqual(
Array.from(original.keys()), [0, 1, 2]
);
Spreading is also convenient for concatenating Arrays (and other iterables) into Arrays:
const arr1 = ['a', 'b'];
const arr2 = ['c', 'd'];
const concatenated = [...arr1, ...arr2, 'e'];
assert.deepEqual(
concatenated,
['a', 'b', 'c', 'd', 'e']);
Due to spreading using iteration, it only works if the value is iterable:
> [...'abc'] // strings are iterable
[ 'a', 'b', 'c' ]
> [...123]
TypeError: 123 is not iterable
> [...undefined]
TypeError: undefined is not iterable
Spreading and Array.from()
produce shallow copies
Copying Arrays via spreading or via Array.from()
is shallow: We get new entries in a new Array, but the values are shared with the original Array. The consequences of shallow copying are demonstrated in “Spreading into object literals (...
)” (§30.4).
Method .keys()
lists the indices of an Array:
const arr = ['a', 'b'];
assert.deepEqual(
Array.from(arr.keys()), // (A)
[0, 1]);
.keys()
returns an iterable. In line A, we convert that iterable to an Array.
Listing Array indices is different from listing properties. The former produces numbers; the latter produces stringified numbers (in addition to non-index property keys):
const arr = ['a', 'b'];
arr.prop = true;
assert.deepEqual(
Object.keys(arr),
['0', '1', 'prop']);
Method .entries()
lists the contents of an Array as [index, element] pairs:
const arr = ['a', 'b'];
assert.deepEqual(
Array.from(arr.entries()),
[[0, 'a'], [1, 'b']]);
Array.isArray()
Array.isArray()
checks if a value is an Array:
> Array.isArray([])
true
We can also use instanceof
:
> [] instanceof Array
true
However, instanceof
has one downside: It doesn’t work if a value comes from another realm. Roughly, a realm is an instance of JavaScript’s global scope. Some realms are isolated from each other (e.g., Web Workers in browsers), but there are also realms between which we can move data – for example, same-origin iframes in browsers. x instanceof Array
checks the prototype chain of x
and therefore returns false
if x
is an Array from another realm.
typeof
considers Arrays to be objects:
> typeof []
'object'
for-of
and ArraysWe have already encountered the for-of
loop earlier in this book. This section briefly recaps how to use it for Arrays.
for-of
: iterating over elementsThe following for-of
loop iterates over the elements of an Array:
for (const element of ['a', 'b']) {
console.log(element);
}
Output:
a
b
for-of
: iterating over indicesThis for-of
loop iterates over the indices of an Array:
for (const element of ['a', 'b'].keys()) {
console.log(element);
}
Output:
0
1
for-of
: iterating over [index, element] pairsThe following for-of
loop iterates over [index, element] pairs. Destructuring (described later), gives us convenient syntax for setting up index
and element
in the head of for-of
.
for (const [index, element] of ['a', 'b'].entries()) {
console.log(index, element);
}
Output:
0 a
1 b
Some operations that work with Arrays require only the bare minimum: values must only be Array-like. An Array-like value is an object with the following properties:
.length
: holds the length of the Array-like object. If this property is missing, the value 0
is used.
[0]
: holds the element at index 0 (etc.). Note that if we use numbers as property names, they are always coerced to strings. Therefore, [0]
retrieves the value of the property whose key is '0'
.
For example, Array.from()
accepts Array-like objects and converts them to Arrays:
// .length is implicitly 0 in this case
assert.deepEqual(
Array.from({}),
[]
);
assert.deepEqual(
Array.from({length: 2, 0: 'a', 1: 'b'}),
[ 'a', 'b' ]
);
The TypeScript interface for Array-like objects is:
interface ArrayLike<T> {
length: number;
[n: number]: T;
}
Array-like objects are relatively rare in modern JavaScript
Array-like objects used to be more common before ES6; now we don’t see them very often.
There are two common ways of converting iterables and Array-like values to Arrays:
Array.from()
I prefer the latter – I find it more self-explanatory.
...
)Inside an Array literal, spreading via ...
converts any iterable object into a series of Array elements. For example:
// Get an Array-like collection from a web browser’s DOM
const domCollection = document.querySelectorAll('a');
// Alas, the collection is missing many Array methods
assert.equal('slice' in domCollection, false);
// Solution: convert it to an Array
const arr = [...domCollection];
assert.deepEqual(
arr.map(x => x.href),
['https://2ality.com', 'https://exploringjs.com']);
The conversion works because the DOM collection is iterable.
Array.from()
Array.from()
can be used in two modes.
Array.from()
: convertingThe first mode has the following type signature:
.from<T>(iterable: Iterable<T> | ArrayLike<T>): Array<T>
Interface Iterable
is shown in the chapter on synchronous iteration. Interface ArrayLike
appeared earlier in this chapter.
With a single parameter, Array.from()
converts anything iterable or Array-like to an Array:
> Array.from(new Set(['a', 'b'])) // iterable
[ 'a', 'b' ]
> Array.from({length: 2, 0:'a', 1:'b'}) // Array-like
[ 'a', 'b' ]
Array.from()
: converting and mappingThe second mode of Array.from()
involves two parameters:
.from<T, U>(
iterable: Iterable<T> | ArrayLike<T>,
mapFunc: (v: T, i: number) => U,
thisArg?: any)
: Array<U>
In this mode, Array.from()
does several things:
iterable
.
mapFunc
with each iterated value. The optional parameter thisArg
specifies a this
for mapFunc
.
mapFunc
to each iterated value.
In other words: we are going from an iterable with elements of type T
to an Array with elements of type U
.
This is an example:
> Array.from(new Set(['a', 'b']), x => x + x)
[ 'aa', 'bb' ]
The best way of creating an Array is via an Array literal. However, we can’t always use one: The Array may be too large, we may not know its length during development, or we may want to keep its length flexible. Then I recommend the following techniques for creating, and possibly filling, Arrays.
The most common technique for creating an Array and adding elements later, is to start with an empty Array and push values into it:
const arr = [];
for (let i=0; i<3; i++) {
arr.push('*'.repeat(i));
}
assert.deepEqual(
arr, ['', '*', '**']
);
The following code creates an Array that is filled with a primitive value:
> new Array(3).fill(0)
[ 0, 0, 0 ]
.fill()
replaces each Array element or hole with a given value. We use it to fill an Array that has 3 holes:
> new Array(3)
[ , , ,]
Note that the result has three holes (empty slots) – the last comma in an Array literal is always ignored.
If we use .fill()
with an object, then each Array element will refer to this same single object:
const arr = new Array(3).fill({});
arr[0].prop = true;
assert.deepEqual(
arr, [
{prop: true},
{prop: true},
{prop: true},
]);
How can we fix this? We can use Array.from()
:
> Array.from(new Array(3), () => ({}))
[{}, {}, {}]
Calling Array.from()
with two arguments:
In contrast to .fill()
, which reuses the same object multiple times, the previous code creates a new object for each element.
Could we have used .map()
in this case? Unfortunately not because .map()
ignores but preserves holes (whereas Array.from()
treats them as undefined
elements):
> new Array(3).map(() => ({}))
[ , , ,]
For large sizes, the temporary Array in the first argument can consume quite a bit of memory. The following approach doesn’t have this downside but is less self-descriptive:
> Array.from({length: 3}, () => ({}))
[{}, {}, {}]
Instead of a temporary Array, we are using a temporary Array-like object.
To create an Array with a range of integers, we use Array.from()
similarly to how we did in the previous subsection:
function createRange(start, end) {
return Array.from({length: end-start}, (_, i) => i+start);
}
assert.deepEqual(
createRange(2, 5),
[2, 3, 4]);
Here is an alternative, slightly hacky technique for creating integer ranges that start at zero:
/** Returns an iterable */
function createRange(end) {
return new Array(end).keys();
}
assert.deepEqual(
Array.from(createRange(4)),
[0, 1, 2, 3]);
This works because .keys()
treats holes like undefined
elements and lists their indices.
When dealing with Arrays of integers or floats, we should consider Typed Arrays, which were created for this purpose.
JavaScript does not have real multidimensional Arrays; we need to resort to Arrays whose elements are Arrays:
function initMultiArray(...dimensions) {
function initMultiArrayRec(dimIndex) {
if (dimIndex >= dimensions.length) {
return 0;
} else {
const dim = dimensions[dimIndex];
const arr = [];
for (let i=0; i<dim; i++) {
arr.push(initMultiArrayRec(dimIndex+1));
}
return arr;
}
}
return initMultiArrayRec(0);
}
const arr = initMultiArray(4, 3, 2);
arr[3][2][1] = 'X'; // last in each dimension
assert.deepEqual(arr, [
[ [ 0, 0 ], [ 0, 0 ], [ 0, 0 ] ],
[ [ 0, 0 ], [ 0, 0 ], [ 0, 0 ] ],
[ [ 0, 0 ], [ 0, 0 ], [ 0, 0 ] ],
[ [ 0, 0 ], [ 0, 0 ], [ 0, 'X' ] ],
]);
In this section, we examine how exactly Arrays store their elements: in properties. We usually don’t need to know that but it helps with understanding a few rarer Array phenomena.
You’d think that Array elements are special because we are accessing them via numbers. But the square brackets operator []
for doing so is the same operator that is used for accessing properties. It coerces any non-symbol value to a string. Therefore, Array elements are (almost) normal properties (line A) and it doesn’t matter if we use numbers or strings as indices (lines B and C):
const arr = ['a', 'b'];
arr.prop = 123;
assert.deepEqual(
Object.keys(arr),
['0', '1', 'prop']); // (A)
assert.equal(arr[0], 'a'); // (B)
assert.equal(arr['0'], 'a'); // (C)
To make matters even more confusing:
Property keys (strings!) that are used for Array elements are called indices. A string str
is an index if converting it to a 32-bit unsigned integer and back results in the original value. Written as a formula:
ToString(ToUint32(str)) === str
When listing property keys, indices are treated specially – they always come first and are sorted like numbers ('2'
comes before '10'
):
const arr = [];
arr.prop = true;
arr[1] = 'b';
arr[0] = 'a';
assert.deepEqual(
Object.keys(arr),
['0', '1', 'prop']);
Note that .length
, .entries()
and .keys()
treat Array indices as numbers and ignore non-index properties:
assert.equal(arr.length, 2);
assert.deepEqual(
Array.from(arr.keys()), [0, 1]);
assert.deepEqual(
Array.from(arr.entries()), [[0, 'a'], [1, 'b']]);
We used Array.from()
to convert the iterables returned by .keys()
and .entries()
to Arrays.
We distinguish two kinds of Arrays in JavaScript:
arr
is dense if all indices i
, with 0 ≤ i
< arr.length
, exist. That is, the indices form a contiguous range.
Arrays can be sparse in JavaScript because Arrays are actually dictionaries from indices to values.
Recommendation: avoid holes
So far, we have only seen dense Arrays and it’s indeed recommended to avoid holes: They make our code more complicated and are not handled consistently by Array methods. Additionally, JavaScript engines optimize dense Arrays, making them faster.
We can create holes by skipping indices when assigning elements:
const arr = [];
arr[0] = 'a';
arr[2] = 'c';
assert.deepEqual(Object.keys(arr), ['0', '2']); // (A)
assert.equal(0 in arr, true); // element
assert.equal(1 in arr, false); // hole
In line A, we are using Object.keys()
because arr.keys()
treats holes as if they were undefined
elements and does not reveal them.
Another way of creating holes is to skip elements in Array literals:
const arr = ['a', , 'c'];
assert.deepEqual(Object.keys(arr), ['0', '2']);
We can also delete Array elements:
const arr = ['a', 'b', 'c'];
assert.deepEqual(Object.keys(arr), ['0', '1', '2']);
delete arr[1];
assert.deepEqual(Object.keys(arr), ['0', '2']);
Alas, there are many different ways in which Array operations treat holes.
Some Array operations remove holes:
> ['a',,'b'].filter(x => true)
[ 'a', 'b' ]
Some Array operations ignore holes:
> ['a', ,'a'].every(x => x === 'a')
true
Some Array operations ignore but preserve holes:
> ['a',,'b'].map(x => 'c')
[ 'c', , 'c' ]
Some Array operations treat holes as undefined
elements:
> Array.from(['a',,'b'], x => x)
[ 'a', undefined, 'b' ]
> Array.from(['a',,'b'].entries())
[[0, 'a'], [1, undefined], [2, 'b']]
Object.keys()
works differently than .keys()
(strings vs. numbers, holes don’t have keys):
> Array.from(['a',,'b'].keys())
[ 0, 1, 2 ]
> Object.keys(['a',,'b'])
[ '0', '2' ]
There is no rule to remember here. If it ever matters how an Array operation treats holes, the best approach is to do a quick test in a console.
Some Array operations are destructive: They change the Array they operate on – e.g., setting an element:
> const arr = ['a', 'b', 'c'];
> arr[1] = 'x';
> arr // the original was modified
[ 'a', 'x', 'c' ]
Other Array operations are non-destructive: They produce new Arrays that contain the desired changes and don’t touch the originals – e.g., method .with()
is the non-destructive version of setting elements:
> const arr = ['a', 'b', 'c'];
> arr.with(1, 'x') // produces copy with changes
[ 'a', 'x', 'c' ]
> arr // the original is unchanged
[ 'a', 'b', 'c' ]
These are three common destructive Array methods:
.reverse()
.sort()
.splice()
We’ll get to .sort()
and .splice()
later in this chapter. .reverse()
rearranges an Array so that the order of its elements is reversed: The element that was previously last now comes first; the second-last element comes second; etc.:
const original = ['a', 'b', 'c'];
const reversed = original.reverse();
assert.deepEqual(reversed, ['c', 'b', 'a']);
assert.ok(reversed === original); // .reverse() returned `this`
assert.deepEqual(original, ['c', 'b', 'a']);
To prevent a destructive method from changing an Array, we can make a copy before using it – e.g.:
const reversed1 = original.slice().reverse();
const reversed2 = [...original].reverse();
const reversed3 = Array.from(original).reverse();
Another option is to use the non-destructive version of a destructive method. That’s what we’ll explore next.
.reverse()
, .sort()
, .splice()
[ES2023]
These are the non-destructive versions of the destructive Array methods .reverse()
, .sort()
and .splice()
:
.toReversed(): Array
.toSorted(compareFn): Array
.toSpliced(start, deleteCount, ...items): Array
We have used .reverse()
in the previous subsection. Its non-destructive version is used like this:
const original = ['a', 'b', 'c'];
const reversed = original.toReversed();
assert.deepEqual(reversed, ['c', 'b', 'a']);
// The original is unchanged
assert.deepEqual(original, ['a', 'b', 'c']);
JavaScript’s Array
is quite flexible and more like a combination of array, stack, and queue. Let’s explore ways of destructively adding and removing Array elements.
.push()
adds elements at the end of an Array:
const arr1 = ['a', 'b'];
arr1.push('x', 'y'); // append single elements
assert.deepEqual(arr1, ['a', 'b', 'x', 'y']);
const arr2 = ['a', 'b'];
arr2.push(...['x', 'y']); // (A) append Array
assert.deepEqual(arr2, ['a', 'b', 'x', 'y']);
Spread arguments (...
) are a feature of function calls. In line A, we used it to push an Array.
.pop()
is the inverse of .push()
and removes elements at the end of an Array:
const arr2 = ['a', 'b', 'c'];
assert.equal(arr2.pop(), 'c');
assert.deepEqual(arr2, ['a', 'b']);
.unshift()
adds element at the beginning of an Array:
const arr1 = ['a', 'b'];
arr1.unshift('x', 'y'); // prepend single elements
assert.deepEqual(arr1, ['x', 'y', 'a', 'b']);
const arr2 = ['a', 'b'];
arr2.unshift(...['x', 'y']); // prepend Array
assert.deepEqual(arr2, ['x', 'y', 'a', 'b']);
.shift()
is the inverse of .unshift()
and removes elements at the beginning of an Array:
const arr1 = ['a', 'b', 'c'];
assert.equal(arr1.shift(), 'a');
assert.deepEqual(arr1, ['b', 'c']);
Tip: remembering the functionality of push
, pop
, shift
and unshift
My recommendation is to focus on remembering the following two methods:
.push()
is the most frequently used of the four methods. One common use case is to assemble an output Array: We first push the element at index 0; then the element at index 1; etc.
.shift()
can be used to consume the elements of an Array: The first time we shift, we get the element at index 0; then the element at index 1; etc.
The remaining two methods, pop
and unshift
, are inverses of these two methods.
Exercise: Implementing a queue via an Array
exercises/arrays/queue_via_array_test.mjs
Spread elements (...
) are a feature of Array literals. In this section, we’ll use it to non-destructively prepend and append elements to Arrays.
Non-destructive prepending:
const arr1 = ['a', 'b'];
assert.deepEqual(
['x', 'y', ...arr1], // prepend single elements
['x', 'y', 'a', 'b']);
assert.deepEqual(arr1, ['a', 'b']); // unchanged!
const arr2 = ['a', 'b'];
assert.deepEqual(
[...['x', 'y'], ...arr2], // prepend Array
['x', 'y', 'a', 'b']);
assert.deepEqual(arr2, ['a', 'b']); // unchanged!
Non-destructive appending:
const arr1 = ['a', 'b'];
assert.deepEqual(
[...arr1, 'x', 'y'], // append single elements
['a', 'b', 'x', 'y']);
assert.deepEqual(arr1, ['a', 'b']); // unchanged!
const arr2 = ['a', 'b'];
assert.deepEqual(
[...arr2, ...['x', 'y']], // append Array
['a', 'b', 'x', 'y']);
assert.deepEqual(arr2, ['a', 'b']); // unchanged!
The following Array methods accept callbacks to which they feed Array elements:
.find
.findLast
.findIndex
.findLastIndex
.map
.flatMap
.filter
.every
.some
.reduce
.reduceRight
.forEach
Element callbacks have type signatures that look as follows:
callback: (value: T, index: number, array: Array<T>) => boolean
That is, the callback gets three parameters (it is free to ignore any of them):
value
is the most important one. This parameter holds the Array element that is currently being processed.
index
can additionally tell the callback what the index of the element is.
array
points to the current Array (the receiver of the method call). Some algorithms need to refer to the whole Array – e.g., to search elsewhere additional data. This parameter lets us write reusable callbacks for such algorithms.
What the callback is expected to return depends on the method it is passed to. Possibilities include:
.map()
fills its result with the values returned by its callback:
> ['a', 'b', 'c'].map(x => x + x)
[ 'aa', 'bb', 'cc' ]
.find()
returns the first Array element for which its callback returns true
:
> ['a', 'bb', 'ccc'].find(str => str.length >= 2)
'bb'
.map()
, .filter()
, .flatMap()
In this section, we explore methods that accept element callbacks which tell them how to transform an input Array into an output Array.
.map()
: Each output element is derived from its input elementEach element of the output Array is the result of applying the callback to the corresponding input element:
> [1, 2, 3].map(x => x * 3)
[ 3, 6, 9 ]
> ['how', 'are', 'you'].map(str => str.toUpperCase())
[ 'HOW', 'ARE', 'YOU' ]
> [true, true, true].map((_x, index) => index)
[ 0, 1, 2 ]
.map()
can be implemented as follows:
function map(arr, mapFunc) {
const result = [];
for (const [i, x] of arr.entries()) {
result.push(mapFunc(x, i, arr));
}
return result;
}
Exercise: Numbering lines via .map()
exercises/arrays/number_lines_test.mjs
.filter()
: Only keep some of the elementsThe Array method .filter()
returns an Array collecting all elements for which the callback returns a truthy value.
For example:
> [-1, 2, 5, -7, 6].filter(x => x >= 0)
[ 2, 5, 6 ]
> ['a', 'b', 'c', 'd'].filter((_x,i) => (i%2)===0)
[ 'a', 'c' ]
.filter()
can be implemented as follows:
function filter(arr, filterFunc) {
const result = [];
for (const [i, x] of arr.entries()) {
if (filterFunc(x, i, arr)) {
result.push(x);
}
}
return result;
}
Exercise: Removing empty lines via .filter()
exercises/arrays/remove_empty_lines_filter_test.mjs
.flatMap()
: Replace each input element with zero or more output elements [ES2019]The type signature of Array<T>.prototype.flatMap()
is:
.flatMap<U>(
callback: (value: T, index: number, array: Array<T>) => U|Array<U>,
thisValue?: any
): Array<U>
Both .map()
and .flatMap()
take a function callback
as a parameter that controls how an input Array is translated to an output Array:
.map()
, each input Array element is translated to exactly one output element. That is, callback
returns a single value.
.flatMap()
, each input Array element is translated to zero or more output elements. That is, callback
returns an Array of values (it can also return non-Array values, but that is rare).
This is .flatMap()
in action:
> ['a', 'b', 'c'].flatMap(x => [x,x])
[ 'a', 'a', 'b', 'b', 'c', 'c' ]
> ['a', 'b', 'c'].flatMap(x => [x])
[ 'a', 'b', 'c' ]
> ['a', 'b', 'c'].flatMap(x => [])
[]
We’ll consider use cases next, before exploring how this method could be implemented.
The result of the Array method .map()
always has the same length as the Array it is invoked on. That is, its callback can’t skip Array elements it isn’t interested in. The ability of .flatMap()
to do so is useful in the next example.
We will use the following function processArray()
to create an Array that we’ll then filter and map via .flatMap()
:
function processArray(arr, callback) {
return arr.map(x => {
try {
return { value: callback(x) };
} catch (e) {
return { error: e };
}
});
}
Next, we create an Array results
via processArray()
:
const results = processArray([1, -5, 6], throwIfNegative);
assert.deepEqual(results, [
{ value: 1 },
{ error: new Error('Illegal value: -5') },
{ value: 6 },
]);
function throwIfNegative(value) {
if (value < 0) {
throw new Error('Illegal value: '+value);
}
return value;
}
We can now use .flatMap()
to extract just the values or just the errors from results
:
const values = results.flatMap(
result => result.value ? [result.value] : []);
assert.deepEqual(values, [1, 6]);
const errors = results.flatMap(
result => result.error ? [result.error] : []);
assert.deepEqual(errors, [new Error('Illegal value: -5')]);
The Array method .map()
maps each input Array element to one output element. But what if we want to map it to multiple output elements?
That becomes necessary in the following example:
> stringsToCodePoints(['many', 'a', 'moon'])
['m', 'a', 'n', 'y', 'a', 'm', 'o', 'o', 'n']
We want to convert an Array of strings to an Array of Unicode characters (code points). The following function achieves that via .flatMap()
:
function stringsToCodePoints(strs) {
return strs.flatMap(str => Array.from(str));
}
We can implement .flatMap()
as follows. Note: This implementation is simpler than the built-in version, which, for example, performs more checks.
function flatMap(arr, mapFunc) {
const result = [];
for (const [index, elem] of arr.entries()) {
const x = mapFunc(elem, index, arr);
// We allow mapFunc() to return non-Arrays
if (Array.isArray(x)) {
result.push(...x);
} else {
result.push(x);
}
}
return result;
}
Exercises: .flatMap()
exercises/arrays/convert_to_numbers_test.mjs
exercises/arrays/replace_objects_test.mjs
.reduce()
: computing a summary for an ArrayMethod .reduce()
is a powerful tool for computing a “summary” of an Array arr
. A summary can be any kind of value:
arr
.
arr
, where each element is twice the original element.
reduce
is also known as foldl
(“fold left”) in functional programming and popular there. One caveat is that it can make code difficult to understand.
.reduce()
has the following type signature (inside an Array<T>
):
.reduce<U>(
callback: (accumulator: U, element: T, index: number, array: Array<T>) => U,
init?: U)
: U
T
is the type of the Array elements, U
is the type of the summary. The two may or may not be different. accumulator
is just another name for “summary”.
To compute the summary of an Array arr
, .reduce()
feeds all Array elements to its callback one at a time:
const accumulator_0 = callback(init, arr[0]);
const accumulator_1 = callback(accumulator_0, arr[1]);
const accumulator_2 = callback(accumulator_1, arr[2]);
// Etc.
callback
combines the previously computed summary (stored in its parameter accumulator
) with the current Array element and returns the next accumulator
. The result of .reduce()
is the final accumulator – the last result of callback
after it has visited all elements.
In other words: callback
does most of the work; .reduce()
just invokes it in a useful manner.
We could say that the callback folds Array elements into the accumulator. That’s why this operation is called “fold” in functional programming.
Let’s look at an example of .reduce()
in action: function addAll()
computes the sum of all numbers in an Array arr
.
function addAll(arr) {
const startSum = 0;
const callback = (sum, element) => sum + element;
return arr.reduce(callback, startSum);
}
assert.equal(addAll([1, 2, 3]), 6); // (A)
assert.equal(addAll([7, -4, 2]), 5);
In this case, the accumulator holds the sum of all Array elements that callback
has already visited.
How was the result 6
derived from the Array in line A? Via the following invocations of callback
:
callback(0, 1) --> 1
callback(1, 2) --> 3
callback(3, 3) --> 6
Notes:
init
of .reduce()
).
callback
is also the result of .reduce()
.
Alternatively, we could have implemented addAll()
via a for-of
loop:
function addAll(arr) {
let sum = 0;
for (const element of arr) {
sum = sum + element;
}
return sum;
}
It’s hard to say which of the two implementations is “better”: the one based on .reduce()
is a little more concise, while the one based on for-of
may be a little easier to understand – especially if someone is not familiar with functional programming.
.reduce()
The following function is an implementation of the Array method .indexOf()
. It returns the first index at which the given searchValue
appears inside the Array arr
:
const NOT_FOUND = -1;
function indexOf(arr, searchValue) {
return arr.reduce(
(result, elem, index) => {
if (result !== NOT_FOUND) {
// We have already found something: don’t change anything
return result;
} else if (elem === searchValue) {
return index;
} else {
return NOT_FOUND;
}
},
NOT_FOUND);
}
assert.equal(indexOf(['a', 'b', 'c'], 'b'), 1);
assert.equal(indexOf(['a', 'b', 'c'], 'x'), -1);
One limitation of .reduce()
is that we can’t finish early. In a for-of
loop, we can immediately return the result once we have found it.
Function double(arr)
returns a copy of inArr
whose elements are all multiplied by 2:
function double(inArr) {
return inArr.reduce(
(outArr, element) => {
outArr.push(element * 2);
return outArr;
},
[]);
}
assert.deepEqual(
double([1, 2, 3]),
[2, 4, 6]);
We modify the initial value []
by pushing into it. A non-destructive, more functional version of double()
looks as follows:
function double(inArr) {
return inArr.reduce(
// Don’t change `outArr`, return a fresh Array
(outArr, element) => [...outArr, element * 2],
[]);
}
assert.deepEqual(
double([1, 2, 3]),
[2, 4, 6]);
This version is more elegant but also slower and uses more memory.
Exercises: .reduce()
map()
via .reduce()
: exercises/arrays/map_via_reduce_test.mjs
filter()
via .reduce()
: exercises/arrays/filter_via_reduce_test.mjs
countMatches()
via .reduce()
: exercises/arrays/count_matches_via_reduce_test.mjs
.reduceRight()
: the end-to-start version of .reduce()
.reduce()
visits elements from start to end:
> ['a', 'b', 'c'].reduce((acc, x) => acc + x)
'abc'
.reduceRight()
has the same functionality but visits elements from end to start:
> ['a', 'b', 'c'].reduceRight((acc, x) => acc + x)
'cba'
.sort()
: sorting Arrays.sort()
has the following type definition:
sort(compareFunc?: (a: T, b: T) => number): this
By default, .sort()
sorts string representations of the elements. These representations are compared via <
. This operator compares code unit values (char codes) lexicographically (the first characters are most significant).
.sort()
sorts in place; it changes and returns its receiver:
> const arr = ['a', 'c', 'b'];
> arr.sort() === arr
true
> arr
[ 'a', 'b', 'c' ]
.sort()
is stable
Since ECMAScript 2019, sorting is guaranteed to be stable: If elements are considered equal by sorting, then sorting does not change the order of those elements (relative to each other).
We can customize the sort order via the parameter compareFunc
, which must return a number that is:
a
is less than b
a
is equal to b
a
is greater than b
We’ll see an example of a compare function in the next subsection.
Tip for remembering these rules
A negative number is less than zero (etc.).
Lexicographical sorting doesn’t work well for numbers:
> [200, 3, 10].sort()
[ 10, 200, 3 ]
We can fix this by writing a compare function:
function compareNumbers(a, b) {
if (a < b) {
return -1; // any negative number will do
} else if (a === b) {
return 0;
} else {
return 1; // any positive number will do
}
}
assert.deepEqual(
[200, 3, 10].sort(compareNumbers),
[3, 10, 200]
);
Why doesn’t .sort()
automatically pick the right sorting approach for numbers?
It would have to examine all Array elements and make sure that they are numbers before switching from lexicographical sorting to numeric sorting.
The following trick uses the fact that (e.g.) the result for “less than” can be any negative number:
> [200, 3, 10].sort((a, z) => a - z)
[ 3, 10, 200 ]
a
and z
because that enables a mnemonic: The callback sorts ascendingly, “from a
to z
” (a - z
).
When sorting human-language strings, we need to be aware that they are compared according to their code unit values (char codes):
> ['pie', 'cookie', 'éclair', 'Pie', 'Cookie', 'Éclair'].sort()
[ 'Cookie', 'Pie', 'cookie', 'pie', 'Éclair', 'éclair' ]
All unaccented uppercase letters come before all unaccented lowercase letters, which come before all accented letters. We can use Intl
, the JavaScript internationalization API if we want proper sorting for human languages:
const arr = ['pie', 'cookie', 'éclair', 'Pie', 'Cookie', 'Éclair'];
assert.deepEqual(
arr.sort(new Intl.Collator('en').compare),
['cookie', 'Cookie', 'éclair', 'Éclair', 'pie', 'Pie']
);
We also need to use a compare function if we want to sort objects. As an example, the following code shows how to sort objects by age.
const arr = [ {age: 200}, {age: 3}, {age: 10} ];
assert.deepEqual(
arr.sort((obj1, obj2) => obj1.age - obj2.age),
[{ age: 3 }, { age: 10 }, { age: 200 }]
);
Exercise: Sorting objects by name
exercises/arrays/sort_objects_test.mjs
Arrays are iterable and therefore can use operations that accept iterables. These are described elsewhere:
Array
Legend:
R
: method does not change the Array (non-destructive).
W
: method changes the Array (destructive).
Negative indices: If a method supports negative indices that means that such indices are added to .length
before they are used: -1
becomes this.length-1
, etc. In other words: -1
refers to the last element, -2
to the second-last element, etc. .at()
is one method that supports negative indices:
const arr = ['a', 'b', 'c'];
assert.equal(
arr.at(-1), 'c'
);
new Array()
new Array(len = 0)
[ES1]
Creates an Array of length len
that only contains holes:
// Trailing commas are always ignored.
// Therefore: number of commas = number of holes
assert.deepEqual(new Array(3), [,,,]);
Array.*
Array.from(iterableOrArrayLike, mapFunc?)
[ES6]
Array.from<T>(
iterableOrArrayLike: Iterable<T> | ArrayLike<T>
): Array<T>
Array.from<T, U>(
iterableOrArrayLike: Iterable<T> | ArrayLike<T>,
mapFunc: (v: T, k: number) => U, thisArg?: any
): Array<U>
mapFunc
before they are added to the output Array.
Examples:
> Array.from(new Set(['a', 'b'])) // iterable
[ 'a', 'b' ]
> Array.from({length: 2, 0:'a', 1:'b'}) // Array-like object
[ 'a', 'b' ]
Array.of(...items)
[ES6]
Array.of<T>(
...items: Array<T>
): Array<T>
This static method is mainly useful for subclasses of Array
, where it serves as a custom Array literal:
class MyArray extends Array {}
assert.equal(
MyArray.of('a', 'b') instanceof MyArray, true
);
Array.prototype.*
: getting, setting and visiting single elementsArray.prototype.at(index)
[R, ES2022]
index
. If there is no such element, it returns undefined
.
This method is mostly equivalent to getting elements via square brackets:
arr[index] === arr.at(index)
One reason for using .at()
is that it supports negative indices:
> ['a', 'b', 'c'].at(0)
'a'
> ['a', 'b', 'c'].at(-1)
'c'
Array.prototype.with(index, value)
[R, ES2023]
index
, there is now value
.
This method is the non-destructive version of setting elements via square brackets. It supports negative indices:
> ['a', 'b', 'c'].with(2, 'x')
[ 'a', 'b', 'x' ]
> ['a', 'b', 'c'].with(-1, 'x')
[ 'a', 'b', 'x' ]
Array.prototype.forEach(callback)
[R, ES5]
Array<T>.prototype.forEach(
callback: (value: T, index: number, array: Array<T>) => void,
thisArg?: any
): void
Calls callback
for each element.
['a', 'b'].forEach((x, i) => console.log(x, i))
Output:
a 0
b 1
A for-of
loop is usually a better choice: it’s faster, supports break
and can iterate over arbitrary iterables.
Array.prototype.*
: keys and valuesArray.prototype.keys()
[R, ES6]
Returns an iterable over the keys of the receiver.
> Array.from(['a', 'b'].keys())
[ 0, 1 ]
Array.prototype.values()
[R, ES6]
Returns an iterable over the values of the receiver.
> Array.from(['a', 'b'].values())
[ 'a', 'b' ]
Array.prototype.entries()
[R, ES6]
Returns an iterable over [index, element] pairs.
> Array.from(['a', 'b'].entries())
[ [ 0, 'a' ], [ 1, 'b' ] ]
Array.prototype.*
: destructively adding or removing elements at either end of an ArrayArray.prototype.pop()
[W, ES3]
Removes and returns the last element of the receiver. That is, it treats the end of the receiver as a stack. The opposite of .push()
.
> const arr = ['a', 'b', 'c'];
> arr.pop()
'c'
> arr
[ 'a', 'b' ]
Array.prototype.push(...items)
[W, ES3]
Adds zero or more items
to the end of the receiver. That is, it treats the end of the receiver as a stack. The return value is the length of the receiver after the change. The opposite of .pop()
.
> const arr = ['a', 'b'];
> arr.push('c', 'd')
4
> arr
[ 'a', 'b', 'c', 'd' ]
We can push an Array by spreading (...
) it into arguments:
> const arr = ['x'];
> arr.push(...['y', 'z'])
3
> arr
[ 'x', 'y', 'z' ]
Array.prototype.shift()
[W, ES3]
Removes and returns the first element of the receiver. The inverse of .unshift()
.
> const arr = ['a', 'b', 'c'];
> arr.shift()
'a'
> arr
[ 'b', 'c' ]
Array.prototype.unshift(...items)
[W, ES3]
Inserts the items
at the beginning of the receiver and returns its length after this modification.
> const arr = ['c', 'd'];
> arr.unshift('e', 'f')
4
> arr
[ 'e', 'f', 'c', 'd' ]
We can push an Array by spreading (...
) it into arguments:
> const arr = ['c'];
> arr.unshift(...['a', 'b'])
3
> arr
[ 'a', 'b', 'c' ]
Array.prototype.*
: combining, extracting and changing sequences of elements Tip: telling .slice()
and .splice()
apart
.slice()
is much more commonly used. The verb “slice” is also much more common than the verb “splice”.
.splice()
is rare: Elements are more commonly (non-destructively) removed via .filter()
. “Splice” has one letter more than “slice” and the method also does more.
Array.prototype.concat(...items)
[R, ES3]
Returns a new Array that is the concatenation of the receiver and all items
. Non-Array parameters (such as 'b'
in the following example) are treated as if they were Arrays with single elements.
> ['a'].concat('b', ['c', 'd'])
[ 'a', 'b', 'c', 'd' ]
Array.prototype.slice(start?, end?)
[R, ES3]
Returns a new Array containing the elements of the receiver whose indices are between (including) start
and (excluding) end
.
> ['a', 'b', 'c', 'd'].slice(1, 3)
[ 'b', 'c' ]
> ['a', 'b'].slice() // shallow copy
[ 'a', 'b' ]
.slice()
supports negative indices:
> ['a', 'b', 'c'].slice(-2)
[ 'b', 'c' ]
It can be used to (shallowly) copy Arrays:
const copy = original.slice();
Array.prototype.splice(start?, deleteCount?, ...items)
[W, ES3]
start
,
deleteCount
elements (default: all remaining elements) and
items
.
.toSpliced()
.
> const arr = ['a', 'b', 'c', 'd'];
> arr.splice(1, 2, 'x', 'y')
[ 'b', 'c' ]
> arr
[ 'a', 'x', 'y', 'd' ]
If deleteCount
is missing, .splice()
deletes until the end of the Array:
> const arr = ['a', 'b', 'c', 'd'];
> arr.splice(2)
[ 'c', 'd' ]
> arr
[ 'a', 'b' ]
start
can be negative:
> ['a', 'b', 'c'].splice(-2)
[ 'b', 'c' ]
Array.prototype.toSpliced(start?, deleteCount?, ...items)
[R, ES2023]
start
, deleteCount
elements are replaced with items
.
deleteCount
is missing, all elements from start
until the end are deleted.
.splice()
.
> const arr = ['a', 'b', 'c', 'd'];
> arr.toSpliced(1, 2, 'x', 'y')
[ 'a', 'x', 'y', 'd' ]
start
can be negative:
> ['a', 'b', 'c'].toSpliced(-2)
[ 'a' ]
Array.prototype.fill(start=0, end=this.length)
[W, ES6]
this
.
value
to every index between (including) start
and (excluding) end
.
> [0, 1, 2].fill('a')
[ 'a', 'a', 'a' ]
Caveat: Don’t use this method to fill an Array with an object obj
; then each element will refer to the same value (sharing it). In this case, it’s better to use Array.from()
.
Array.prototype.copyWithin(target, start, end=this.length)
[W, ES6]
this
.
Copies the elements whose indices range from (including) start
to (excluding) end
to indices starting with target
. Overlapping is handled correctly.
> ['a', 'b', 'c', 'd'].copyWithin(0, 2, 4)
[ 'c', 'd', 'c', 'd' ]
start
or end
can be negative.
Array.prototype.*
: searching for elementsArray.prototype.includes(searchElement, fromIndex)
[R, ES2016]
Returns true
if the receiver has an element whose value is searchElement
and false
, otherwise. Searching starts at index fromIndex
.
> [0, 1, 2].includes(1)
true
> [0, 1, 2].includes(5)
false
Array.prototype.indexOf(searchElement, fromIndex)
[R, ES5]
Returns the index of the first element that is strictly equal to searchElement
. Returns -1
if there is no such element. Starts searching at index fromIndex
, visiting higher indices next.
> ['a', 'b', 'a'].indexOf('a')
0
> ['a', 'b', 'a'].indexOf('a', 1)
2
> ['a', 'b', 'a'].indexOf('c')
-1
Array.prototype.lastIndexOf(searchElement, fromIndex)
[R, ES5]
Returns the index of the last element that is strictly equal to searchElement
. Returns -1
if there is no such element. Starts searching at index fromIndex
, visiting lower indices next.
> ['a', 'b', 'a'].lastIndexOf('a')
2
> ['a', 'b', 'a'].lastIndexOf('a', 1)
0
> ['a', 'b', 'a'].lastIndexOf('c')
-1
Array.prototype.find(predicate, thisArg?)
[R, ES6]
Array<T>.prototype.find(
predicate: (value: T, index: number, obj: Array<T>) => boolean,
thisArg?: any
): T | undefined
predicate
returns a truthy value.
undefined
.
> [-1, 2, -3].find(x => x < 0)
-1
> [1, 2, 3].find(x => x < 0)
undefined
Array.prototype.findLast(predicate, thisArg?)
[R, ES2023]
Array<T>.prototype.findLast(
predicate: (value: T, index: number, obj: Array<T>) => boolean,
thisArg?: any
): T | undefined
predicate
returns a truthy value.
undefined
.
> [-1, 2, -3].findLast(x => x < 0)
-3
> [1, 2, 3].findLast(x => x < 0)
undefined
Array.prototype.findIndex(predicate, thisArg?)
[R, ES6]
Array<T>.prototype.findIndex(
predicate: (value: T, index: number, obj: Array<T>) => boolean,
thisArg?: any
): number
predicate
returns a truthy value.
-1
.
> [-1, 2, -3].findIndex(x => x < 0)
0
> [1, 2, 3].findIndex(x => x < 0)
-1
Array.prototype.findLastIndex(predicate, thisArg?)
[R, ES2023]
Array<T>.prototype.findLastIndex(
predicate: (value: T, index: number, obj: Array<T>) => boolean,
thisArg?: any
): number
predicate
returns a truthy value.
-1
.
> [-1, 2, -3].findLastIndex(x => x < 0)
2
> [1, 2, 3].findLastIndex(x => x < 0)
-1
Array.prototype.*
: filtering and mappingArray.prototype.filter(predicate, thisArg?)
[R, ES5]
Array<T>.prototype.filter(
predicate: (value: T, index: number, array: Array<T>) => boolean,
thisArg?: any
): Array<T>
Returns an Array with only those elements for which predicate
returns a truthy value.
> [1, -2, 3].filter(x => x > 0)
[ 1, 3 ]
Array.prototype.map(callback, thisArg?)
[R, ES5]
Array<T>.prototype.map<U>(
mapFunc: (value: T, index: number, array: Array<T>) => U,
thisArg?: any
): Array<U>
Returns a new Array, in which every element is the result of mapFunc
being applied to the corresponding element of the receiver.
> [1, 2, 3].map(x => x * 2)
[ 2, 4, 6 ]
> ['a', 'b', 'c'].map((x, i) => i)
[ 0, 1, 2 ]
Array.prototype.flatMap(callback, thisArg?)
[R, ES2019]
Array<T>.prototype.flatMap<U>(
callback: (value: T, index: number, array: Array<T>) => U|Array<U>,
thisValue?: any
): Array<U>
The result is produced by invoking callback()
for each element of the original Array and concatenating the Arrays it returns.
> ['a', 'b', 'c'].flatMap(x => [x,x])
[ 'a', 'a', 'b', 'b', 'c', 'c' ]
> ['a', 'b', 'c'].flatMap(x => [x])
[ 'a', 'b', 'c' ]
> ['a', 'b', 'c'].flatMap(x => [])
[]
Array.prototype.flat(depth = 1)
[R, ES2019]
“Flattens” an Array: It descends into the Arrays that are nested inside the input Array and creates a copy where all values it finds at level depth
or lower are moved to the top level.
> [ 1,2, [3,4], [[5,6]] ].flat(0) // no change
[ 1, 2, [3,4], [[5,6]] ]
> [ 1,2, [3,4], [[5,6]] ].flat(1)
[1, 2, 3, 4, [5,6]]
> [ 1,2, [3,4], [[5,6]] ].flat(2)
[1, 2, 3, 4, 5, 6]
Array.prototype.*
: computing summariesArray.prototype.every(predicate, thisArg?)
[R, ES5]
Array<T>.prototype.every(
predicate: (value: T, index: number, array: Array<T>) => boolean,
thisArg?: any
): boolean
Returns true
if predicate
returns a truthy value for every element. Otherwise, it returns false
:
> [1, 2, 3].every(x => x > 0)
true
> [1, -2, 3].every(x => x > 0)
false
false
).
.some()
(“exists”).
Array.prototype.some(predicate, thisArg?)
[R, ES5]
Array<T>.prototype.some(
predicate: (value: T, index: number, array: Array<T>) => boolean,
thisArg?: any
): boolean
Returns true
if predicate
returns a truthy value for at least one element. Otherwise, it returns false
.
> [1, 2, 3].some(x => x < 0)
false
> [1, -2, 3].some(x => x < 0)
true
true
).
.every()
(“for all”).
Array.prototype.reduce(callback, initialValue?)
[R, ES5]
Array<T>.prototype.reduce<U>(
callback: (accumulator: U, element: T, index: number, array: Array<T>) => U,
initialValue?: U
): U
This method produces a summary of the receiver: it feeds all Array elements to callback
, which combines a current summary (in parameter accumulator
) with the current Array element and returns the next accumulator
:
const accumulator_0 = callback(initialValue, arr[0]);
const accumulator_1 = callback(accumulator_0, arr[1]);
const accumulator_2 = callback(accumulator_1, arr[2]);
// Etc.
The result of .reduce()
is the last result of callback
after it has visited all Array elements.
> [1, 2, 3].reduce((accu, x) => accu + x, 0)
6
> [1, 2, 3].reduce((accu, x) => accu + String(x), '')
'123'
If no initialValue
is provided, the Array element at index 0 is used and the element at index 1 is visited first. Therefore, the Array must have at least length 1.
Array.prototype.reduceRight(callback, initialValue?)
[R, ES5]
Array<T>.prototype.reduceRight<U>(
callback: (accumulator: U, element: T, index: number, array: Array<T>) => U,
initialValue?: U
): U
Works like .reduce()
, but visits the Array elements backward, starting with the last element.
> [1, 2, 3].reduceRight((accu, x) => accu + String(x), '')
'321'
Array.prototype.*
: converting to stringArray.prototype.join(separator = ',')
[R, ES1]
Creates a string by concatenating string representations of all elements, separating them with separator
.
> ['a', 'b', 'c'].join('##')
'a##b##c'
> ['a', 'b', 'c'].join()
'a,b,c'
Array.prototype.toString()
[R, ES1]
Converts all elements to strings via String()
, concatenates them while separating them with commas, and returns the result.
> [1, 2, 3].toString()
'1,2,3'
> ['1', '2', '3'].toString()
'1,2,3'
> [].toString()
''
Array.prototype.toLocaleString()
[R, ES3]
Works like .toString()
but converts its elements to strings via .toLocaleString()
(not via .toString()
) before separating them via commas and concatenating them to a single string – that it returns.
Array.prototype.*
: sorting and reversingArray.prototype.sort(compareFunc?)
[W, ES1]
Array<T>.prototype.sort(
compareFunc?: (a: T, b: T) => number
): this
.toSorted()
.
Sorting numbers:
// Default: lexicographical sorting
assert.deepEqual(
[200, 3, 10].sort(),
[10, 200, 3]
);
// Ascending numerical sorting (“from a to z”)
assert.deepEqual(
[200, 3, 10].sort((a, z) => a - z),
[3, 10, 200]
);
Sorting strings: By default, strings are sorted by code unit values (char codes), where, e.g., all unaccented uppercase letters come before all unaccented lowercase letters:
> ['pie', 'cookie', 'éclair', 'Pie', 'Cookie', 'Éclair'].sort()
[ 'Cookie', 'Pie', 'cookie', 'pie', 'Éclair', 'éclair' ]
For human languages, we can use Intl.Collator
:
const arr = ['pie', 'cookie', 'éclair', 'Pie', 'Cookie', 'Éclair'];
assert.deepEqual(
arr.sort(new Intl.Collator('en').compare),
['cookie', 'Cookie', 'éclair', 'Éclair', 'pie', 'Pie']
);
Array.prototype.toSorted(compareFunc?)
[R, ES2023]
Array<T>.prototype.toSorted.toSorted(
compareFunc?: (a: T, b: T) => number
): Array<T>
.sort()
.
const original = ['y', 'z', 'x'];
const sorted = original.toSorted();
assert.deepEqual(
// The original is unchanged
original, ['y', 'z', 'x']
);
assert.deepEqual(
// The copy is sorted
sorted, ['x', 'y', 'z']
);
See the description of .sort()
for more information on how to use this method.
Array.prototype.reverse()
[W, ES1]
Rearranges the elements of the receiver so that they are in reverse order and then returns the receiver.
> const arr = ['a', 'b', 'c'];
> arr.reverse()
[ 'c', 'b', 'a' ]
> arr
[ 'c', 'b', 'a' ]
The non-destructive version of this method is .toReversed()
.
Array.prototype.toReversed()
[R, ES2023]
.reverse()
.
const original = ['x', 'y', 'z'];
const reversed = original.toReversed();
assert.deepEqual(
// The original is unchanged
original, ['x', 'y', 'z']
);
assert.deepEqual(
// The copy is reversed
reversed, ['z', 'y', 'x']
);