Beginner JavaScript Interview Questions and Answers

๐Ÿ“‹ Table of Contents โ–พ
  1. Questions & Answers
  2. 📝 Knowledge Check

📰 Beginner JavaScript Interview Questions

This lesson covers the fundamental JavaScript concepts every web developer must know. Master data types, scope, closures, the event loop, prototypes, ES6+ features, array methods, and asynchronous patterns. These questions reflect what interviewers ask at junior and entry-level JavaScript roles.

Questions & Answers

01 What are JavaScript’s data types? ►

Core JavaScript has eight data types split into two categories: primitives (stored by value) and objects (stored by reference).

Primitive types (7): undefined, null, boolean, number, bigint, string, symbol

Object type (1): object โ€” includes plain objects, arrays, functions, Date, Map, Set, etc.

// Primitives -- copied by value
let a = 42;
let b = a;
b = 99;
console.log(a); // 42 -- unchanged

// Objects -- copied by reference
const obj1 = { x: 1 };
const obj2 = obj1;   // obj2 points to the SAME object
obj2.x = 99;
console.log(obj1.x); // 99 -- obj1 was mutated!

// typeof operator
typeof undefined   // "undefined"
typeof null        // "object"  โ† famous JS quirk (bug from 1995)
typeof true        // "boolean"
typeof 42          // "number"
typeof 42n         // "bigint"
typeof "hello"     // "string"
typeof Symbol()    // "symbol"
typeof {}          // "object"
typeof []          // "object"  โ† arrays are objects
typeof function(){} // "function"  โ† functions are objects but typeof returns "function"

// Precise type check for arrays
Array.isArray([])   // true
Object.prototype.toString.call([]) // "[object Array]"

02 What is the difference between var, let, and const? ►

Scope

// var -- function-scoped, hoisted to top of function, re-declarable
function example() {
  console.log(x); // undefined (hoisted, not ReferenceError)
  var x = 10;
  if (true) {
    var x = 20;   // same variable! leaks out of the block
  }
  console.log(x); // 20
}

// let -- block-scoped, temporal dead zone (TDZ), not re-declarable
{
  // console.log(y); // ReferenceError: Cannot access 'y' before initialisation (TDZ)
  let y = 10;
  y = 20;          // reassignment OK
}
// console.log(y); // ReferenceError: y is not defined (block scope)

// const -- block-scoped, must be initialised, cannot be reassigned
const PI = 3.14159;
// PI = 3;         // TypeError: Assignment to constant variable

// const with objects -- reference is const, contents are mutable
const arr = [1, 2, 3];
arr.push(4);       // OK -- mutating the object, not the binding
// arr = [];       // TypeError -- can't reassign the binding

// Best practices (modern JS):
// Default to const -- signals intent that value won't be reassigned
// Use let when reassignment is needed
// Avoid var in new code (unpredictable scoping)

03 What is hoisting in JavaScript? ►

Core Hoisting is JavaScript’s behaviour of moving declarations to the top of their scope during compilation. Only declarations are hoisted, not initialisations.

// Function declarations -- fully hoisted (name AND body)
sayHello(); // "Hello!" -- works before the declaration
function sayHello() { console.log("Hello!"); }

// var declarations -- hoisted but initialised as undefined
console.log(name); // undefined (NOT ReferenceError)
var name = "Alice";
console.log(name); // "Alice"

// let and const -- hoisted but NOT initialised (TDZ)
// console.log(age); // ReferenceError: Cannot access 'age' before initialisation
let age = 30;

// Function expressions -- only the variable is hoisted (as undefined)
// greet(); // TypeError: greet is not a function
var greet = function() { return "Hi!"; };

// Arrow functions -- same as function expressions
// hello(); // TypeError: hello is not a function
var hello = () => "Hello!";

// Class declarations -- hoisted but in TDZ (like let)
// const c = new Car(); // ReferenceError
class Car {}

// Practical rule: always declare before use
// Use const/let to avoid var hoisting surprises

