Skip to main content

📦 Working with Arrays

Week 2: JavaScript Deep Dive — course module banner illustration

Almost every program you write handles lists of things: products in a cart, messages in a chat, rows returned from a database. The array is JavaScript's fundamental tool for holding an ordered collection in a single named container — and mastering it is the difference between fighting your data and flowing with it.

Week 2 · Day 1 (Monday: Arrays and Objects) · Lecture 1

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Create arrays three different ways and choose the array literal by default
  • Access, update, and count elements using zero-based indexing and length
  • Add and remove items from either end with push, pop, unshift, and shift
  • Search arrays with indexOf, lastIndexOf, and includes
  • Distinguish the non-mutating slice from the mutating splice
  • Explain why arrays are reference types and copy them safely

Estimated Time: 60 minutes

Practice: Build reverse, de-duplicate, second-largest, and rotate helper functions.

In This Lesson

What Are Arrays?

Imagine a shopping list. Instead of inventing a separate variable for every item — item1, item2, item3 — an array lets you store the whole list in one ordered container. Think of it as a train: a single named engine pulls a line of numbered cars, and each car holds one piece of data.

graph TD A[fruits Array] --> B["index 0: Apple"] A --> C["index 1: Banana"] A --> D["index 2: Orange"] A --> E["index 3: Grape"]

The key words are ordered and indexed. Every element has a position — its index — and those indices start at 0, not 1. That single fact is the source of more beginner bugs than any other, so keep it front of mind: the first element lives at index 0, and the last lives at index length - 1.

Creating Arrays

Just as there are different ways to make a sandwich, there are several ways to create an array. In practice you'll reach for the first one almost every time.

// Method 1: Array literal — the idiomatic, most common way
const fruits = ['apple', 'banana', 'orange'];

// Method 2: Array constructor — rarely needed, and easy to misuse
const numbers = new Array(1, 2, 3, 4, 5);   // [1, 2, 3, 4, 5]
const oops    = new Array(3);               // [ <3 empty slots> ] — NOT [3]!

// Method 3: Empty array you fill in later
const emptyBasket = [];

// Arrays can hold any mix of data types
const mixedBag = ['text', 42, true, null, { name: 'John' }];

⚠️ The new Array() gotcha

new Array(3) does not create [3] — it creates an empty array of length 3. That inconsistency is exactly why the array literal [] is preferred: it always means what it looks like.

Real-world example: a shopping cart

Arrays really shine when each element is itself a small object. A shopping cart is a classic case — an ordered list of line items.

// A shopping cart in an e-commerce site
const shoppingCart = [
    { product: 'Laptop',   price: 999.99, quantity: 1 },
    { product: 'Mouse',    price: 29.99,  quantity: 2 },
    { product: 'Keyboard', price: 89.99,  quantity: 1 }
];

console.log(shoppingCart.length);        // 3 line items
console.log(shoppingCart[0].product);    // "Laptop"

Indexing & Length

Arrays are like an apartment building: each item has its own "apartment number" (index), and the numbering starts on the ground floor at 0.

An array of four colors with indices 0 through 3 'red' 'green' 'blue' 'yellow' index 0 index 1 index 2 index 3 ← last = length - 1
Indices run from 0 to length - 1. To reach the last element, use arr[arr.length - 1].
const colors = ['red', 'green', 'blue', 'yellow'];

// Accessing elements by index
console.log(colors[0]);      // 'red'   (first element)
console.log(colors[2]);      // 'blue'  (third element)

// length — how many elements the array holds
console.log(colors.length);  // 4

// Modifying an element — like repainting one apartment
colors[1] = 'lime';
console.log(colors);         // ['red', 'lime', 'blue', 'yellow']

// Reaching the last element reliably
console.log(colors[colors.length - 1]); // 'yellow'

// Modern shortcut (ES2022): .at() accepts negative indices
console.log(colors.at(-1));  // 'yellow'
console.log(colors.at(-2));  // 'blue'

💡 Use .at(-1) for the last element

The .at() method (ES2022) lets you count from the end with negative indices, so arr.at(-1) is a clean, readable way to grab the final element without the length - 1 arithmetic.

Adding & Removing at the Ends

Four methods add or remove elements from the ends of an array. They come in two pairs — one for the end and one for the start.

push & pop — work at the end

const playlist = ['Song A', 'Song B'];

