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%
B3

Operators & flow

Every operator here has an interview question hiding behind it.

Arithmetic — and the one that lies

Six operators: + - * / % **. Five of them only ever do math. + is the odd one out — if either side is a string, it stops adding and starts concatenating.

Expression Result Why
5 - "2" 3 - only means subtract — the string is coerced to a number
5 + "2" "52" + sees a string and switches to concatenation
"5" + 2 + 1 "521" left-to-right — once it's a string, it stays a string
5 + 2 + "1" "71" 5 + 2 runs first (both numbers), then it meets the string
10 % 3 1 remainder, not "percent" — the sign follows the left operand
2 ** 3 ** 2 512 ** is right-associative: 2 ** (3 ** 2), not (2 ** 3) ** 2
console.log([] + []);        // what happens?
console.log([] + {});        // what happens?

Every array/object first converts to a primitive before + ever runs. [].toString() is "", so [] + [] is "" + "" → "". {}.toString() is "[object Object]", so [] + {} → "[object Object]".

Swap the order — {} + [] — and if that {} is the very first token of a statement rather than sitting inside an expression, the parser reads it as an empty block, not an object literal. What's left is a new statement, +[] — unary plus on an empty array — which is 0. Wrap it in anything (console.log(...), an assignment, parens) and {} is back in expression position, parsed as an object literal again, giving the same "[object Object]" as before:

// {} is the first token of a statement here — parsed as a block
{} + [];              // two statements: an empty block, then +[] (discarded)

// {} is inside an expression here — parsed as an object literal
console.log({} + []); // "[object Object]"

This is exactly why node -p "{} + []" prints 0 — the REPL evaluates that line as a standalone statement, so {} lands in statement position. The moment it's an argument to something else, it can't be a statement anymore.

Say it like this → "+ triggers ToPrimitive on both sides before it decides whether it's adding or concatenating. Every other arithmetic operator forces ToNumber and never looks back."

Unary, increment, typeof

-x and +x coerce to a number without any other math — +x is a fast, common way to turn a string into a number. ++ and -- exist in two flavors that differ only in when they hand back a value.

let x = 5;
console.log(x++);   // 5 — returns the OLD value, then increments
console.log(x);     // 6
console.log(++x);   // 7 — increments FIRST, then returns
⚠ Avoid mixing them into an expression let y = x++ + ++x; works, but nobody can read it at a glance — including you, in six months. Put the increment on its own line.

Assignment — plain and compound

Operator Means
x += yx = x + y
x -= y, *=, /=, %=, **=same pattern for each
x &&= yif (x) x = y — assign only when x is truthy
x ||= yif (!x) x = y — assign only when x is falsy
x ??= yif (x == null) x = y — assign only when x is null/undefined

The logical-assignment trio (ES2021) are the standard way to fill in a missing config value without an if block:

const config = {};
config.retries ??= 3;   // only sets it if it's null/undefined
console.log(config.retries); // 3

Comparison — the interview's favorite trap

== compares after converting both sides to a common type. === refuses to convert anything — different types is an automatic false. That's the whole rule; the coercion table is just that rule played out across every type pair.

null == undefined 0 == false "" == false "0" == 0 [] == false [1] == 1

All true under == — every one of them false under ===.

NaN == NaN null == 0 "" == "0" "" == 0 == "0"

All false — even the last one, which looks like it should chain. "" == 0 is true, and 0 == "0" is true, but == isn't transitive, so "" == "0" is false on its own. That non-transitivity is the strongest argument against == that exists.

Rule Use === and !== by default. The one accepted exception is x == null, which deliberately catches both null and undefined in one check.

Logical operators — short-circuit, not just booleans

&& and || don't return true/false — they return one of their actual operands. && returns the first falsy value it finds, or the last value if none are falsy. || returns the first truthy value, or the last value if none are truthy. Once the answer is decided, the other side is never even evaluated.

function log(x) { console.log("checked", x); return x; }

log(false) && log("never runs");   // stops at the first falsy value
log(true)  || log("never runs");   // stops at the first truthy value

That short-circuit is what makes the classic default-value idiom work — and also what makes it dangerous:

function greet(name) {
  const who = name || "friend";   // falls back on ANY falsy name
  return "Hi " + who;
}
greet("");   // "Hi friend" — is that what you wanted?

?? — the fix for || 's blind spot

?? only falls back on null or undefined — 0, "", and false all survive it untouched. That's the entire reason it exists.