04 What is a closure in JavaScript? ►

Scope A closure is a function that retains access to its enclosing scope even after that scope has finished executing. Inner functions “close over” the variables of their outer function.

// Basic closure
function makeCounter() {
  let count = 0;          // enclosed variable

  return function() {
    count++;
    return count;
  };
}

const counter = makeCounter();
counter(); // 1
counter(); // 2
counter(); // 3
// count is private -- not accessible from outside

// Closure factory pattern
function multiplier(factor) {
  return (number) => number * factor;  // closes over 'factor'
}

const double = multiplier(2);
const triple = multiplier(3);
double(5);  // 10
triple(5);  // 15

// Classic closure bug with var in loops
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
} // prints: 3 3 3 (all share the same 'i', which is 3 after the loop)

// Fix 1: use let (block scope creates a new binding per iteration)
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
} // prints: 0 1 2

// Fix 2: IIFE to capture current value
for (var i = 0; i < 3; i++) {
  ((j) => setTimeout(() => console.log(j), 100))(i);
} // prints: 0 1 2

05 What is the difference between == and === in JavaScript? ►

Core

  • == (loose equality) โ€” performs type coercion before comparison. Converts operands to the same type then compares values.
  • === (strict equality) โ€” no type coercion. Must be same type AND same value. Always prefer ===.
// == with coercion (surprising results)
0   == false     // true  (false coerces to 0)
1   == true      // true  (true coerces to 1)
""  == false     // true  (both coerce to 0)
0   == ""        // true  (both coerce to 0)
0   == "0"       // true  ("0" coerces to 0)
"0" == false     // true  (both coerce to 0)
null == undefined // true  (special case)
null == 0        // false  (null only == undefined/null)

// === no coercion (predictable)
0   === false    // false (different types: number vs boolean)
1   === true     // false
""  === false    // false
null === undefined // false

// NaN is special -- not equal to anything, even itself
NaN === NaN      // false
Number.isNaN(NaN) // true (correct check)

// Object equality (both == and === check REFERENCE, not content)
const a = { x: 1 };
const b = { x: 1 };
a == b   // false (different objects)
a === b  // false

// Rule: always use === except when intentionally checking null OR undefined
if (value == null) { ... }  // catches both null and undefined (acceptable use of ==)

06 What is the event loop in JavaScript? How does asynchronous code work? ►

Async JavaScript is single-threaded but handles asynchronous operations via the event loop. The event loop continuously checks the call stack and processes tasks from the callback queue when the stack is empty.

// Architecture:
// Call Stack     -- where synchronous code executes (LIFO)
// Web APIs       -- browser-provided async services (setTimeout, fetch, DOM events)
// Macrotask Queue -- callbacks from setTimeout, setInterval, I/O events
// Microtask Queue -- Promise callbacks, queueMicrotask (processed before macrotasks!)

console.log("1");                    // call stack

setTimeout(() => console.log("2"), 0); // macrotask queue (even with 0ms delay)

Promise.resolve().then(() => console.log("3")); // microtask queue

console.log("4");                    // call stack

// Output: 1, 4, 3, 2
// Reason:
// 1. Sync: "1", then "4"
// 2. Stack empty: drain microtask queue โ†’ "3"
// 3. Next event loop tick: macrotask โ†’ "2"

// Microtasks (higher priority) include:
// Promise.then/.catch/.finally, queueMicrotask(), MutationObserver

// Macrotasks (lower priority) include:
// setTimeout, setInterval, setImmediate (Node), requestAnimationFrame

// Async/Await is syntactic sugar over Promises
async function fetchUser(id) {
  const response = await fetch(`/api/users/${id}`); // suspends here
  const user = await response.json();               // suspends again
  return user;
}
// Each 'await' resumes as a microtask when the Promise resolves

07 What are Promises and how do you use them? ►

Async A Promise represents a value that may be available now, in the future, or never. It has three states: pending, fulfilled, or rejected.

