Skip to the chapter
←JavaScriptGGroundworkJSJavaScript
JavaScript/Beginner/B9
9 / 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

JSB9 · Part 1 · Beginner

Operators & flow

Every operator here has an interview question hiding behind it.

11 min read15 sections1 exercise
9 of 41

In this chapter

  • Arithmetic — and the one that lies
  • Unary, increment, typeof
  • Assignment — plain and compound
  • Comparison — the interview's favorite trap
  • in and delete — checking and removing a property

and 10 more

about 11 min
15On this page
  1. Arithmetic — and the one that lies
  2. Unary, increment, typeof
  3. Assignment — plain and compound
  4. Comparison — the interview's favorite trap
  5. in and delete — checking and removing a property
  6. Bitwise operators — rare, but they show up
  7. Logical operators — short-circuit, not just booleans
  8. ?? — the fix for || 's blind spot
  9. ?. — optional chaining
  10. The ternary
  11. if / else / switch
  12. Loops
  13. for...of vs for...in — don't mix these up
  14. break, continue, and labels
  15. Precedence & associativity, compressed

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.

Dry run: evaluating "" == 0 == "0" left to right

StepSub-expressionCoercion appliedResult
1"" == 0string vs number → ToNumber("") is 00 == 0 → true
2true == "0"boolean vs string → both convert to numbers: ToNumber(true) is 1, ToNumber("0") is 01 == 0 → false
final"" == 0 == "0"—false

== is left-associative like every other binary operator here — it never chains three operands the way a math teacher's a < b < c would. The result of step 1 becomes a plain boolean, and that boolean gets coerced all over again for step 2 — which is exactly how a chain of individually-true comparisons ends up false.

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

in and delete — checking and removing a property

const user = { name: "Ana", age: 29 };

"name" in user;         // true — checks the KEY, not the value
"toString" in user;     // true too — in walks the prototype chain
user.hasOwnProperty("toString");   // false — this one doesn't

delete user.age;
console.log(user);      // { name: "Ana" } — age is gone entirely, not just undefined
⚠ in isn't the same check as a truthy value "key" in obj is true even when obj.key is 0, "", or undefined — as long as the key was actually set. if (obj.key) and if ("key" in obj) answer two different questions: "is the value truthy" versus "does this key exist at all."

Bitwise operators — rare, but they show up

OperatorDoes
&AND, bit by bit
|OR, bit by bit
^XOR, bit by bit
~NOT — flips every bit
<<, >>shift left/right, sign-preserving
>>>shift right, zero-fill — the sign bit doesn't survive
6 & 3;    // 2 — 110 & 011 = 010
6 | 3;    // 7 — 110 | 011 = 111
5 << 1;   // 10 — shift left one bit = multiply by 2
~5;       // -6 — NOT is always -(x + 1)

Every bitwise operator first converts its operands to a 32-bit signed integer, does the math there, then converts back — which is also why they quietly break past Number.MAX_SAFE_INTEGER. Real code almost never touches these directly; the two survivors in everyday use are ~arr.indexOf(x) as an old-school truthy "found it" check (rare now that .includes() exists) and flags | newFlag for building a bitmask.

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
<< >> >>> bit shiftleft → right
< <= > >= inleft → right
== != === !==left → right
&, ^, | bitwiseleft → 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."
Interview answer, one sentence

"Use === to avoid coercion, ?? rather than || when 0 or an empty string is a valid value, and optional chaining to stop at null or undefined — and remember && and || return one of their operands, not a boolean."

Before you move on
  • Explain why 5 + "2" and 5 - "2" give different-typed results, in terms of what + does that the other operators don't.
  • Walk "" == 0 == "0" through its two comparisons by hand, and explain why == not being transitive is the result.
  • Say what value || fallback gets wrong that value ?? fallback fixes, with 0 or "" as the example.
  • Explain why && and || return one of their operands instead of a boolean, and predict what log(false) && log("x") actually evaluates to.
  • Predict what a switch does when a case is missing its break, and why switch uses === semantics while if uses truthy/falsy.

01Practice this chapter

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

  • 8 testsDefaults that respect 0beginner
All 35 for this level →
B9

Finished reading?

0 of 41 chapters read in this series

→Next stop

  1. B8Previous · B8Strings, numbers & Math
  2. B9you are here
  3. Next · B10FunctionsA function is a value first, a block of code second.7 min read

01On this page

15 sections · 0%

  1. 01Arithmetic — and the one that lies
  2. 02Unary, increment, typeof
  3. 03Assignment — plain and compound
  4. 04Comparison — the interview's favorite trap
  5. 05in and delete — checking and removing a property
  6. 06Bitwise operators — rare, but they show up
  7. 07Logical operators — short-circuit, not just booleans
  8. 08?? — the fix for || 's blind spot
  9. 09?. — optional chaining
  10. 10The ternary
  11. 11if / else / switch
  12. 12Loops
  13. 13for...of vs for...in — don't mix these up
  14. 14break, continue, and labels
  15. 15Precedence & associativity, compressed

[] chapters · t top