Skip to the notes
JSGroundwork
JSGroundwork handwritten · web dev
✎Playground→⌘Problems↻Review🔥Progress

Chapters

27 chapters
⌕
Beginner9›
B1Setup & mental modelB2Types & valuesB3Operators & flowB4Functions (first half)B5Objects & arrays (first half)B6DOM & eventsB7Basic asyncB8Errors & tools★Cheat page
Intermediate10›
I1Scope & functions, properlyI2Objects deeplyI3Prototypes & OOPI4Async, properlyI5Modules & toolingI6Regex, dates & APIsI7Error handlingI8Real-time connectionsI9Offline & storage★Cheat page
Advanced10›
A1Engine & memoryA2Advanced asyncA3MetaprogrammingA4Types & dataA5Patterns & architectureA6PerformanceA7SecurityA8EcosystemA9Testing★Cheat page
/ search[ ] chaptert top

JavaScript levels

1Beginner2Intermediate3Advanced

Ready to read

JSJavaScript⑂Git◎Interview prepΣDSA in JSSDSystem Design
More topics15›
</>HTML{ }CSS⚛ReactNNext.jsNeNest.jsTSTypeScriptNoNode.js🐳DockerDBSQL & Databases✓Testing🔒Web Security☁Cloud & DevOps◈GraphQL◆Redis☸Kubernetes
100%
B5

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 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
⚠ 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."

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
Practice this layer

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

Total up a shopping cart4 tests · beginnerFormat an address, safely4 tests · beginner
←previousFunctions (first half)↑ CovernextDOM & events→