// push() — add one (or more) to the END; returns the new length
playlist.push('Song C');
console.log(playlist);   // ['Song A', 'Song B', 'Song C']

// pop() — remove the LAST element; returns the removed item
const lastSong = playlist.pop();
console.log(lastSong);   // 'Song C'
console.log(playlist);   // ['Song A', 'Song B']

unshift & shift — work at the start

const queue = ['Person 2', 'Person 3'];

// unshift() — add to the FRONT; returns the new length
queue.unshift('Person 1');
console.log(queue);          // ['Person 1', 'Person 2', 'Person 3']

// shift() — remove the FIRST element; returns the removed item
const firstPerson = queue.shift();
console.log(firstPerson);    // 'Person 1'
console.log(queue);          // ['Person 2', 'Person 3']
MethodWhereActionReturns
push()EndAddNew length
pop()EndRemoveRemoved item
unshift()StartAddNew length
shift()StartRemoveRemoved item

Combining these gives you two classic data structures: push + pop makes a stack (last in, first out), while push + shift makes a queue (first in, first out).

Finding Elements

Searching an array is like looking for a book in a library. JavaScript gives you three quick tools depending on whether you want a position or a simple yes/no.

const books = ['JavaScript', 'Python', 'Java', 'C++', 'Python'];

// indexOf() — position of the FIRST match, or -1 if absent
console.log(books.indexOf('Python'));     // 1
console.log(books.indexOf('Ruby'));       // -1  (not found)

// lastIndexOf() — position of the LAST match
console.log(books.lastIndexOf('Python')); // 4

// includes() — a clean true/false existence check
console.log(books.includes('Java'));      // true
console.log(books.includes('Go'));        // false

✅ Prefer includes() for existence checks

Older code often wrote if (books.indexOf('Java') !== -1). Since ES2016, if (books.includes('Java')) says exactly the same thing and reads far better. Reserve indexOf for when you actually need the position.

slice vs splice — Array Surgery

These two look almost identical but behave completely differently. The one-letter difference in their names hides a critical distinction: slice copies and leaves the original alone; splice cuts into the original and changes it.

slice() — take a copy of a range (non-mutating)

const cake = ['layer1', 'layer2', 'layer3', 'layer4', 'layer5'];

// slice(start, end) — end is EXCLUSIVE; returns a new array
const middlePiece = cake.slice(1, 4);
console.log(middlePiece);  // ['layer2', 'layer3', 'layer4']
console.log(cake);         // original UNCHANGED

// A no-argument slice() is a quick way to shallow-copy an array
const copy = cake.slice();

splice() — remove and/or insert in place (mutating)

const ingredients = ['flour', 'sugar', 'eggs', 'butter', 'salt'];

// splice(startIndex, deleteCount) — removes and RETURNS removed items
const removed = ingredients.splice(1, 2);
console.log(removed);      // ['sugar', 'eggs']
console.log(ingredients);  // ['flour', 'butter', 'salt'] — MODIFIED

// splice can also insert: splice(startIndex, deleteCount, ...itemsToAdd)
ingredients.splice(1, 0, 'milk', 'vanilla');
console.log(ingredients);  // ['flour', 'milk', 'vanilla', 'butter', 'salt']

📖 Remember the difference

slice = copy. It never touches the original and returns the extracted range. splice is surgery: it mutates the array in place and returns whatever it removed. If you find yourself confused mid-project, that mnemonic will save you.

Arrays Are Reference Types

This is the concept that trips up nearly every JavaScript learner. When you assign one array to another variable, you are not making a copy — both names point at the same underlying array in memory. Change it through one name and the other "sees" the change too.

const original = [1, 2, 3];
const copy = original;   // NOT a copy — just a second label for the same array!

copy[0] = 99;
console.log(original);   // [99, 2, 3] — the "original" changed too

To make an independent copy, use one of these. Each produces a new array so edits don't leak back:

const original = [1, 2, 3];

const actualCopy1 = [...original];        // spread operator (most common)
const actualCopy2 = original.slice();     // slice with no arguments
const actualCopy3 = Array.from(original); // Array.from

actualCopy1[0] = 99;
console.log(original);    // [1, 2, 3] — safe, untouched

⚠️ These are shallow copies

