Episode 1 — Fundamentals / 1.21 — Arrays

1.21.b -- Accessing Elements

In one sentence: JavaScript arrays use zero-based indexing with bracket notation arr[i], the modern at() method for negative indices, destructuring for elegant unpacking, and several search methods to find elements by value.

Navigation: <- 1.21.a -- Creating Arrays . 1.21.c -- Array Operations ->


1. Zero-based indexing explained

Arrays in JavaScript (and most programming languages) count from 0, not 1.

Index:    0        1        2        3        4
        ┌────────┬────────┬────────┬────────┬────────┐
Value:  │ "apple"│"banana"│"cherry"│ "date" │ "fig"  │
        └────────┴────────┴────────┴────────┴────────┘
const fruits = ["apple", "banana", "cherry", "date", "fig"];

// First element is at index 0
console.log(fruits[0]);  // "apple"

// Second element is at index 1
console.log(fruits[1]);  // "banana"

// Last element is at index (length - 1)
console.log(fruits[4]);  // "fig"

Why zero-based? The index represents the offset from the start of the array. The first element is 0 positions away from the beginning.


2. Accessing with bracket notation arr[index]

Bracket notation is the fundamental way to read and write array elements.

const scores = [95, 82, 74, 100, 88];

// Reading
console.log(scores[0]);   // 95
console.log(scores[2]);   // 74

// Writing (modifying)
scores[2] = 90;
console.log(scores[2]);   // 90
console.log(scores);      // [95, 82, 90, 100, 88]

// You can also use variables as indices
const i = 3;
console.log(scores[i]);   // 100

// Expressions work too
console.log(scores[1 + 1]); // 90 (index 2)

Setting beyond current length extends the array:

const arr = [10, 20, 30];
arr[5] = 60;
console.log(arr);          // [10, 20, 30, <2 empty items>, 60]
console.log(arr.length);   // 6

3. Accessing the last element

Getting the last element is one of the most common operations. There are multiple ways:

const colors = ["red", "green", "blue", "yellow", "purple"];

// Classic: arr[arr.length - 1]
console.log(colors[colors.length - 1]);  // "purple"

// Modern: arr.at(-1)  (ES2022)
console.log(colors.at(-1));              // "purple"

// Destructuring with rest (less efficient for just the last item)
const last = colors[colors.length - 1];

Common pattern for second-to-last:

console.log(colors[colors.length - 2]);  // "yellow"
console.log(colors.at(-2));              // "yellow"

4. Array.at() method (ES2022)

at() works like bracket notation but supports negative indices that count from the end.

const letters = ["a", "b", "c", "d", "e"];

// Positive indices -- same as bracket notation
letters.at(0);    // "a"
letters.at(2);    // "c"

// Negative indices -- count from the end
letters.at(-1);   // "e"  (last)
letters.at(-2);   // "d"  (second to last)
letters.at(-5);   // "a"  (first, same as at(0))

// Out of bounds
letters.at(10);   // undefined
letters.at(-10);  // undefined

Comparison with bracket notation:

OperationBracket notationat() method
First elementarr[0]arr.at(0)
Third elementarr[2]arr.at(2)
Last elementarr[arr.length - 1]arr.at(-1)
Second to lastarr[arr.length - 2]arr.at(-2)

Why at() is useful: It makes code more readable when accessing from the end, especially in chained expressions.

// Without at() -- verbose
const lastChar = "hello".split("")[("hello".split("").length - 1)];

// With at() -- clean
const lastChar2 = "hello".split("").at(-1);  // "o"

5. Out-of-bounds access (undefined, not error)

Unlike many languages that throw errors, JavaScript silently returns undefined for invalid indices.

const arr = [10, 20, 30];

console.log(arr[0]);     // 10
console.log(arr[2]);     // 30
console.log(arr[3]);     // undefined  (no error!)
console.log(arr[100]);   // undefined
console.log(arr[-1]);    // undefined  (negative indices don't work with brackets)