// Creating a Promise
const fetchData = (id) => new Promise((resolve, reject) => {
  setTimeout(() => {
    if (id > 0)
      resolve({ id, name: "Alice" });
  else
    reject(new Error("Invalid ID"));
  }, 1000);
});

// .then / .catch / .finally chaining
fetchData(1)
  .then(user => user.name.toUpperCase())
  .then(name => console.log(name))     // "ALICE"
  .catch(err => console.error(err))
  .finally(() => console.log("Done"));

// Async/await (cleaner for complex flows)
async function getUser(id) {
  try {
    const user = await fetchData(id);
    return user.name.toUpperCase();
  } catch (err) {
    console.error(err);
  }
}

// Promise combinators
// Promise.all -- wait for ALL (rejects if ANY rejects)
const [user, posts, comments] = await Promise.all([
  fetchUser(1), fetchPosts(1), fetchComments(1)
]);

// Promise.allSettled -- wait for ALL, never rejects
const results = await Promise.allSettled([p1, p2, p3]);
// [{ status: "fulfilled", value: ... }, { status: "rejected", reason: ... }]

// Promise.race -- first to settle wins (fulfilled OR rejected)
const first = await Promise.race([fetchFast(), fetchSlow()]);

// Promise.any -- first to FULFILL (rejects only if ALL reject)
const first2 = await Promise.any([p1, p2, p3]);

08 What is prototype-based inheritance in JavaScript? ►

OOP JavaScript uses prototype chains for inheritance. Every object has an internal [[Prototype]] link. When you access a property, JS walks up the chain until it finds it or reaches null.

// Prototype chain
const animal = {
  speak() { return `${this.name} makes a sound`; }
};

const dog = Object.create(animal);   // dog's [[Prototype]] = animal
dog.name = "Rex";
dog.fetch = () => "fetching!";

dog.speak();        // "Rex makes a sound"  (found on animal via chain)
dog.fetch();        // "fetching!"          (found directly on dog)
dog.toString();     // found on Object.prototype (top of chain)

// Object.getPrototypeOf(dog) === animal  // true

// Constructor functions (old style)
function Person(name) { this.name = name; }
Person.prototype.greet = function() { return `Hi, I'm ${this.name}`; };

const alice = new Person("Alice");
alice.greet(); // "Hi, I'm Alice"

// ES6 class syntax (syntactic sugar over prototypes)
class Animal {
  constructor(name) { this.name = name; }
  speak() { return `${this.name} makes a sound`; }
}

class Dog extends Animal {
  speak() { return `${this.name} barks!`; }  // overrides parent
  fetch() { return `${this.name} fetches!`; }
}

const d = new Dog("Rex");
d.speak();                            // "Rex barks!"
d instanceof Dog                      // true
d instanceof Animal                   // true
Object.getPrototypeOf(d) === Dog.prototype // true

09 What is the this keyword in JavaScript? ►

Core this is determined by how a function is called, not where it is defined (except for arrow functions which inherit this from their enclosing lexical scope).

// 1. Global context
console.log(this);        // window (browser) or global (Node.js)
// In strict mode: undefined inside functions

// 2. Method call -- this = the object before the dot
const person = {
  name: "Alice",
  greet() { return `Hi, I'm ${this.name}`; }
};
person.greet();           // "Hi, I'm Alice"

// 3. Regular function -- depends on call site
function whoAmI() { return this; }
whoAmI();                 // window / undefined (strict mode)

// 4. Arrow function -- inherits this from enclosing scope (lexical this)
const obj = {
  name: "Alice",
  greet: () => `Hi, I'm ${this.name}`,   // โŒ this = window (not obj)
  greetFixed() {
    const inner = () => `Hi, I'm ${this.name}`;  // โœ… this = obj
    return inner();
  }
};

