Setup & mental model
The layer everybody skips — and it shows in interviews.
Two boxes, not one
Beginners think "JavaScript" is one thing. It's two things stacked, and almost every "weird JS behaviour" question traces back to which box actually did the work.
The 5 words that describe JS
- Single-threaded — one worker. It can only do one thing at a time. A slow loop freezes your whole page.
- Synchronous by default — line 1, then line 2, then line 3. Waiting doesn't happen unless you hand work to the runtime.
- Dynamically typed — a variable is a box, not a shape. Any value can go in it, and what's in it can change.
-
Weakly typed — if types don't match, JS converts
instead of complaining.
"5" * 2 → 10. - Interpreted + JIT compiled — it starts running instantly, then quietly re-compiles the parts you run a lot into fast machine code.
"Dynamic" and "weak" are often confused, but they're separate questions.
Dynamic asks when a type is checked — JS decides at runtime, not
ahead of time. Weak asks what happens on a mismatch — JS converts
instead of refusing. Python is dynamic but strongly typed:
1 + "1" is an error there. JS does both at once, which is why
it gets blamed for more than its share.
Watch the single thread work
This is the whole model in one demo: one call stack, a queue for promises, and a separate queue for everything the runtime hands back (timers, clicks, I/O). Step through it — the code on the left is really the code running underneath the panel on the right.
The punchline: C prints before B, even though
the timer's delay was 0ms. The microtask queue always drains
completely before the event loop looks at a single macrotask.
That's not a quirk of timers — it's the ordering rule every Promise,
every async/await, and every render sits on top of.
Where your code actually runs
Three places you'll type JavaScript, and they behave differently.
-
The console — a REPL. It prints the result of every expression,
which is why a stray
undefinedshows up afterconsole.log(...)— that'slog's own return value being echoed, not a bug. -
A
<script>tag — by default, HTML parsing stops while the script downloads and runs. That's why a plain<script>sitting in<head>delays everything below it from painting. -
Node —
node app.jsruns a file; barenodeopens a REPL. This is where JS gets a filesystem, a process, and no DOM at all.
index.html works fine — until you add
type="module", at which point every import silently fails.
Modules need a real origin. Run one locally:
npx serve or a "Live Server" extension.
var / let / const
| var 💀 | let | const ⭐ | |
|---|---|---|---|
| Lives inside | the whole function | the nearest { } | the nearest { } |
| Use before declaring | undefined |
💥 error | 💥 error |
| Reassign | yes | yes | no |
Becomes a window property? |
yes, at the top level | no | no |
Hoisting means: before running a line of code, JS scans the scope
and registers every name in it. var names get created
and pre-filled with undefined. let and
const names get created but left empty — touch one early and
you get an error. That empty gap has a dramatic name: the
Temporal Dead Zone.
console.log(a); // what happens?
console.log(b); // what happens?
var a = 1;
let b = 2;
Run it above — the first line quietly prints undefined
because var was pre-filled. The second line never gets the
chance: b is still in the Temporal Dead Zone, so the whole
script throws a real ReferenceError right there.
const by default →
let when you must reassign → var never.
Naming & comments
Identifiers may contain letters, digits, $ and
_. They can't start with a digit and can't be a reserved
word. They're case-sensitive — Name and name
are two different variables.
const userName = "ana"; // camelCase — variables, functions
class UserAccount {} // PascalCase — classes, constructors
const MAX_RETRIES = 3; // UPPER_SNAKE — true fixed constants
const _internal = {}; // leading _ — "private" by convention only
class A { #secret = 1; } // # — actually private (ES2022)
Name for intent, not type. Booleans read as questions
(isActive, hasPermission), functions start with
a verb (getUserById, handleSubmit).
// single line
/* multi
line — these do NOT nest */
/**
* JSDoc — tooling reads this for autocomplete and type hints.
* @param {string} name
*/
function greet(name) { return "Hi " + name; }
Write comments that explain why, not what — the code already says what.
'use strict'
Added in ES5 to fix old design mistakes without breaking the existing web. It doesn't add powers — it removes the silence. Mistakes that used to fail quietly now throw a real error.
'use strict';
x = 5; // no var, no let — what happens?
Run it — strict mode refuses to guess and throws
ReferenceError: x is not defined. Delete the first line
(or run it in a plain script) and the exact same assignment succeeds
silently, quietly creating a global variable named x. That
silent global is the bug strict mode exists to kill.
You rarely type 'use strict' yourself anymore, because
modules and classes are strict automatically.
Script vs module
A genuine fork in how a file is parsed and run, decided before a single line executes.
<script src="a.js"></script> // classic script
<script type="module" src="a.js"></script> // ES module
| Classic script | ES module | |
|---|---|---|
| Strict mode | opt-in | always on |
| Top-level scope | the global object | module-local |
Top-level this |
window |
undefined |
import / export |
not allowed | the whole point |
| Loading | blocking, unless defer |
deferred by default |
| Evaluated | once per tag | once per URL, then cached and shared |
<script> is loose, blocks the page, and dumps its
variables on window.<script type="module"> is strict, waits for the page,
keeps its variables to itself, and can import. Use modules.
One more thing worth knowing: an imported name is a live view into the module that exported it, not a copy taken once. If that module later changes the value, every importer sees the new one. That, plus imports being static and resolved before any code runs, is exactly what lets a bundler tree-shake unused exports away.
A harder ordering puzzle
Harder than the demo above: an async function, an await, and a promise chain all competing. Watch where the code after await actually goes.
Opens in the editor — write it, run it, and check it against real tests.