Objects & arrays (first half)
The two shapes almost everything you build is made of.
Object literals
const user = {
name: "Ana",
age: 29,
isAdmin: false,
address: { // objects nest freely
city: "Pune",
},
};
Two ways to reach a property, and they're not interchangeable. Dot notation needs a literal, valid identifier known when you write the code. Bracket notation takes any expression — a variable, a computed string, a key with a space in it.
user.name; // "Ana" — the key is a literal you typed
user["name"]; // same thing, spelled differently
const key = "age";
user[key]; // 26 — dot notation CAN'T do this; user.key would look for a property literally named "key"
user["favorite color"]; // dot notation can't have a space in it at all
As a reminder from the types chapter: an object variable holds a reference, not the data itself — copying the variable copies the pointer, not the object.
Shorthand and computed keys
const name = "Ana", age = 29;
const user2 = { name, age }; // shorthand — same as { name: name, age: age }
const field = "role";
const user3 = { [field]: "admin" }; // computed key — the property is named by field's VALUE
console.log(user3); // { role: "admin" }, not { field: "admin" }
Arrays — indexed, ordered, still objects underneath
const nums = [10, 20, 30];
nums[0]; // 10 — indexing starts at 0
nums.length; // 3
nums[nums.length - 1]; // 30 — the standard "last element" idiom
nums[10]; // undefined — out of range, not an error
typeof [] is "object" and
Array.isArray() is the only reliable check — both covered
back in the mental model
chapter. What actually makes an array useful is the ordered,
numerically-indexed methods below.
Mutating methods — they change the array in place
| Call | Does | Returns |
|---|---|---|
arr.push(x) | adds to the end | new length |
arr.pop() | removes from the end | the removed element |
arr.unshift(x) | adds to the start | new length |
arr.shift() | removes from the start | the removed element |
arr.splice(start, count, …items) | removes count at start, inserts …items there | array of removed elements |
arr.sort(cmp) | sorts in place | the same array |
arr.reverse() | reverses in place | the same array |
console.log([10, 1, 2].sort()); // what happens?
console.log([10, 1, 2].sort((a, b) => a - b)); // what happens?
Without a comparator, sort() converts everything to a
string and sorts lexicographically — so 10 comes
before 2, because "1" sorts before
"2". A comparator that returns negative/zero/positive is
the only reliable way to sort numbers.
push/shift and know that's a
trade-off, not a free choice.
Non-mutating methods — they read, they don't touch
| Call | Returns |
|---|---|
arr.slice(start, end) | a new array, end excluded — negative indices count from the back |
arr.indexOf(x) | first matching index, or -1 — compares with === |
arr.includes(x) | true/false — the one case where it differs from indexOf: it also matches NaN |
console.log([NaN].indexOf(NaN)); // what happens?
console.log([NaN].includes(NaN)); // what happens?
-1, then true. indexOf compares
with ===, and NaN === NaN is
false — so indexOf can never find a
NaN, no matter how many are in the array.
includes uses a different algorithm (SameValueZero) that
treats NaN as equal to itself. It's a small detail with a
real consequence: includes is the safer default unless
you specifically need the index back.
splice mutates,
slice doesn't — same six letters, opposite behavior. If
you're not sure whether a method is safe on a shared array, check
first; it's the single most common source of "why did this other
variable change too" bugs.
map / filter / find / forEach / reduce
Five methods that all walk the array element by element — the difference is entirely in what each one hands back.
| Method | Gives back | Use it when |
|---|---|---|
.map(fn) | a new array, same length | you're transforming every element |
.filter(fn) | a new array, shorter or equal | you're keeping some elements, dropping others |
.find(fn) | one element, or undefined | you want the first match and nothing else |
.forEach(fn) | nothing (undefined) | you're only running side effects — no new array |
.reduce(fn, initial) | whatever you build up | collapsing the array into one value — a sum, an object, another array |
const cart = [
{ name: "Pen", price: 20, qty: 3 },
{ name: "Book", price: 150, qty: 1 },
{ name: "Eraser", price: 5, qty: 0 },
];
cart.map(item => item.name); // ["Pen", "Book", "Eraser"]
cart.filter(item => item.qty > 0); // Pen and Book only
cart.find(item => item.price > 100); // the Book object itself
cart.forEach(item => console.log(item.name)); // logs 3 times, returns undefined
cart.reduce((total, item) => total + item.price * item.qty, 0); // 210
break doesn't work inside a forEach
callback, and returning from it just skips to the next
element — it does not exit the loop. Need to stop early? Use a real
for/for...of loop, or .find/
.some if you're really just searching.
Destructuring
Unpacking values out of an object or array into their own named variables, in one line instead of one assignment per field.
// Object destructuring — order doesn't matter, names must match
const { name, age } = user;
const { name: fullName } = user; // rename while unpacking
const { role = "guest" } = user; // default when the key is missing
const { address: { city } } = user; // nested, straight to "city"
// Array destructuring — position IS the match, gaps are allowed
const [first, , third] = [10, 20, 30]; // skips index 1
const [head, ...tail] = [1, 2, 3, 4]; // head = 1, tail = [2, 3, 4]
let x = 1, y = 2;
[x, y] = [y, x];
console.log(x, y); // what happens?
2 1 — swapped, with no temporary variable. The right
side builds a whole new array [y, x] first, then
destructuring unpacks it back into x and y
in one step.
Destructuring is everywhere a value shows up, including function parameters — a very common way to accept an options object:
function createUser({ name, age = 18 }) {
return name + " is " + age;
}
createUser({ name: "Ana" }); // "Ana is 18"
Spread — the opposite of destructuring
... on the way in (an array/object literal, a
function call) expands a collection into its individual elements.
const a = [1, 2, 3];
const b = [...a, 4, 5]; // [1, 2, 3, 4, 5] — a new array
const base = { name: "Ana", age: 29 };
const patched = { ...base, age: 30 }; // { name: "Ana", age: 30 } — later keys win
Math.max(...a); // spreads the array into 3 separate arguments
{ ...base } makes a fresh top-level object, but any
property that's itself an object or array is still the
same reference, shared between the original and the copy.
Mutate a nested field through the copy and the original sees it too —
the reference-copying rule from earlier never went away, spread just
copies the outer layer for you.
const original = { nested: { count: 1 } };
const copy = { ...original };
copy.nested.count = 99;
console.log(original.nested.count); // 99 — same nested object, not a copy of itOpens in the editor — write it, run it, and check it against real tests.