// This can hide bugs!
const name = arr[5];     // undefined -- no crash, but wrong data
console.log(name.toUpperCase()); // TypeError: Cannot read properties of undefined

Defensive access patterns:

const arr = [10, 20, 30];

// Check before accessing
if (arr.length > 3) {
  console.log(arr[3]);
}

// Default value with nullish coalescing
const value = arr[5] ?? "default";  // "default"

// Default value with logical OR (careful: 0 and "" are falsy)
const value2 = arr[5] || "default"; // "default"

6. Checking if an index exists

const arr = [10, undefined, 30];

// Using `in` operator -- checks if index exists as a property
0 in arr;  // true
1 in arr;  // true  (index 1 exists, even though value is undefined)
5 in arr;  // false

// Sparse array example
const sparse = [1, , 3];  // hole at index 1
0 in sparse;  // true
1 in sparse;  // false  (hole -- index does not exist)
2 in sparse;  // true

// Using hasOwnProperty
arr.hasOwnProperty(0);  // true
arr.hasOwnProperty(5);  // false

// The difference: undefined value vs. missing index
const a = [10, undefined, 30];  // index 1 EXISTS with value undefined
const b = [10, , 30];           // index 1 DOES NOT EXIST (hole)

1 in a;  // true
1 in b;  // false
a[1];    // undefined (from both!)
b[1];    // undefined (from both!)

7. Destructuring arrays

Array destructuring lets you unpack values from arrays into distinct variables.

const rgb = [255, 128, 0];

// Without destructuring
const red   = rgb[0];
const green = rgb[1];
const blue  = rgb[2];

// With destructuring -- cleaner
const [r, g, b] = rgb;
console.log(r);  // 255
console.log(g);  // 128
console.log(b);  // 0

Skipping elements:

const scores = [95, 82, 74, 100, 88];

// Skip second and third
const [first, , , fourth] = scores;
console.log(first);   // 95
console.log(fourth);  // 100

Rest pattern ...rest:

const numbers = [1, 2, 3, 4, 5];

const [head, ...tail] = numbers;
console.log(head);  // 1
console.log(tail);  // [2, 3, 4, 5]

const [a, b, ...rest] = numbers;
console.log(a);     // 1
console.log(b);     // 2
console.log(rest);  // [3, 4, 5]

Default values:

const [x = 0, y = 0, z = 0] = [10, 20];
console.log(x);  // 10
console.log(y);  // 20
console.log(z);  // 0  (default, since no third element)

Nested destructuring:

const matrix = [[1, 2], [3, 4]];
const [[a, b], [c, d]] = matrix;
console.log(a, b, c, d);  // 1 2 3 4

Practical example -- function returning multiple values:

function getMinMax(arr) {
  let min = arr[0];
  let max = arr[0];
  for (let i = 1; i < arr.length; i++) {
    if (arr[i] < min) min = arr[i];
    if (arr[i] > max) max = arr[i];
  }
  return [min, max];
}

const [minimum, maximum] = getMinMax([5, 2, 9, 1, 7]);
console.log(minimum);  // 1
console.log(maximum);  // 9

8. Swapping elements with destructuring

Before ES6, swapping required a temporary variable. Destructuring makes it one line.

let a = 1;
let b = 2;

// Old way
let temp = a;
a = b;
b = temp;

// Modern way -- destructuring swap
[a, b] = [b, a];
console.log(a);  // 2
console.log(b);  // 1

Swapping array elements in place:

const arr = ["first", "second", "third"];

// Swap index 0 and index 2
[arr[0], arr[2]] = [arr[2], arr[0]];
console.log(arr);  // ["third", "second", "first"]

// Swap any two indices
function swap(array, i, j) {
  [array[i], array[j]] = [array[j], array[i]];
}

const nums = [10, 20, 30, 40];
swap(nums, 1, 3);
console.log(nums);  // [10, 40, 30, 20]

9. Finding elements (preview of array methods)

