Mastering JavaScript Core: From Variables and Symbols to Control Flow
Unlock the bedrock of modern web development. This guide transcends basic syntax, offering a technical dissection of JavaScript execution environments, the behavior of primitive vs. reference types, the often-misunderstood Temporal Dead Zone, and the utility of the Symbol type. Whether you are scripting for the browser or architecting server-side logic with Node.js, these are the essential patterns for writing robust code.
JavaScript Basics
JavaScript Execution Environments (Browser, Node.js, Deno, Bun)
JavaScript runs in multiple environments: Browsers provide the DOM and Web APIs for client-side scripting; Node.js is a server-side runtime built on V8 with npm ecosystem; Deno is a secure TypeScript-first runtime by Node's creator with built-in tooling; Bun is the newest, fastest runtime focused on speed and all-in-one tooling.
┌─────────────────────────────────────────────────────────────┐
│ JavaScript Code │
└─────────────────────────────────────────────────────────────┘
│
┌─────────────┬───────┴───────┬─────────────┐
▼ ▼ ▼ ▼
┌─────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Browser │ │ Node.js │ │ Deno │ │ Bun │
│ (V8) │ │ (V8) │ │ (V8) │ │ (JSC) │
└─────────┘ └──────────┘ └──────────┘ └──────────┘
Script Tag and Script Loading
The <script> tag embeds JavaScript in HTML with attributes like src for external files, defer to load after HTML parsing, and async to load in parallel without blocking—defer maintains execution order while async executes immediately when ready.
<!-- Inline script --> <script> console.log("Hello!"); </script> <!-- External with loading strategies --> <script src="app.js"></script> <!-- Blocks parsing --> <script src="app.js" defer></script> <!-- After DOM ready, in order --> <script src="app.js" async></script> <!-- ASAP, no order guarantee --> <!-- Module (always deferred) --> <script type="module" src="app.mjs"></script>
Console and Developer Tools
Browser DevTools (F12) and Node.js provide console methods for debugging—console.log() for general output, console.error() for errors, console.table() for tabular data, console.time()/timeEnd() for performance measurement, and console.trace() for stack traces.
console.log("Basic output"); console.error("Error message"); console.warn("Warning message"); console.table([{ name: "Alice", age: 30 }, { name: "Bob", age: 25 }]); console.time("loop"); for (let i = 0; i < 1000000; i++) {} console.timeEnd("loop"); // loop: 2.345ms console.group("User Details"); console.log("Name: John"); console.log("Age: 30"); console.groupEnd();
Variables (var, let, const)
var is function-scoped and hoisted (legacy, avoid it); let is block-scoped and reassignable; const is block-scoped and cannot be reassigned (but object properties can be mutated)—prefer const by default, use let when reassignment is needed.
// var - function scoped, hoisted var x = 1; var x = 2; // OK, can redeclare // let - block scoped let y = 1; y = 2; // OK, can reassign // let y = 3; // Error: already declared // const - block scoped, no reassignment const z = 1; // z = 2; // Error: Assignment to constant const obj = { a: 1 }; obj.a = 2; // OK! Object mutation allowed
┌────────────────────────────────────────┐
│ var let const │
├────────────────────────────────────────┤
│ Scope │ func │ block │ block │
│ Reassign │ ✓ │ ✓ │ ✗ │
│ Redeclare │ ✓ │ ✗ │ ✗ │
│ Hoisted │ ✓ │ ✓* │ ✓* │
└────────────────────────────────────────┘
* Hoisted but in Temporal Dead Zone
Hoisting
Hoisting moves variable and function declarations to the top of their scope during compilation—var declarations are initialized as undefined, function declarations are fully hoisted with their body, but let/const are hoisted without initialization (causing TDZ errors if accessed early).
console.log(a); // undefined (var hoisted, initialized as undefined) var a = 5; // console.log(b); // ReferenceError: Cannot access 'b' before initialization let b = 5; greet(); // "Hello!" - function fully hoisted function greet() { console.log("Hello!"); } // sayHi(); // TypeError: sayHi is not a function var sayHi = function() { console.log("Hi!"); };
Temporal Dead Zone
The Temporal Dead Zone (TDZ) is the period between entering a scope and the variable's declaration where accessing let or const variables throws a ReferenceError—this prevents using variables before they're properly initialized, catching bugs that var would silently allow.
{ // TDZ starts for 'x' ──────────┐ // │ // console.log(x); // Error! │ TDZ // │ let x = 10; // TDZ ends ────────┘ console.log(x); // 10 } // Even typeof isn't safe in TDZ { // typeof y; // ReferenceError let y = 5; typeof y; // "number" }
┌──────────────────────────────────────┐
│ { │
│ ┌─── TDZ for 'value' starts ───┐ │
│ │ │ │
│ │ console.log(value); // 💥 │ │
│ │ │ │
│ └─── let value = 42; ──────────┘ │
│ │
│ console.log(value); // 42 ✓ │
│ } │
└──────────────────────────────────────┘
Data Types (Primitive vs Reference)
Primitives (string, number, boolean, null, undefined, symbol, bigint) are immutable and stored directly in the stack with copy-by-value behavior; Reference types (objects, arrays, functions) are stored in the heap with variables holding memory addresses, so copying creates shared references to the same data.
// Primitive - copy by value let a = 10; let b = a; b = 20; console.log(a); // 10 (unchanged) // Reference - copy by reference let obj1 = { name: "Alice" }; let obj2 = obj1; obj2.name = "Bob"; console.log(obj1.name); // "Bob" (changed!)
PRIMITIVES (Stack) REFERENCE (Heap)
┌─────────┬───────┐ ┌─────────┬─────────┐
│ a │ 10 │ │ obj1 │ 0x001 ─┼──┐
├─────────┼───────┤ ├─────────┼─────────┤ │
│ b │ 20 │ │ obj2 │ 0x001 ─┼──┤
└─────────┴───────┘ └─────────┴─────────┘ │
▼
┌─────────────────────────┐
│ { name: "Bob" } 0x001 │
└─────────────────────────┘
Number, String, Boolean, Null, Undefined, Symbol, BigInt
JavaScript has 7 primitive types: Number (64-bit float for all numeric values), String (immutable UTF-16 text), Boolean (true/false), Null (intentional absence), Undefined (uninitialized), Symbol (unique identifiers), and BigInt (arbitrary precision integers for values beyond Number.MAX_SAFE_INTEGER).
// Number - includes integers, floats, Infinity, NaN let num = 42; let float = 3.14; let inf = Infinity; let notNum = NaN; // String - immutable text let str = "Hello"; let template = `Value: ${num}`; // Boolean let bool = true; // Null vs Undefined let empty = null; // Intentional "no value" let notDefined; // undefined by default // Symbol - unique identifier let sym1 = Symbol("id"); let sym2 = Symbol("id"); console.log(sym1 === sym2); // false // BigInt - large integers let big = 9007199254740993n; let big2 = BigInt("9007199254740993");
typeof Operator
The typeof operator returns a string indicating the type of a value—it correctly identifies most primitives but has quirks: typeof null returns "object" (historical bug), typeof function returns "function" (though functions are objects), and arrays return "object".
typeof 42 // "number" typeof "hello" // "string" typeof true // "boolean" typeof undefined // "undefined" typeof Symbol() // "symbol" typeof 42n // "bigint" // Quirks typeof null // "object" (historical bug!) typeof [] // "object" typeof {} // "object" typeof function(){} // "function" // Better checks Array.isArray([]); // true obj === null; // check for null obj instanceof Date; // check for Date Object.prototype.toString.call([]); // "[object Array]"
Type Coercion and Conversion
Coercion is implicit automatic type conversion by JavaScript operators (e.g., "5" + 3 = "53"), while conversion is explicit using functions like Number(), String(), or Boolean()—understanding coercion rules prevents bugs, especially with + (prefers strings) vs -, *, / (prefer numbers).
// Implicit Coercion "5" + 3 // "53" (number → string) "5" - 3 // 2 (string → number) "5" * "2" // 10 (both → numbers) true + true // 2 (boolean → number) "5" > 3 // true (string → number) // Explicit Conversion Number("42") // 42 Number("hello") // NaN Number(true) // 1 parseInt("42px") // 42 parseFloat("3.14") // 3.14 String(42) // "42" String(null) // "null" Boolean(0) // false Boolean("hello") // true !!value // Convert to boolean (double negation)
Truthy and Falsy Values
Falsy values are the 8 values that coerce to false in boolean contexts: false, 0, -0, 0n, "", null, undefined, and NaN—everything else is truthy, including empty objects {}, empty arrays [], and the string "false".
// Falsy values (8 total) if (false) {} // falsy if (0) {} // falsy if (-0) {} // falsy if (0n) {} // falsy (BigInt zero) if ("") {} // falsy (empty string) if (null) {} // falsy if (undefined) {} // falsy if (NaN) {} // falsy // Truthy (everything else!) if ("0") {} // truthy! (non-empty string) if ("false") {} // truthy! if ([]) {} // truthy! (empty array) if ({}) {} // truthy! (empty object) if (function(){}) {} // truthy! // Common pattern const name = inputName || "Anonymous";
┌────────────────────────────────────┐
│ FALSY VALUES │
├────────────────────────────────────┤
│ false │ 0 │ -0 │ 0n │
│ "" │ null │ undefined │ NaN │
└────────────────────────────────────┘
Everything else is TRUTHY!
Operators (Arithmetic, Assignment, Comparison, Logical, Bitwise)
JavaScript provides arithmetic (+, -, *, /, %, **), assignment (=, +=, -=, etc.), comparison (>, <, >=, <=, ==, ===), logical (&&, ||, !), and bitwise (&, |, ^, ~, <<, >>, >>>) operators for manipulating values and controlling program flow.
// Arithmetic 5 + 3; // 8 10 % 3; // 1 (modulo) 2 ** 3; // 8 (exponentiation) // Assignment let x = 5; x += 3; // x = 8 x **= 2; // x = 64 // Comparison 5 > 3; // true "a" < "b"; // true (lexicographic) // Logical (short-circuit) true && "yes"; // "yes" (returns last truthy) false || "default"; // "default" (returns first truthy) !true; // false // Bitwise 5 & 3; // 1 (AND: 101 & 011 = 001) 5 | 3; // 7 (OR: 101 | 011 = 111) 5 ^ 3; // 6 (XOR: 101 ^ 011 = 110) ~5; // -6 (NOT) 5 << 1; // 10 (left shift) 5 >> 1; // 2 (right shift)
Strict Equality (===) vs Loose Equality (==)
Strict equality (===) compares value and type without conversion (use this by default); loose equality (==) performs type coercion before comparison, leading to unexpected results like "0" == false being true—always prefer === unless you explicitly need coercion.
// Strict equality - no type coercion 5 === 5 // true 5 === "5" // false (different types) null === undefined // false // Loose equality - with type coercion 5 == "5" // true (string coerced to number) 0 == false // true "" == false // true null == undefined // true [] == false // true (wat!) [] == ![] // true (wat!!) // The infamous table "0" == false // true 0 == "0" // true false == "0" // true null == false // FALSE! (null only == undefined)
LOOSE EQUALITY (==) QUIRKS
┌─────────────────────────────────────┐
│ "" == "0" │ false │
│ 0 == "" │ true ← Surprise! │
│ 0 == "0" │ true │
│ false == "0" │ true ← Surprise! │
│ false == null │ false │
│ null == undefined │ true │
└─────────────────────────────────────┘
Rule: Always use === for predictability
Nullish Coalescing Operator (??)
The nullish coalescing operator (??) returns the right-hand value only when the left is null or undefined, unlike || which triggers on any falsy value—use ?? when you want to preserve legitimate falsy values like 0, "", or false.
// Difference from || const count = 0; count || 10; // 10 (0 is falsy, so fallback) count ?? 10; // 0 (0 is not nullish, so keep it) const text = ""; text || "default"; // "default" (empty string is falsy) text ?? "default"; // "" (empty string is not nullish) // Practical examples const config = { timeout: 0, retries: null }; config.timeout ?? 5000; // 0 (intended value preserved) config.retries ?? 3; // 3 (null → use default) config.missing ?? 100; // 100 (undefined → use default) // Cannot mix with && or || without parentheses // a ?? b || c; // SyntaxError (a ?? b) || c; // OK
Optional Chaining (?.)
Optional chaining (?.) safely accesses nested object properties, returning undefined instead of throwing an error if any part of the chain is null or undefined—works with property access (?.), array indices (?.[]), and method calls (?.()), eliminating verbose null-checking code.
const user = { name: "Alice", address: { city: "Seattle" }, greet() { return "Hello!"; } }; // Without optional chaining const city = user && user.address && user.address.city; // With optional chaining user?.address?.city; // "Seattle" user?.phone?.number; // undefined (no error!) // With arrays const users = [{ name: "Bob" }]; users?.[0]?.name; // "Bob" users?.[5]?.name; // undefined // With methods user?.greet?.(); // "Hello!" user?.missing?.(); // undefined // Combining with nullish coalescing const phone = user?.phone?.number ?? "No phone";
WITHOUT OPTIONAL CHAINING: WITH OPTIONAL CHAINING:
┌─────────────────────────┐ ┌───────────────────────┐
│ if (user && │ │ │
│ user.address && │ → │ user?.address?.city │
│ user.address.city) │ │ │
└─────────────────────────┘ └───────────────────────┘
Comments (Single-line, Multi-line, JSDoc)
Single-line comments use //, multi-line use /* */, and JSDoc comments (/** */) provide structured documentation that IDEs and documentation generators can parse for type hints, parameter descriptions, and API documentation.
// Single-line comment /* * Multi-line comment * Can span multiple lines */ /** * JSDoc - Generates documentation and provides IDE hints * @param {string} name - The user's name * @param {number} [age=0] - Optional age with default * @returns {string} A greeting message * @throws {Error} If name is empty * @example * greet("Alice", 30); // "Hello, Alice! Age: 30" */ function greet(name, age = 0) { if (!name) throw new Error("Name required"); return `Hello, ${name}! Age: ${age}`; } /** * @typedef {Object} User * @property {string} name * @property {number} age */ /** @type {User} */ const user = { name: "Bob", age: 25 };
Semicolons and ASI (Automatic Semicolon Insertion)
JavaScript automatically inserts semicolons at line endings where needed (ASI), but this can cause bugs—notably with return, throw, break, continue followed by a newline, or lines starting with (, [, /, +, or -; most style guides recommend either always or never using semicolons, with consistent team agreement.
// ASI works fine here const a = 1 const b = 2 // ASI DANGER - return with newline function getData() { return // ASI inserts semicolon here! { data: 42 } } getData(); // undefined (not { data: 42 }) // ASI DANGER - line starting with ( const x = 1 (function() { console.log("IIFE") })() // Interpreted as: const x = 1(function...)() → Error! // Safe patterns const y = 1; ;(function() { console.log("IIFE") })() // Defensive semicolon // Lines that NEED semicolons (or defensive ;) // Lines starting with: ( [ / + - `
ASI DANGER ZONES:
┌────────────────────────────────────────┐
│ return ← newline breaks return │
│ throw ← newline breaks throw │
│ (...) ← start with ( is dangerous │
│ [...] ← start with [ is dangerous │
│ `template` ← start with ` is dangerous │
└────────────────────────────────────────┘
Strict Mode ("use strict")
Strict mode is a restricted variant of JavaScript that catches common errors, prevents use of reserved keywords, disables problematic features (like with), and makes eval/arguments safer—enable it with "use strict"; at file/function top; ES6 modules and classes are strict by default.
"use strict"; // Must be first statement // What strict mode prevents: x = 10; // Error: x is not defined (no implicit globals) delete Object.prototype; // Error: can't delete var let = 5; // Error: reserved word function f(a, a) {} // Error: duplicate parameter // Silent errors become throws const obj = {}; Object.defineProperty(obj, 'x', { writable: false }); obj.x = 10; // Error in strict, silent fail otherwise // 'this' in functions function showThis() { console.log(this); // undefined (not window/global) } // Automatically strict: class MyClass { } // Classes are always strict import { x } from './m'; // Modules are always strict
Code Style and Formatting
Consistent code style improves readability and team collaboration—use tools like ESLint for linting (catching errors and enforcing rules) and Prettier for formatting (auto-fixing style); popular style guides include Airbnb, Google, and StandardJS, and most teams use a combination with IDE integration for auto-fix on save.
// .eslintrc.js module.exports = { extends: ['eslint:recommended', 'prettier'], rules: { 'no-unused-vars': 'error', 'prefer-const': 'error', 'no-console': 'warn' } }; // .prettierrc { "semi": true, "singleQuote": true, "tabWidth": 2, "trailingComma": "es5", "printWidth": 80 } // Good practices const userName = 'Alice'; // camelCase variables const MAX_RETRIES = 3; // SCREAMING_SNAKE for constants class UserService { } // PascalCase for classes function getUserById(id) { } // camelCase for functions const isActive = true; // 'is/has/can' prefix for booleans
COMMON STYLE CHOICES:
┌────────────────────────────────────────┐
│ Naming │ camelCase, PascalCase │
│ Indent │ 2 spaces (most common) │
│ Quotes │ Single or Double (pick one)│
│ Semicolons│ With or Without (pick one) │
│ Line length│ 80-120 characters │
└────────────────────────────────────────┘
Tools: ESLint + Prettier + EditorConfig
This covers all the JavaScript basics topics. Each concept builds on the others—understanding variables and types leads to understanding operators, which leads to understanding equality and coercion. The key takeaway: prefer const over let over var, use === over ==, leverage ?. and ?? for safer code, and always use tooling (ESLint/Prettier) to maintain consistency.
Symbols
Symbol Creation
Symbol() creates a unique, immutable primitive value; each call produces a distinct symbol even with the same description.
const s1 = Symbol(); const s2 = Symbol(); console.log(s1 === s2); // false
Symbol Description
The optional description string aids debugging but doesn't affect uniqueness; access via symbol.description property.
const sym = Symbol("userId"); console.log(sym.description); // "userId" console.log(sym.toString()); // "Symbol(userId)"
Symbol.for, Symbol.keyFor
Symbol.for(key) creates/retrieves symbols from a global registry; Symbol.keyFor(sym) returns the key for a registry symbol.
const global1 = Symbol.for("app.id"); const global2 = Symbol.for("app.id"); console.log(global1 === global2); // true console.log(Symbol.keyFor(global1)); // "app.id"
Well-known Symbols
Well-known symbols are predefined symbols that customize object behavior for language operations like iteration, type conversion, and instanceof checks.
┌──────────────────────────────────────────┐
│ Well-known Symbols │
├──────────────────────────────────────────┤
│ Symbol.iterator │ Iteration │
│ Symbol.toStringTag │ Object.toString │
│ Symbol.hasInstance │ instanceof │
│ Symbol.toPrimitive │ Type conversion │
│ Symbol.species │ Derived objects │
└──────────────────────────────────────────┘
Symbol.iterator
Defines the default iterator for an object, enabling it to be used with for...of and spread syntax (covered above in Iterators section).
Symbol.toStringTag
Customizes the string returned by Object.prototype.toString.call(), useful for identifying custom object types.
class MyClass { get [Symbol.toStringTag]() { return "MyClass"; } } console.log(Object.prototype.toString.call(new MyClass())); // "[object MyClass]"
Symbol.hasInstance
Customizes instanceof behavior by defining a static method that determines if an object is an instance.
class MyArray { static [Symbol.hasInstance](instance) { return Array.isArray(instance); } } console.log([] instanceof MyArray); // true
Symbol.toPrimitive
Controls how an object converts to a primitive value, receiving a hint of "number", "string", or "default".
const obj = { [Symbol.toPrimitive](hint) { if (hint === "number") return 42; if (hint === "string") return "hello"; return true; } }; console.log(+obj, `${obj}`, obj + ""); // 42, "hello", "true"
Symbols as Property Keys
Symbols can be used as unique property keys that don't clash with string keys and are not enumerable by default in for...in or Object.keys().
const SECRET = Symbol("secret"); const obj = { name: "public", [SECRET]: "hidden" }; console.log(Object.keys(obj)); // ["name"] console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(secret)]
Symbol Use Cases
Symbols are ideal for: unique property keys (avoid collisions), private-ish properties (not truly private but hidden), protocol definitions (like iterators), and preventing property overwrites in libraries.
┌─────────────────────────────────────────────────┐
│ Symbol Use Cases │
├─────────────────────────────────────────────────┤
│ ✓ Unique object keys (no collision) │
│ ✓ Hide properties from Object.keys/for-in │
│ ✓ Define protocols (iteration, conversion) │
│ ✓ Library/framework extension points │
│ ✓ Metaprogramming hooks │
└─────────────────────────────────────────────────┘
Control Flow
if-else statements
The if-else statement executes code blocks conditionally based on boolean evaluation; if the condition is truthy, the first block runs, otherwise the else block executes.
let age = 18; if (age >= 18) { console.log("Adult"); } else { console.log("Minor"); }
Ternary operator
A concise one-line conditional operator with syntax condition ? valueIfTrue : valueIfFalse, perfect for simple conditional assignments or returns.
let status = age >= 18 ? "Adult" : "Minor";
switch statement
Evaluates an expression and matches it against multiple case clauses, executing the corresponding block; always use break to prevent fall-through behavior.
switch (day) { case 1: console.log("Monday"); break; case 2: console.log("Tuesday"); break; default: console.log("Other day"); }
for loop
A counter-controlled loop with three parts: initialization, condition, and increment/decrement, ideal when you know the exact number of iterations.
for (let i = 0; i < 5; i++) { console.log(i); // 0, 1, 2, 3, 4 }
while loop
A pre-condition loop that checks the condition before each iteration; the body may never execute if the condition is initially false.
let i = 0; while (i < 3) { console.log(i++); // 0, 1, 2 }
do-while loop
A post-condition loop that executes the body at least once before checking the condition, guaranteeing minimum one iteration.
let i = 0; do { console.log(i++); } while (i < 3);
for-in loop
Iterates over enumerable property keys (including inherited ones) of an object; not recommended for arrays due to unpredictable order.
const obj = {a: 1, b: 2}; for (let key in obj) { console.log(key, obj[key]); // "a" 1, "b" 2 }
for-of loop
Iterates over values of iterable objects (arrays, strings, Maps, Sets); introduced in ES6 and preferred for array iteration.
const arr = [10, 20, 30]; for (let value of arr) { console.log(value); // 10, 20, 30 }
break and continue
break exits the loop entirely, while continue skips the current iteration and proceeds to the next one.
for (let i = 0; i < 5; i++) { if (i === 2) continue; // Skip 2 if (i === 4) break; // Stop at 4 console.log(i); // 0, 1, 3 }
Labels
Labels are identifiers that prefix loops, allowing break or continue to target specific outer loops in nested structures.
outer: for (let i = 0; i < 3; i++) { for (let j = 0; j < 3; j++) { if (i === 1) break outer; // Exits both loops console.log(i, j); } }