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?