These methods help you locate elements. They are covered in depth in 1.22 -- Array Methods, but knowing the basics here is essential.

indexOf() -- find index of a value

const fruits = ["apple", "banana", "cherry", "banana"];

fruits.indexOf("banana");    // 1  (first occurrence)
fruits.indexOf("grape");     // -1 (not found)
fruits.indexOf("banana", 2); // 3  (search from index 2)

includes() -- check if a value exists (ES2016)

const nums = [1, 2, 3, 4, 5];

nums.includes(3);     // true
nums.includes(10);    // false
nums.includes(3, 3);  // false (search from index 3)

// includes vs indexOf
// indexOf returns index (-1 if missing); includes returns boolean
// includes handles NaN correctly
[NaN].indexOf(NaN);   // -1  (broken!)
[NaN].includes(NaN);  // true (correct!)

find() -- find first element matching a condition

const users = [
  { name: "Alice", age: 25 },
  { name: "Bob",   age: 30 },
  { name: "Carol", age: 28 }
];

const found = users.find(user => user.age > 26);
console.log(found);  // { name: "Bob", age: 30 }

const notFound = users.find(user => user.age > 50);
console.log(notFound);  // undefined

findIndex() -- find index of first match

const scores = [45, 82, 73, 91, 68];

const idx = scores.findIndex(score => score > 80);
console.log(idx);  // 1

const noIdx = scores.findIndex(score => score > 100);
console.log(noIdx);  // -1

Quick comparison:

MethodReturnsUse when
indexOf(val)Index or -1Searching for a primitive value
includes(val)BooleanChecking existence of a primitive
find(fn)Element or undefinedSearching with a condition / objects
findIndex(fn)Index or -1Need index of a conditional match

10. Real example: accessing data from API responses

Arrays from APIs often need careful access patterns.

// Simulated API response
const apiResponse = {
  status: "success",
  data: {
    users: [
      { id: 1, name: "Alice", roles: ["admin", "user"] },
      { id: 2, name: "Bob",   roles: ["user"] },
      { id: 3, name: "Carol", roles: ["user", "editor"] }
    ]
  }
};

// Accessing nested array data
const users = apiResponse.data.users;

// First user's name
console.log(users[0].name);        // "Alice"

// Last user using at()
console.log(users.at(-1).name);    // "Carol"

// First user's first role
console.log(users[0].roles[0]);    // "admin"

// Destructuring the response
const [firstUser, ...otherUsers] = users;
console.log(firstUser.name);       // "Alice"
console.log(otherUsers.length);    // 2

// Safe access with optional chaining
console.log(users[10]?.name);           // undefined (no error)
console.log(users[0]?.roles?.[5]);      // undefined (no error)

// Find a specific user
const bob = users.find(u => u.name === "Bob");
console.log(bob?.roles);                // ["user"]

Key takeaways

  1. Arrays are zero-indexed -- the first element is arr[0], the last is arr[arr.length - 1].
  2. Bracket notation arr[i] is the fundamental access pattern; it returns undefined for missing indices (no error).
  3. at() (ES2022) supports negative indices: arr.at(-1) is the last element.
  4. Destructuring const [a, b, ...rest] = arr unpacks arrays into variables cleanly.
  5. Swapping is a one-liner with destructuring: [a, b] = [b, a].
  6. Use indexOf/includes for primitives, find/findIndex for objects or conditions.
  7. Always guard against out-of-bounds access with length checks or optional chaining.

Explain-It Challenge

Explain without notes:

  1. Why does arr[-1] return undefined in bracket notation, but arr.at(-1) returns the last element?
  2. What is the difference between a hole in a sparse array and an index whose value is undefined?
  3. Write destructuring that extracts the first element, skips the second, and collects the rest into a variable.
  4. Why does [NaN].indexOf(NaN) return -1 while [NaN].includes(NaN) returns true?

Navigation: <- 1.21.a -- Creating Arrays . 1.21.c -- Array Operations ->