B11 · Part 1 · Beginner
Objects & arrays
The two shapes almost everything you build is made of.
11On this page
- Object literals
- Assignment copies the reference, not the object
- Shorthand and computed keys
- Arrays — indexed, ordered, still objects underneath
- Mutating methods — they change the array in place
- Non-mutating methods — they read, they don't touch
- map / filter / find / forEach / reduce
- some, every, and findIndex — checking, not collecting
- Destructuring
- Spread — the opposite of destructuring
- What comes next
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]; // 29 — 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.
Assignment copies the reference, not the object
const original = { count: 1 };
const alias = original; // alias points at the SAME object — nothing was copied
alias.count = 99;
console.log(original.count); // what happens?
99 — original and alias are two
names for the exact same object in memory, so mutating one mutates
the other; there was only ever one object. This is true for arrays
too, and it's the single most common source of "I only changed one
variable, why did this other one change too" bugs. Comparing them
tells the same story:
const a = { x: 1 };
const b = { x: 1 };
console.log(a === b); // false — same shape, DIFFERENT objects
console.log(a === a); // true — same object, obviously
⚠ A real copy needs an explicit copy
const copy = original; is not a copy — it's a second name
for the same thing. Actually copying means spread
({ ...original }) or Object.assign({}, original),
covered a few sections down — and even those only copy one level deep.
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 types
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/pop are cheap, shift/unshift are not
Adding or removing at the end of an array is O(1). Doing it
at the start is O(n) — every other element has to shift
index. For a queue you fill from one end and drain from the other,
reach for
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.
Rule
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
Dry run: cart.reduce building up the total
| Item | price * qty | total before | total after |
|---|---|---|---|
| Pen | 20 * 3 = 60 | 0 (the initial value) | 60 |
| Book | 150 * 1 = 150 | 60 | 210 |
| Eraser | 5 * 0 = 0 | 210 | 210 |
total is the one value that survives from call to call —
reduce hands back whatever the callback returned last time
as the next call's total. The final 210 is
just the last row's "total after," nothing more mysterious than that.
⚠ forEach can't be stopped, and its return is
thrown away
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.
Say it like this → "map and filter are for
building a new array. forEach is for side effects — logging,
pushing into something outside the callback. reduce is the general
case underneath map and filter — either one could be written with
reduce, but reduce for a simple transform reads worse, not better."
some, every, and findIndex — checking, not collecting
Three more array walkers, and none of them build a new array — they answer a yes/no or "where" question instead.
| Method | Returns | Stops early? |
|---|---|---|
.some(fn) | true the moment any element passes | yes — as soon as one match is found |
.every(fn) | true only if every element passes | yes — as soon as one fails |
.findIndex(fn) | the index of the first match, or -1 | yes |
cart.some(item => item.qty === 0); // true — the Eraser is out of stock
cart.every(item => item.price > 0); // true — nothing is free
cart.findIndex(item => item.name === "Book"); // 1 — position, not the item itself
⚠ every() on an empty array is true
[].every(x => x > 100) is true — there's no
element to fail the check, a case called "vacuous truth." The
opposite empty-array case is the safer one to remember:
[].some(...) is always false, since nothing
can pass either.
.find() and .findIndex() are the same walk,
different return value — reach for .find() when you need
the element, .findIndex() when you need its position (to
splice it out, say). Their mirror images,
.findLast() and .findLastIndex() — covered
with the rest of the newer array methods in
Objects deeply — walk from the end
instead.
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
⚠ Spread copies one level deep only
{ ...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 it
What comes next
The rest of the toolbox is Objects deeply:
getters and setters, the Object statics, shallow versus deep
copying, Map and Set and their weak versions, sort
stability, JSON with a replacer and reviver, and the array methods
this chapter did not reach. It is written to follow directly from here.
Interview answer, one sentence
"Objects and arrays are reference types, so assignment copies the reference rather than the data — the reason changing a "copy" changes the original — and knowing which array methods mutate (push, sort, splice) and which return a new array (map, filter, slice) prevents most state bugs."
Before you move on
- Explain why
alias.count = 99changesoriginal.counttoo, and how spread orObject.assignavoids it. - Name which array methods mutate in place and which return a new array, without looking at the tables.
- Trace a
.reduce()call by hand, one element at a time, writing down the accumulator after each step. - Explain why
[NaN].indexOf(NaN)is-1but[NaN].includes(NaN)istrue. - Destructure a nested object with a default at every level, and say what happens if a middle level is missing.
Opens in the editor: write it, run it, and check it against real tests.
All 35 for this level →Finished reading?
0 of 41 chapters read in this series