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

Types & values

Eight types, one conversion table, and the equality check that never stops mattering.

Seven primitives + object

JavaScript has exactly eight types. Seven are primitives; everything else — arrays, functions, dates, regular expressions — is an object. Two things define a primitive: it's immutable, and it's copied by value.

Primitive Example typeof
string "hi" "string"
number 42, 3.14, NaN "number"
bigint 10n "bigint"
boolean true, false "boolean"
undefined undefined "undefined"
null null "object" — a bug, see below
symbol Symbol("id") "symbol"
— everything else — arrays, functions, dates, {}… "object" or "function"
The one rule that explains most bugs → a primitive is copied whole; an object is only ever handed around by reference. Two different variables can end up pointing at the same object.
Copy by value vs copy by reference
Primitive — the value itself is copied
let a10
let b = a10
two independent boxes
Object — only the reference is copied
let o1{ n: 10 }
let o2 = o1{ n: 10 }
both names point at the SAME object ↑

Change the copy and watch what happens to the original. This one difference explains most "why did my array change?" bugs — and it's exactly what the "Copy without sharing" exercise below is testing.

let s = "hello";
s[0] = "H";                     // silently ignored
console.log(s);                 // still "hello"
console.log(s.toUpperCase());   // "HELLO" — a NEW string
console.log(s);                 // still "hello"
s.custom = 1;                   // silently ignored too
console.log(s.custom);          // undefined

So why does "abc".length work at all, if strings can't hold properties? Autoboxing — the engine wraps the primitive in a throwaway String object, reads the property, then discards the wrapper. That's also why s.custom = 1 above does nothing: you wrote to something that was already gone.

typeof — and its two lies

typeof returns one of eight strings, and it's the one operator that can safely touch an undeclared name without throwing. It tells the truth about primitives — but two of its answers are traps.

Expression typeof Note
typeof null "object" a 1995 bug kept forever for compatibility — check value === null instead
typeof [] "object" arrays are objects — use Array.isArray(v)
typeof function(){} "function" the one honest special case — functions are still objects underneath
typeof undeclaredName "undefined" no ReferenceError — safe to use as a feature check
console.log(typeof null);        // the trap
console.log(typeof []);          // also a trap
console.log(Array.isArray([]));  // the fix
console.log(typeof (() => {}));
console.log(typeof Symbol("x"));
console.log(typeof 10n);

null vs undefined

Both mean "no value" — the difference is who wrote it. undefined is absence by default, handed to you by JavaScript. null is absence on purpose, assigned by a developer.

Default parameter fires? Survives JSON.stringify?
undefined yes no — the key is dropped
null no — null is a real value, not "missing" yes, kept
function greet(name = "friend") { return "Hi " + name; }
console.log(greet(undefined));   // default fires
console.log(greet(null));        // default does NOT fire

const v1 = 0, v2 = null;
console.log(v1 ?? "fallback");   // 0 — a real value survives
console.log(v1 || "fallback");   // "fallback" — || can't tell 0 from "missing"
console.log(v2 ?? "fallback");   // "fallback"

|| falls back on any falsy value, so a real 0 or "" gets silently replaced. ?? only falls back on null and undefined — reach for it whenever zero or empty string are legitimate answers.

The one accepted == exception x == null is true for both null and undefined, and false for everything else — a deliberate, readable way to check "is this missing".

Numbers

There is only one number type: a 64-bit IEEE 754 double. No int, no float distinction — 1 and 1.0 are the same value, which is also why floating point gets weird.

console.log(0.1 + 0.2);          // not 0.3
console.log(0.1 + 0.2 === 0.3);  // false
console.log(Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON); // the real check

Not a JavaScript bug — 0.1 has no exact binary form, the same way 1/3 has no exact decimal form. Identical result in Java, C and Python. Compare with a tolerance, and never store money in a float — keep integer cents.

Value How you get it How to detect it
NaN 0 / 0, Number("abc") Number.isNaN(v) — never ===
Infinity 1 / 0 Number.isFinite(v)
-0 literal -0, or -1 * 0 Object.is(v, -0) — -0 === 0 is true
past MAX_SAFE_INTEGER 253 and beyond Number.isSafeInteger(v) — use BigInt for exact large integers
console.log(isNaN("abc"));         // true — coerces first
console.log(Number.isNaN("abc"));  // false — no coercion, the honest answer
console.log(Number.isNaN(NaN));    // true

The global isNaN coerces its argument before checking, so it really answers "would this become NaN" — almost never the question you meant. Number.isNaN does no coercion. Use it.

parseInt, parseFloat, toFixed

Three different parsers, not synonyms. Number() is strict — the whole string or NaN. parseInt and parseFloat are lenient: they read a prefix and stop at the first character they don't understand.