Value on the left value || "fallback" value ?? "fallback"
0"fallback"0
"""fallback"""
false"fallback"false
null"fallback""fallback"
undefined"fallback""fallback"
⚠ ?? can't mix with && or || directly a || b ?? c is a SyntaxError — JS refuses to guess which one you meant to run first. Parenthesize: (a || b) ?? c.

?. — optional chaining

Before ?., reaching into a maybe-missing property meant a wall of && guards. Now the chain just stops — and returns undefined — the moment it hits null/undefined.

const user = { profile: null };

user.profile.bio;     // 💥 throws — profile is null
user.profile?.bio;    // undefined — stops safely
user.greet?.();       // calls greet() only if it exists
user.tags?.[0];       // safe computed access too

It short-circuits the whole rest of the chain, not just the next step: a?.b.c.d — if a is null, the entire expression is undefined; .c.d never even gets attempted.

⚠ Not an assignment tool user.profile?.bio = "hi" is a SyntaxError — ?. can only be used to read, never as the target of an assignment.

The ternary

const label = age >= 18 ? "adult" : "minor";

A full if/else squeezed into one expression — which is exactly its advantage: it produces a value, so it can sit inside a return, a template, or a prop. Nesting one is fine; nesting two gets unreadable fast — reach for if/else once you're past one level.

if / else / switch

if only cares about truthy vs falsy — the same rules from the last chapter apply here with no exceptions. switch compares with === (no coercion) and, unlike if/else, falls through to the next case unless you break.

function trafficAction(color) {
  switch (color) {
    case "red":
      return "stop";
    case "yellow":
    case "amber":         // two labels, one body — the fall-through you actually want
      return "slow down";
    case "green":
      return "go";
    default:
      return "unknown";
  }
}
⚠ The classic switch bug Forgetting break anywhere else and it silently keeps running into the next case's code — no error, just wrong behavior. If a case doesn't return, it needs an explicit break.

Loops

Loop When to use it
for (let i = 0; i < n; i++)you need the index, or a custom step
while (cond)you don't know the count in advance
do { } while (cond)the body must run at least once, condition checked after
for (const x of iterable)you just want the values — arrays, strings, Maps, Sets
for (const k in obj)you want an object's enumerable keys — plain objects only

for...of vs for...in — don't mix these up

This pair gets confused constantly, and the confusion has a real cost: for...in on an array walks its keys as strings, includes any inherited enumerable properties, and makes no promise about numeric ordering. for...of walks values, in order, and works on anything iterable.

const arr = ["a", "b", "c"];

for (const v of arr) console.log(v);     // "a" "b" "c" — values, in order
for (const k in arr)  console.log(k);    // "0" "1" "2" — STRING indices
Rule for...of for arrays and anything iterable. for...in only for plain objects — and even there, Object.keys/values/entries is usually clearer.

break, continue, and labels

outer: for (let i = 0; i < 3; i++) {
  for (let j = 0; j < 3; j++) {
    if (j === 1) continue outer;   // skip to the NEXT i, not just this j
    if (i === 2) break outer;      // exit BOTH loops
    console.log(i, j);
  }
}

Labels are rare in real code — one un-labeled break or continue only ever touches its nearest enclosing loop. But recognize the syntax; it shows up in coding-round trick questions more than it does in production code.

Precedence & associativity, compressed

Highest → lowest Associativity
() grouping, . ?. [] member accessleft → right
unary: ! ~ + - ++ -- typeofright → left
**right → left
* / %left → right
+ -left → right
< <= > >=left → right
== != === !==left → right
&&left → right
||, ??left → right
?: ternaryright → left
= += -= &&= ||= ??= …right → left
console.log(1 < 2 < 3);    // what happens?
console.log(3 > 2 > 1);    // what happens?

Both read left to right because < and > have no special chaining rule in JS — it's two separate comparisons. 1 < 2 < 3 is (1 < 2) < 3 → true < 3 → 1 < 3 → true. 3 > 2 > 1 is (3 > 2) > 1 → true > 1 → 1 > 1 → false. Same shape, opposite answer — that's the trap.

Say it like this → "Chained comparisons aren't a thing in JS — every relational and equality operator only ever sees two operands, and the result of one comparison becomes a boolean operand in the next."
Practice this layer

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

Defaults that respect 05 tests · beginner
←previousTypes & values↑ CovernextFunctions (first half)→