Skip to the chapter
←JavaScriptGGroundworkJSJavaScript
JavaScript/Beginner/B11
11 / 41 written chapters
/

The cover
  1. B1Setup & mental model
  2. B2Execution context
  3. B3var, let & const
  4. B4Scope
  5. B5Single thread
  6. B6In the browser
  7. B7Types & values
  8. B8Strings & Math
  9. B9Operators & flow
  10. B10Functions
  11. B11Objects & arrays
  12. B12this
  13. B13Closures
  14. B14DOM & events
  15. B15Callbacks & promises
  16. B16Errors & tools
  17. B17Guided project
  18. ★Cheat page
  1. I1Scope & functions, properly
  2. I2Objects deeply
  3. I3Prototypes & OOP
  4. I4Async, properly
  5. I5Modules & tooling
  6. I6Regex & dates
  7. I7Error handling
  8. I8Real-time connections
  9. I9Offline & storage
  10. I10Browser APIs
  11. I11Modern JS
  12. ★Cheat page
  1. A1Engine & memory
  2. A2Advanced async
  3. A3Metaprogramming
  4. A4Types & data
  5. A5Patterns & architecture
  6. A6Performance
  7. A7Security
  8. A8Ecosystem
  9. A9Testing
  10. A10Observability
  11. A11Interview bank
  12. A12Build it yourself
  13. ★Cheat page

JSB11 · Part 1 · Beginner

Objects & arrays

The two shapes almost everything you build is made of.

8 min read11 sections2 exercises
11 of 41

In this chapter

  • 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

and 6 more

about 8 min
11On this page
  1. Object literals
  2. Assignment copies the reference, not the object
  3. Shorthand and computed keys
  4. Arrays — indexed, ordered, still objects underneath
  5. Mutating methods — they change the array in place
  6. Non-mutating methods — they read, they don't touch
  7. map / filter / find / forEach / reduce
  8. some, every, and findIndex — checking, not collecting
  9. Destructuring
  10. Spread — the opposite of destructuring
  11. 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 endnew length
arr.pop()removes from the endthe removed element
arr.unshift(x)adds to the startnew length
arr.shift()removes from the startthe removed element
arr.splice(start, count, …items)removes count at start, inserts …items therearray of removed elements
arr.sort(cmp)sorts in placethe same array
arr.reverse()reverses in placethe 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 lengthyou're transforming every element
.filter(fn)a new array, shorter or equalyou're keeping some elements, dropping others
.find(fn)one element, or undefinedyou 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 upcollapsing 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

Itemprice * qtytotal beforetotal after
Pen20 * 3 = 600 (the initial value)60
Book150 * 1 = 15060210
Eraser5 * 0 = 0210210

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.

MethodReturnsStops early?
.some(fn)true the moment any element passesyes — as soon as one match is found
.every(fn)true only if every element passesyes — as soon as one fails
.findIndex(fn)the index of the first match, or -1yes
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 = 99 changes original.count too, and how spread or Object.assign avoids 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 -1 but [NaN].includes(NaN) is true.
  • Destructure a nested object with a default at every level, and say what happens if a middle level is missing.

02Practice this chapter

Opens in the editor: write it, run it, and check it against real tests.

  • 10 testsTotal up a shopping cartbeginner
  • 10 testsFormat an address, safelybeginner
All 35 for this level →
B11

Finished reading?

0 of 41 chapters read in this series

→Next stop

  1. B10Previous · B10Functions
  2. B11you are here
  3. Next · B12The this keywordNot where it was written — who called it.6 min read

01On this page

11 sections · 0%

  1. 01Object literals
  2. 02Assignment copies the reference, not the object
  3. 03Shorthand and computed keys
  4. 04Arrays — indexed, ordered, still objects underneath
  5. 05Mutating methods — they change the array in place
  6. 06Non-mutating methods — they read, they don't touch
  7. 07map / filter / find / forEach / reduce
  8. 08some, every, and findIndex — checking, not collecting
  9. 09Destructuring
  10. 10Spread — the opposite of destructuring
  11. 11What comes next

[] chapters · t top