// 5. Explicit binding
function greet(greeting) { return `${greeting}, ${this.name}`; }
greet.call({ name: "Bob" }, "Hello");    // "Hello, Bob" (call with args)
greet.apply({ name: "Bob" }, ["Hi"]);    // "Hi, Bob" (apply with array)
const boundGreet = greet.bind({ name: "Carol" }); // returns new function
boundGreet("Hey");                       // "Hey, Carol"

// 6. new keyword -- this = newly created object
class Timer {
  constructor() { this.seconds = 0; }
  start() { setInterval(() => this.seconds++, 1000); }
}
const t = new Timer(); // this inside constructor = new Timer instance

10 What are arrow functions and how do they differ from regular functions? ►

ES6

// Regular function
function square(x) { return x * x; }
const square2 = function(x) { return x * x; };

// Arrow function (concise)
const square3 = x => x * x;               // implicit return
const add = (a, b) => a + b;
const getObj = () => ({ name: "Alice" });  // wrap in () for object literal
const multi = (a, b) => { return a * b; }; // block body, explicit return

// Key differences:
// 1. Lexical 'this' (no own this binding)
class Counter {
  constructor() { this.count = 0; }
  start() {
    // Regular: this would be lost inside setInterval callback
    setInterval(function() { this.count++; }, 1000); // โŒ this = window

    // Arrow: inherits this from start() (the Counter instance)
    setInterval(() => { this.count++; }, 1000);      // โœ…
  }
}

// 2. No 'arguments' object (use rest params instead)
function regular() { console.log(arguments); }  // works
const arrow = (...args) => console.log(args);   // use rest params

// 3. Cannot be used as constructors (no 'new')
const Foo = () => {};
// new Foo(); // TypeError: Foo is not a constructor

// 4. No 'prototype' property
const fn = () => {};
fn.prototype; // undefined

// When NOT to use arrow functions:
// - Object methods (this would be wrong)
// - Event handlers where 'this' should be the element
// - Prototype methods
// - When you need 'arguments'

11 What is destructuring in JavaScript? ►

ES6 Destructuring extracts values from arrays or properties from objects into distinct variables with concise syntax.

// Array destructuring
const [first, second, , fourth] = [1, 2, 3, 4];
// Skip elements with empty comma

// Default values
const [a = 10, b = 20] = [1];
// a = 1, b = 20 (default used)

// Swap variables
let x = 1, y = 2;
[x, y] = [y, x];  // x=2, y=1

// Rest element
const [head, ...tail] = [1, 2, 3, 4];
// head = 1, tail = [2, 3, 4]

// Object destructuring
const { name, age, city = "London" } = { name: "Alice", age: 30 };
// name = "Alice", age = 30, city = "London" (default)

// Rename on destructure
const { name: personName, age: personAge } = { name: "Bob", age: 25 };
// personName = "Bob", personAge = 25

// Nested destructuring
const { address: { street, postcode } } = { address: { street: "Main St", postcode: "SW1A" } };

// In function parameters (very common in React)
function greet({ name, greeting = "Hello" }) {
  return `${greeting}, ${name}!`;
}
greet({ name: "Alice" });  // "Hello, Alice!"

// Rest in objects
const { name: n, ...rest } = { name: "Alice", age: 30, city: "London" };
// n = "Alice", rest = { age: 30, city: "London" }

12 What are the spread and rest operators? ►

ES6 Both use ... syntax but serve opposite purposes. Spread expands an iterable into individual elements. Rest collects multiple elements into an array/object.

// Spread operator -- expands iterables
// Copy an array (shallow)
const original = [1, 2, 3];
const copy = [...original];
copy.push(4);
console.log(original); // [1, 2, 3] -- unchanged

// Merge arrays
const merged = [...[1, 2], ...[3, 4], 5]; // [1, 2, 3, 4, 5]

// Spread in function call
Math.max(...[1, 5, 3, 9, 2]);  // 9

// Copy an object (shallow)
const obj = { a: 1, b: 2 };
const copy2 = { ...obj, c: 3 };      // { a:1, b:2, c:3 }
const override = { ...obj, b: 99 }; // { a:1, b:99 } -- b overridden