console.log(Number("42px"));        // NaN — not the whole string
console.log(parseInt("42px"));      // 42 — reads a prefix
console.log(parseFloat("3.14em"));  // 3.14
console.log(parseInt("0x1F"));      // 31 — reads hex on its own
console.log(parseInt("08", 10));    // always pass the radix
⚠ The map(parseInt) trap ["1","2","3"].map(parseInt) gives [1, NaN, NaN]. map calls its function with (value, index, array), so parseInt receives the index as its radix — radix 1 is invalid, and radix 2 can't read "3". Use .map(Number) or .map(s => parseInt(s, 10)) instead.
console.log((1.005).toFixed(2));    // "1.00" — not "1.01"
console.log(typeof (1).toFixed(2)); // "string"

toFixed returns a string, and it rounds the double that actually exists in memory — 1.005 is really 1.00499999…, so it rounds down. For display use Intl.NumberFormat; for money, round integer cents yourself.

Strings

Immutable, and stored as UTF-16 code units — which is where .length stops meaning "number of characters".

const name = "Ana", n = 3;
console.log(`Hi ${name}, you have ${n} item${n === 1 ? "" : "s"}`);

Any expression fits inside ${…} — ternaries, function calls, even math. Multiline needs no \n — a real line break inside the backticks is enough.

const s = "café 👍";
console.log(s.length);        // 6 — code UNITS, not characters
console.log([...s].length);   // 5 — code points

The 👍 is a surrogate pair — one character stored as two code units. For plain ASCII this never shows up; the moment emoji or accented characters appear, .length quietly lies.

slice(a, b) substring(a, b)
negative index counts from the end clamped to 0
start > end returns "" silently swaps the two

Prefer slice — one consistent set of rules. substr is deprecated.

const s = "  Hello, World  ";
console.log(s.trim());
console.log(s.trim().toUpperCase());
console.log(s.includes("World"));
console.log(s.trim().split(", "));
console.log(s.trim().replaceAll("o", "0"));
console.log(s.slice(2, 7));

Every one of these returns a new string. There is no in-place string operation in JavaScript.

Sequence Meaning
\n newline
\t tab
\\ one literal backslash
\" escaped quote — or just switch quote style
\u00e9 → é — 4 hex digits, one code unit

Truthy / falsy — the eight

The falsy list is short and closed. Memorise these eight — everything else in the language is truthy.

false 0 -0 0n "" null undefined NaN

Those eight are falsy. Everything else — including these commonly mistaken ones — is truthy:

"0" "false" [] {} [0] " " -1 function(){}
console.log(!![]);         // true — [] is not in the falsy list
console.log([] == false);  // true — == turns BOTH sides into numbers: false→0, []→""→0
console.log([] === false); // false — different types, no coercion

Two unrelated mechanisms landing on opposite-looking answers for the same value. if consults the falsy list; == runs a coercion algorithm.

Explicit conversion

Three functions, always called without new. Converting on purpose is how you stop the language converting behind your back.

value String(v) Number(v) Boolean(v)
"" "" 0 false
"12" "12" 12 true
"12px" "12px" NaN true
null "null" 0 false
undefined "undefined" NaN false
[] "" 0 true
[5] "5" 5 true
[1, 2] "1,2" NaN true

Object-to-primitive conversion runs Symbol.toPrimitive, then valueOf, then toString — the entire explanation for why [] + [] is "" and [] + {} is "[object Object]".

console.log(String(null), Number(null), Boolean(null));
console.log(+"3.14");   // Number("3.14")
console.log(5 + "");    // String(5) — the lazy way
console.log(!!"");      // Boolean("")
console.log([] + []);
console.log([] + {});

== vs ===

=== is one rule: same type and same value. == is an algorithm — null == undefined is a special case, and a mismatched type on either side gets converted before comparing.

NaN vs NaN 0 vs -0 coerces types?
== false true yes
=== false true no
Object.is true false no
SameValueZero true true no

SameValueZero is what Array.prototype.includes, Map keys and Set members actually use — which is why [NaN].includes(NaN) is true while [NaN].indexOf(NaN) is -1 (indexOf uses ===).

console.log(0 == "0", 0 == "", "0" == "");
console.log(false == "false");    // false — "false" isn't the number 0
console.log(null == undefined);   // true — the one special case
console.log(null === undefined);  // false — different types
console.log([NaN].includes(NaN), [NaN].indexOf(NaN));
The rule Always ===. The one accepted exception is x == null, which tests null-or-undefined in a single check.

See what the callback captured

The same loop twice, one keyword apart. Watch how many bindings each version creates — that is the whole difference.

var vs let in a loop — what the callback actually captured
Bindings in scope
Queued callbacks

See the lookup walk

Watch the lookup walk. JavaScript does not copy methods onto objects; it walks a chain until it finds one, and stops at the first hit.

Prototype chain — how a method is actually found
Lookup walk
What each level owns

Practice this layer

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

Fix typeof's two lies4 tests · beginnerKeep only the truthy values4 tests · beginnerCopy without sharing4 tests · beginner
←previousSetup & mental model↑ CovernextOperators & flow→