Skip to content
academia.sh

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.

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.of or a direct literal for an element.
  • Holes differ from elements carrying undefined and are skipped by mapping methods; writing to the length field truncates the array or opens a hole.
  • An out-of-range index gives undefined; at accepts a negative index; indexOf cannot find NaN, includes can.
  • 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.

Start typing to search.

↑↓ Esc navigate · open · close