---
title: Arrays
source: 'https://academia.sh/en/courses/javascript-fundamentals/arrays'
course: 'JavaScript Fundamentals'
language: en
updated: '2026-08-17T18:09:46+00:00'
license: 'CC BY-SA 4.0'
---

# Arrays

Ways to create an array and holes, index access, the distinction between mutating and non-mutating methods, the sorting trap, and transformation chains.

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

```js
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`:

```js
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:

```js
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

```js
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`.

```js
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**:

```js
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:

```js
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:

```js
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:

```js
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`:

```js
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.
