Skip to content

05 · Arrays & Objects

Arrays — ordered, mutable lists

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

fruits.push("date");        // add to end
fruits.unshift("avocado");  // add to start
fruits.splice(2, 1);         // remove 1 item starting at index 2
const last = fruits.pop();   // remove & return last item

console.log(fruits[0]);       // first item
console.log(fruits.at(-1));   // last item (modern, cleaner than fruits[fruits.length - 1])
console.log(fruits.slice(1, 3)); // shallow copy of a range, doesn't mutate
console.log(fruits.length);

Common array methods

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

const doubled = numbers.map((n) => n * 2);          // [2, 4, 6, 8, 10]
const evens = numbers.filter((n) => n % 2 === 0);    // [2, 4]
const sum = numbers.reduce((total, n) => total + n, 0); // 15
const found = numbers.find((n) => n > 3);             // 4
const hasNegative = numbers.some((n) => n < 0);        // false
const allPositive = numbers.every((n) => n > 0);       // true

numbers.forEach((n) => console.log(n)); // side-effect iteration, no return value

console.log([...numbers].sort((a, b) => b - a)); // [5, 4, 3, 2, 1] — sort a copy!
console.log(numbers.includes(3)); // true
console.log(numbers.join(", ")); // "1, 2, 3, 4, 5"

map, filter, and reduce all return new arrays or values without mutating the original — prefer them over manual loops for transformations.

Destructuring and the spread operator

const [first, second, ...rest] = fruits;
console.log(first, second, rest); // avocado banana [ 'cherry', 'date' ]

const combined = [...fruits, "elderberry"]; // spread into a new array

Objects — key/value pairs

const person = { name: "Ada", age: 30 };

person.email = "ada@example.com";  // add/update via dot notation
person["age"] = 31;                 // bracket notation — needed for dynamic keys
delete person.age;                   // remove a key

for (const key in person) {
  console.log(key, person[key]);
}

for (const [key, value] of Object.entries(person)) {
  console.log(key, value);
}

console.log(Object.keys(person));   // ['name', 'email']
console.log(Object.values(person)); // ['Ada', 'ada@example.com']

Object destructuring and shorthand

const { name, email } = person;
console.log(name, email); // Ada ada@example.com

const city = "London";
const country = "UK";
const address = { city, country }; // shorthand: same as { city: city, country: country }

function describe({ name, age = 0 }) { // destructure directly in parameters
  return `${name} (${age})`;
}
console.log(describe({ name: "Grace" })); // Grace (0)

Spread and optional chaining

const base = { role: "user" };
const admin = { ...base, role: "admin", canDelete: true }; // spread + override

const nested = { profile: { social: null } };
console.log(nested.profile?.social?.twitter); // undefined, no error — optional chaining
console.log(nested.profile?.social?.twitter ?? "none"); // "none" — nullish coalescing

Choosing the right structure

Need Use
Ordered list, index-based access Array
Key/value pairs, known field names plain Object
Frequent additions/removals by key, guaranteed insertion order Map
Unique values, fast membership tests Set
const uniqueTags = new Set(["js", "web", "js"]);
console.log(uniqueTags); // Set(2) { 'js', 'web' }

const scoreboard = new Map();
scoreboard.set("Ada", 10);
scoreboard.set("Grace", 15);
console.log(scoreboard.get("Ada")); // 10

How It Actually Works

Arrays in V8 aren't a single data structure — they silently switch between several internal representations depending on how you use them. A "packed" array of small integers ([1,2,3]) is stored as a contiguous PACKED_SMI_ELEMENTS backing store, essentially a raw C array, giving O(1) indexed access with no boxing. The moment you do something like arr[0] = 1.5 or arr[10] = 'x' on a 3-element array, V8 has to transition the array to a more general representation (PACKED_DOUBLE_ELEMENTS, then PACKED_ELEMENTS, and finally DICTIONARY_ELEMENTS if you create sparse holes like arr[1000] = 1). Each transition is one-way and makes every future element access slightly slower, because V8 now has to handle more cases per access.

Object property access works through the hidden-class mechanism mentioned earlier: obj.name doesn't do a hash-map lookup on every call. The first time V8 sees that access site, it records "objects with this hidden class have name at offset 2" as an inline cache (IC). Every subsequent access at that same call site checks "does this object still have that hidden class?" — if yes, it reads the offset directly with no lookup at all (a monomorphic hit). Object.freeze, deleting properties, or adding properties in inconsistent orders across similar objects all defeat this optimization by forcing hidden-class transitions or dictionary-mode storage.

🔀 See this in another language

Exercise

Given an array of words, use .reduce() to build an object mapping each unique word to its length, then use a Set to find which words appear more than once in the original array.