Lesson 17 / 21
Arrays
Ways to create an array and holes, index access, the distinction between mutating and non-mutating methods, the sorting trap, and transformation chains.
Contents
The previous topic used methods like filter, map, and split without explaining
them. This topic defines them. The Data Structures course examined an array’s internal
layout, cost, and a dynamic array’s amortized behavior; here, the concrete behavior and
traps of JavaScript’s array object are the subject.
A JavaScript array is not a one-to-one match for the contiguous-memory model from the
Data Structures course. Its typeof result is object; it is a special object that
carries indices as keys and whose length property updates itself. This distinction
directly explains several behaviors.
Creation
const direct = [21.5, 19.75, 23]; const sparse = new Array(3); const singleElement = Array.of(3); const fromText = Array.from("A1"); const generated = Array.from({ length: 3 }, (_, i) => i * 10); console.log(direct, direct.length); console.log(sparse, sparse.length); console.log(singleElement, fromText); console.log(generated); console.log(Array.isArray(direct), typeof direct);
[ 21.5, 19.75, 23 ] 3 [ <3 empty items> ] 3 [ 3 ] [ 'A', '1' ] [ 0, 10, 20 ] true object
The second line shows a trap: the constructor called with a single numeric argument
does not turn that value into an element, it produces an empty array of that length.
Array.of removes this ambiguity and counts the argument as an element.
Array.from does two jobs: it converts an iterable value into an array, and it produces
an array from an object that has a length field. Its second argument is a mapping
function called for every element. The underscore name is a writing convention chosen
for an unused parameter; it is not a rule of the language.
The last line is a type check: whether a value is an array cannot be told with
typeof; it is tested with Array.isArray.
Holes
Some indices of an array may hold no element at all. These holes are different from
elements carrying the value undefined:
const sparse = new Array(3); console.log(sparse[0], 0 in sparse); console.log(sparse.map(() => 1)); console.log(Array.from({ length: 3 }, () => 1));
undefined false [ <3 empty items> ] [ 1, 1, 1 ]
The first line shows the distinction: reading gave undefined, but the index is not
in the array. The second line is the consequence: the mapping method skips holes, and
the callback never runs. The third line is the correct way.
Holes also form by writing directly to an index:
const arr = [21.5, 19.75]; arr.length = 1; console.log(arr); arr[4] = 99; console.log(arr, arr.length);
[ 21.5 ] [ 21.5, <3 empty items>, 99 ] 5
Writing to the length field truncates the array. Writing to an index beyond the length
leaves the indices in between empty and grows the length value. This course’s rule:
arrays are not left with holes; adding is done with push, generating with
Array.from.
The <3 empty items> shown in the output is the runtime’s inspection format; it is used
to distinguish a hole from the value undefined, and it varies across runtimes.
Access and Search
const readings = [21.5, 19.75, 23, 18.25]; console.log(readings[0], readings[readings.length - 1], readings.at(-1), readings.at(-2)); console.log(readings[10]); console.log(readings.indexOf(23), readings.includes(23)); const withNaN = [NaN, 1]; console.log(withNaN.indexOf(NaN), withNaN.includes(NaN));
21.5 18.25 18.25 23 undefined 2 true -1 true
An out-of-range index does not throw; it gives undefined. The at method accepts a
negative index and counts from the end.
The last line pays off a hint left in the Equality Comparisons lesson: indexOf uses
strict equality, and since NaN is not equal to anything, it cannot be found;
includes uses a relation close to same-value equality and does find NaN. The two
methods do not ask the same question.
Mutating and Non-Mutating Methods
Arrays are mutable objects. Methods split into two classes, and not knowing which class a method belongs to produces silent bugs in shared data.
Mutating methods update the array in place: push, pop, shift, unshift,
splice, sort, reverse, fill.
Non-mutating methods return a new array: slice, concat, map, filter, flat,
toSorted, toReversed.
const original = [21.5, 19.75, 23]; const copy = original.slice(); copy.push(18.25); console.log(original, copy); const reversed = original.toReversed(); console.log(original, reversed); const inPlace = [...original]; inPlace.reverse(); console.log(inPlace);
[ 21.5, 19.75, 23 ] [ 21.5, 19.75, 23, 18.25 ] [ 21.5, 19.75, 23 ] [ 23, 19.75, 21.5 ] [ 23, 19.75, 21.5 ]
A copy taken with spread syntax ([...original]) or with slice is shallow: the
array itself is new, but the elements are the same objects. The shallow-copy/deep-copy
distinction from the Programming Fundamentals course applies here; in an array of
objects, a change made to an element is visible from both arrays.
The Sorting Trap
When given no comparison function, the sort method converts elements to text and
sorts them in lexicographic order:
const numbers = [21.5, 19.75, 100, 3]; console.log([...numbers].sort()); console.log([...numbers].sort((a, b) => a - b)); console.log(numbers.toSorted((a, b) => b - a)); console.log(numbers);
[ 100, 19.75, 21.5, 3 ] [ 3, 19.75, 21.5, 100 ] [ 100, 21.5, 19.75, 3 ] [ 21.5, 19.75, 100, 3 ]
The first line is numerically wrong but correct by the rule: the text "100" comes
before the text "3". A comparison function is mandatory for numeric sorting. The
function reports order by returning negative, zero, or positive; a - b gives ascending
order, b - a descending.
The last two lines also show the mutating/non-mutating distinction: toSorted did not
disturb the original array. Since the sort calls were made on copies, numbers never
changed.
Transformation Chains
Most work on the measurement list is written by chaining a few methods:
const records = [ { station: "A1", temperature: 21.5 }, { station: "A2", temperature: 19.75 }, { station: "B1", temperature: 23 }, { station: "B2", temperature: 18.25 }, ]; console.log(records.map((r) => r.temperature)); console.log(records.filter((r) => r.temperature > 20).map((r) => r.station)); console.log(records.reduce((t, r) => t + r.temperature, 0)); console.log(records.find((r) => r.station === "B1")); console.log(records.findIndex((r) => r.temperature > 22)); console.log(records.some((r) => r.temperature > 22), records.every((r) => r.temperature > 15));
[ 21.5, 19.75, 23, 18.25 ]
[ 'A1', 'B1' ]
82.5
{ station: 'B1', temperature: 23 }
2
true true
These are the map/filter/reduce trio from the Programming Fundamentals course. find
gives the first matching element, findIndex its index; if not found, they return
undefined and -1 respectively. some and every produce a boolean and
short-circuit.
Reduce produces more than one summary in a single pass when the accumulator is an object:
const records = [ { station: "A1", temperature: 21.5 }, { station: "A2", temperature: 19.75 }, { station: "B1", temperature: 23 }, ]; const summary = records.reduce( (accumulator, record) => ({ count: accumulator.count + 1, total: accumulator.total + record.temperature, highest: Math.max(accumulator.highest, record.temperature), }), { count: 0, total: 0, highest: -Infinity }, ); console.log(summary); console.log("average:", summary.total / summary.count);
{ count: 3, total: 64.25, highest: 23 }
average: 21.416666666666668
The initial value looks optional, but it is not:
try { console.log([].reduce((a, b) => a + b)); } catch (error) { console.log(error.name + ": " + error.message); } console.log([].reduce((a, b) => a + b, 0));
TypeError: Reduce of empty array with no initial value 0
If no initial value is given, the first element becomes the accumulator, and an empty array has no such element. The rule: reduce is always written with an initial value.
Nested lists are flattened with flat and flatMap:
const groups = [[21.5, 19.75], [23], [], [18.25]]; console.log(groups.flat()); console.log(groups.flatMap((g) => g.map((d) => d * 2))); console.log([1, [2, [3, [4]]]].flat(2));
[ 21.5, 19.75, 23, 18.25 ] [ 43, 39.5, 46, 36.5 ] [ 1, 2, 3, [ 4 ] ]
By default, flat opens only one level; depth is given as an argument. In the last
line, two levels were opened, and the third level stayed an array.
Summary
- The array constructor with a single numeric argument produces an empty array of that
length; use
Array.ofor a direct literal for an element. - Holes differ from elements carrying
undefinedand are skipped by mapping methods; writing to thelengthfield truncates the array or opens a hole. - An out-of-range index gives
undefined;ataccepts a negative index;indexOfcannot findNaN,includescan. - Methods split into mutating and non-mutating; copies are shallow.
- Sorting with no comparison function converts to text and applies lexicographic order.
- Reduce is written with an initial value; without one, it throws on an empty array.
Next Step
This lesson used records as objects, but object syntax was never defined. The next lesson takes up objects: the two forms of property access, shorthand syntax, computed keys, destructuring, and spread. Grouping the measurement records by station will also be written there.
To keep your progress and take notes, Log in
My notes
Log in to take notes.