Spread and slice copy the top level only. If your array holds objects, both arrays still share those nested objects. For a fully independent deep copy of simple data, structuredClone(original) (built into modern browsers and Node 17+) is the modern tool.

Practice & Quiz

🏋️ Exercise 1: Reverse without .reverse()

Goal: Write reverseArray(arr) that returns a new array with the elements in reverse order — without calling the built-in reverse().

function reverseArray(arr) {
    // TODO: build and return a new reversed array
}
console.log(reverseArray([1, 2, 3, 4]));  // should log: [4, 3, 2, 1]
💡 Hint

Start with an empty result array. Loop from the last index (arr.length - 1) down to 0, and push each element onto the result.

✅ Solution
function reverseArray(arr) {
    const result = [];
    for (let i = arr.length - 1; i >= 0; i--) {
        result.push(arr[i]);
    }
    return result;
}
console.log(reverseArray([1, 2, 3, 4]));  // [4, 3, 2, 1]

🏋️ Exercise 2: Remove duplicates

Goal: Write removeDuplicates(arr) that returns a new array with each value appearing only once.

💡 Hint

A Set stores only unique values. Spread the array into a Set, then spread it back into an array.

✅ Solution
function removeDuplicates(arr) {
    return [...new Set(arr)];
    // Classic alternative:
    // return arr.filter((item, index) => arr.indexOf(item) === index);
}
console.log(removeDuplicates([1, 2, 2, 3, 4, 4, 5])); // [1, 2, 3, 4, 5]

🏋️ Exercise 3: Second largest & rotate

Goal: Write secondLargest(arr) (returns the second-biggest number, or null) and rotateArray(arr, n) (shifts elements n places to the left).

✅ Solution
function secondLargest(arr) {
    if (arr.length < 2) return null;
    let first = -Infinity, second = -Infinity;
    for (const num of arr) {
        if (num > first) {
            second = first;
            first = num;
        } else if (num > second && num < first) {
            second = num;
        }
    }
    return second === -Infinity ? null : second;
}
console.log(secondLargest([10, 40, 30, 40, 20])); // 30

function rotateArray(arr, n) {
    n = n % arr.length;                       // handle n larger than length
    return [...arr.slice(n), ...arr.slice(0, n)];
}
console.log(rotateArray([1, 2, 3, 4, 5], 2)); // [3, 4, 5, 1, 2]

🎯 Quick Quiz

Question 1: Given const a = ['x', 'y', 'z'];, what is a[a.length - 1]?

Question 2: Which method changes the original array in place?

Question 3: After const b = a; you run b.push(9). What happened to a?

Best Practices & Pitfalls

✅ Do

  • Declare arrays with const — you can still push/pop; const only blocks reassignment of the variable
  • Use the array literal [] instead of new Array()
  • Reach for includes() for existence checks and .at(-1) for the last element
  • Copy before mutating when you need to keep the original: [...arr]

❌ Don't

  • Use delete arr[i] — it leaves a "hole" (a sparse array); use splice() instead
  • Assume const copy = original makes a copy — it shares the same array
  • Modify an array's length while iterating over it — you'll skip or repeat elements
  • Forget that array indices are zero-based (the classic off-by-one bug)

⚠️ delete creates sparse arrays

const sparse = [1, 2, 3];
delete sparse[1];       // leaves a hole, length stays 3
console.log(sparse);    // [1, <empty>, 3]  ← bad

const dense = [1, 2, 3];
dense.splice(1, 1);     // removes AND shifts everything down
console.log(dense);     // [1, 3]  ← what you actually wanted

Sparse arrays behave unpredictably with loops and methods. Always remove elements with splice(), not delete.

Summary

🎉 Key Takeaways

  • Arrays store an ordered, zero-indexed collection in one container — create them with a literal []
  • Reach elements by index; the last one is arr[arr.length - 1] or arr.at(-1)
  • push/pop work the end, unshift/shift work the start
  • slice copies (non-mutating); splice operates in place (mutating)
  • Arrays are reference types — copy with [...arr] before you mutate

📚 Additional Resources

🚀 What's Next?

Arrays hold data by position. Next you'll meet the other half of JavaScript's data toolkit — data organized by name: Object Literals and Properties, where you'll store related values under descriptive keys.

🎉 Great work!

Arrays are the building blocks of nearly every data structure and algorithm you'll write. You now have the fundamentals to manipulate them with confidence.