// Rest parameters -- collect remaining arguments
function sum(first, ...rest) {
  return rest.reduce((acc, n) => acc + n, first);
}
sum(1, 2, 3, 4); // 10

// Rest in destructuring
const [head, ...tail] = [1, 2, 3, 4];
const { a: aa, ...others } = { a:1, b:2, c:3 };
// aa = 1, others = { b:2, c:3 }

// Spread strings (splits into characters)
[..."hello"] // ['h','e','l','l','o']

// Convert NodeList to array
const divs = [...document.querySelectorAll('div')];

13 What are common array methods in JavaScript? ►

Arrays

const nums = [1, 2, 3, 4, 5];

// Transformation (return new array)
nums.map(n => n * 2)                  // [2, 4, 6, 8, 10]
nums.filter(n => n % 2 === 0)         // [2, 4]
nums.reduce((acc, n) => acc + n, 0)   // 15 (sum)
nums.flatMap(n => [n, n * 2])         // [1,2,2,4,3,6,4,8,5,10]

// Search
nums.find(n => n > 3)                // 4 (first match)
nums.findIndex(n => n > 3)           // 3 (index of first match)
nums.indexOf(3)                        // 2
nums.includes(3)                       // true
nums.some(n => n > 4)                 // true (at least one)
nums.every(n => n > 0)                // true (all match)

// Sorting and ordering
[3, 1, 4, 1, 5].sort((a, b) => a - b) // [1,1,3,4,5] ascending
[3, 1, 4].sort((a, b) => b - a)        // [4,3,1] descending
[...nums].reverse()                     // [5,4,3,2,1] (use spread to avoid mutation)

// Slice and splice
nums.slice(1, 3)                       // [2, 3] (non-mutating)
const a = [1,2,3,4,5];
a.splice(2, 1, 99)                     // removes 1 at index 2, inserts 99 โ†’ a=[1,2,99,4,5]

// Flatten
[[1,2],[3,[4,5]]].flat()               // [1,2,3,[4,5]] (one level)
[[1,2],[3,[4,5]]].flat(Infinity)       // [1,2,3,4,5] (all levels)

// Mutation methods (modify in place)
nums.push(6)      // add to end
nums.pop()        // remove from end
nums.unshift(0)   // add to start
nums.shift()      // remove from start
nums.fill(0, 2, 4) // fill indices 2-3 with 0

14 What are template literals? ►

ES6 Template literals use backticks (`) and allow embedded expressions, multi-line strings, and tagged templates.

const name = "Alice";
const age = 30;

// String interpolation (replaces ugly string concatenation)
const msg = `Hello, ${name}! You are ${age} years old.`;
const calc = `2 + 2 = ${2 + 2}`;             // expressions work
const upper = `${name.toUpperCase()} logged in`; // method calls work
const nested = `${age >= 18 ? "adult" : "minor"}`;

// Multi-line strings (no \n needed)
const html = `
  <div class="card">
    <h2>${name}</h2>
    <p>Age: ${age}</p>
  </div>
`;

// Tagged templates -- process template with a function
function highlight(strings, ...values) {
  return strings.reduce((result, str, i) =>
    `${result}${str}${values[i] !== undefined ? `<em>${values[i]}</em>` : ""}`, "");
}

highlight`Hello, ${name}! You are ${age} years old.`;
// "Hello, <em>Alice</em>! You are <em>30</em> years old."

// Real-world tagged templates:
// - css`color: red;` (styled-components)
// - html`<div>${content}</div>` (lit-html, sanitised rendering)
// - sql`SELECT * FROM users WHERE id = ${userId}` (parameterised queries)

15 What is the difference between null and undefined in JavaScript? ►

Core

  • undefined โ€” a variable has been declared but not assigned a value. Also the default return value of functions that return nothing. Assigned by the JavaScript engine automatically.
  • null โ€” an intentional absence of a value. Assigned explicitly by the programmer to represent “no value”.
let x;
console.log(x);             // undefined (declared, not assigned)
console.log(typeof x);      // "undefined"

function noReturn() {}
console.log(noReturn());    // undefined

const obj = { name: "Alice" };
console.log(obj.age);       // undefined (property doesn't exist)

let y = null;               // programmer intentionally set to "no value"
console.log(y);             // null
console.log(typeof y);      // "object" (the infamous JS bug)

// Comparisons
null == undefined   // true  (loose -- they are "equal" to each other)
null === undefined  // false (strict -- different types)

// Nullish coalescing ??
const value = null ?? "default";  // "default" (only null/undefined trigger)
const zero  = 0 ?? "default";     // 0 (0 is NOT nullish)
const empty = "" ?? "default";    // "" (empty string is NOT nullish)

// Optional chaining ?. (avoids TypeError on null/undefined)
const street = user?.address?.street; // undefined instead of TypeError
const firstItem = items?.[0];         // safe array access
const result = fn?.();                // safe function call

16 What are ES6 modules (import / export)? ►

ES6

// Named exports -- can have multiple per file
// math.js
export const PI = 3.14159;
export function add(a, b) { return a + b; }
export function subtract(a, b) { return a - b; }

// Named import
import { add, subtract } from "./math.js";
import { add as sum } from "./math.js";  // rename
import * as math from "./math.js";       // namespace import
math.add(1, 2);

// Default export -- one per file
// greet.js
export default function greet(name) { return `Hello, ${name}!`; }

// Default import (any name you want)
import greet from "./greet.js";
import sayHello from "./greet.js";  // same thing, different name

// Mix named and default
export default class User { ... }
export const MAX_USERS = 100;

import User, { MAX_USERS } from "./user.js";

// Re-export (barrel file pattern)
// index.js
export { add, subtract } from "./math.js";
export { default as User } from "./user.js";
export * from "./utils.js";

// Dynamic import (lazy loading)
const { default: module } = await import("./heavy-module.js");
// Loaded on demand -- not bundled at startup

// Module characteristics:
// - Always in strict mode
// - Own scope (variables not global)
// - Evaluated once (subsequent imports return cached exports)
// - Static analysis (enables tree-shaking)

17 What are Map and Set in JavaScript? How do they differ from objects and arrays? ►

ES6

// Map -- key-value pairs (any type as key, ordered, iterable)
const map = new Map();
map.set("name", "Alice");
map.set(42, "the answer");
map.set({ id: 1 }, "obj key");  // objects CAN be keys (unlike plain objects)

map.get("name");          // "Alice"
map.has(42);              // true
map.size;                 // 3
map.delete(42);
map.forEach((value, key) => console.log(key, value));

// Convert
const fromObj = new Map(Object.entries({ a: 1, b: 2 }));
const toObj = Object.fromEntries(map);

// Map vs Object:
// Map: any key type, ordered, iterable directly, .size, no prototype pollution
// Object: string/symbol keys only, less ergonomic, faster for simple lookups

// Set -- unique values (ordered, iterable)
const set = new Set([1, 2, 3, 2, 1]);  // {1, 2, 3} -- duplicates removed
set.add(4);
set.has(2);        // true
set.delete(3);
set.size;          // 3

// Deduplicate an array
const unique = [...new Set([1, 2, 2, 3, 3, 3])]; // [1, 2, 3]

// Set operations
const a = new Set([1, 2, 3, 4]);
const b = new Set([3, 4, 5, 6]);
const union        = new Set([...a, ...b]);             // {1,2,3,4,5,6}
const intersection = new Set([...a].filter(x => b.has(x))); // {3,4}
const difference   = new Set([...a].filter(x => !b.has(x))); // {1,2}

// WeakMap and WeakSet -- hold WEAK references (allow GC)
const wm = new WeakMap(); // keys must be objects, not iterable
// Useful for: storing metadata about objects without preventing GC

18 What is event delegation in JavaScript? ►

DOM Event delegation attaches a single event listener to a parent element instead of many listeners on each child. It works because events bubble up the DOM tree.

// Without delegation -- listener on every button (bad for 1000 items)
document.querySelectorAll('.btn').forEach(btn =>
  btn.addEventListener('click', handleClick)
);

// With delegation -- ONE listener on the parent
document.getElementById('button-list')
  .addEventListener('click', (event) => {
    // Check the actual target
    if (event.target.matches('.btn')) {
      console.log('Clicked:', event.target.dataset.id);
    }
    // target   = element that was actually clicked
    // currentTarget = element with the listener (button-list)
  });

// Works for dynamically added elements (don't need to re-bind)
// Adding new buttons to #button-list automatically gets the listener

// Event propagation phases:
// 1. Capture phase: event travels from window DOWN to target
// 2. Target phase: event reaches the target
// 3. Bubble phase: event bubbles UP from target to window

// Most listeners use bubble phase (default, third arg false/omitted)
// Capture phase listeners: addEventListener(event, handler, true)

// Stop propagation
event.stopPropagation();    // stop bubbling/capturing
event.stopImmediatePropagation(); // also prevents other handlers on same element

// Prevent default browser behaviour
event.preventDefault();     // e.g., prevent form submission, link navigation

19 What is the difference between synchronous and asynchronous code? ►

Async

// Synchronous -- blocks execution until complete
function fetchSync(url) {
  // Imagine this blocks for 2 seconds
  const data = readFileSync(url); // blocks the entire thread
  return data;                    // nothing else can run during those 2 seconds
}

// Asynchronous -- non-blocking; execution continues while waiting
// Pattern 1: Callbacks (old style)
fs.readFile("data.txt", "utf8", (err, data) => {
  if (err) throw err;
  console.log(data);
});
console.log("This runs before the file is read!");

// Pattern 2: Promises
fetch("/api/user/1")
  .then(res => res.json())
  .then(user => console.log(user));
console.log("This runs before fetch completes");

// Pattern 3: Async/await (most readable)
async function getUser() {
  const res  = await fetch("/api/user/1");   // suspended here, no blocking
  const user = await res.json();
  console.log(user);
}
getUser();
console.log("This runs before getUser finishes");

// Callback hell problem (nested callbacks)
getUser(id, (user) => {
  getPosts(user.id, (posts) => {
    getComments(posts[0].id, (comments) => {
      // deeply nested, hard to read and error-handle
    });
  });
});

// Solved with async/await
const user     = await getUser(id);
const posts    = await getPosts(user.id);
const comments = await getComments(posts[0].id);
// Linear, readable, proper try/catch error handling

20 What is JSON and how do you work with it in JavaScript? ►

Core JSON (JavaScript Object Notation) is a lightweight text format for data exchange. It is language-agnostic but JavaScript-inspired.

// JSON syntax rules:
// Keys must be double-quoted strings
// Values: string, number, boolean, null, array, or object (no undefined, no functions)
// No trailing commas, no comments

const jsonString = '{"name":"Alice","age":30,"hobbies":["JS","chess"],"active":true}';

// Parsing: JSON string โ†’ JavaScript object
const obj = JSON.parse(jsonString);
obj.name;    // "Alice"
obj.hobbies; // ["JS", "chess"]

// Serialising: JavaScript object โ†’ JSON string
const user = { name: "Bob", age: 25, greet() { return "Hi!"; } };
JSON.stringify(user);
// '{"name":"Bob","age":25}'  -- functions are OMITTED

// Formatting (pretty-print)
JSON.stringify(user, null, 2); // 2-space indented

// Replacer -- filter/transform properties
JSON.stringify(user, ["name", "age"]); // only include name and age
JSON.stringify(user, (key, val) => key === "password" ? undefined : val); // omit password

// Reviver -- transform when parsing
JSON.parse('{"date":"2026-04-22"}', (key, val) =>
  key === "date" ? new Date(val) : val
);

// Deep clone with JSON (works for serialisable data)
const clone = JSON.parse(JSON.stringify(obj));
// โš  Loses: undefined, functions, Date (becomes string), Map, Set, circular refs

// Better deep clone (modern)
const clone2 = structuredClone(obj);  // handles Dates, Maps, Sets, etc.

21 What is the optional chaining operator (?.) and nullish coalescing (??)? ►

ES2020

const user = {
  name: "Alice",
  address: {
    street: "123 Main St"
    // no city property
  }
};

// Without optional chaining -- verbose and error-prone
const city = user && user.address && user.address.city;

// With optional chaining -- short-circuits to undefined if any step is null/undefined
const city2 = user?.address?.city;          // undefined (no error)
const zip   = user?.address?.postcode;      // undefined

// Method calls
const upper = user?.getName?.();            // undefined if getName doesn't exist
const len   = user?.hobbies?.length;        // undefined if hobbies is null/undefined

// Array access
const first = user?.hobbies?.[0];           // undefined if hobbies is null

// Nullish coalescing ?? (only triggers on null or undefined, NOT 0 or "")
const city3 = user?.address?.city ?? "London";  // "London" (city is undefined)
const count = user?.postCount ?? 0;             // 0 default

// ?? vs || (logical OR)
0 || "default"       // "default" (0 is falsy, || triggers)
0 ?? "default"       // 0 (0 is not null/undefined, ?? does NOT trigger)
"" || "default"      // "default" (empty string is falsy)
"" ?? "default"      // "" (?? only cares about null/undefined)

// Nullish assignment ??=
user.nickname ??= "Anonymous"; // assigns only if user.nickname is null/undefined

// Logical assignment
user.score ||= 100;  // assigns if falsy
user.score &&= user.score * 2;  // assigns if truthy

22 What is a Symbol in JavaScript and when do you use it? ►

ES6 Symbol creates a guaranteed-unique value. Symbols are never accidentally equal to anything else โ€” useful for private-like object keys and well-known protocol hooks.

// Every Symbol is unique
const s1 = Symbol("label");
const s2 = Symbol("label");
s1 === s2  // false (even with the same description!)

// Use as unique object property keys (won't clash with string keys)
const ID   = Symbol("id");
const user = {
  name: "Alice",
  [ID]: 12345          // Symbol as computed key
};
user[ID];              // 12345
user.name;             // "Alice"

// Symbols are NOT enumerable (don't appear in for...in or Object.keys)
Object.keys(user);     // ["name"] -- Symbol is hidden
Object.getOwnPropertySymbols(user); // [Symbol(id)]

// Well-known Symbols -- customise language behaviour
class Range {
  constructor(start, end) { this.start = start; this.end = end; }

  // Make Range iterable with for...of
  [Symbol.iterator]() {
    let current = this.start;
    const end   = this.end;
    return {
      next() {
        return current <= end
          ? { value: current++, done: false }
          : { done: true };
      }
    };
  }
}

for (const n of new Range(1, 5)) console.log(n); // 1 2 3 4 5
[...new Range(1, 3)];  // [1, 2, 3]

// Other well-known Symbols:
// Symbol.toPrimitive -- customise type conversion
// Symbol.hasInstance -- customise instanceof behaviour
// Symbol.toStringTag -- customise Object.prototype.toString output

📝 Knowledge Check

🧠 Quiz Question 1 of 5

What is the output order of: console.log(“A”); setTimeout(()=>console.log(“B”),0); Promise.resolve().then(()=>console.log(“C”)); console.log(“D”);





🧠 Quiz Question 2 of 5

What is the classic closure bug with var in a for loop and how does let fix it?





🧠 Quiz Question 3 of 5

Why does const not make objects immutable?





🧠 Quiz Question 4 of 5

What is the key difference between the ?? (nullish coalescing) and || (logical OR) operators?





🧠 Quiz Question 5 of 5

Why do arrow functions not work correctly as object methods when they reference this?