Skip to content
beginner Phase 4 · JavaScript Fundamentals

JavaScript ES6+

Master modern JavaScript — let/const, arrow functions, destructuring, spread/rest, modules, and template literals.

1h 30m
0 problems
Topic Progress 0%

let, const, and Variable Scoping

let, const, and Variable Scoping

The Problem with var

var is function-scoped, which leads to surprising behavior in loops and conditionals. It is also hoisted to the top of its function scope, meaning a variable can be accessed before it is declared, resulting in undefined rather than a ReferenceError.

function varExample() {
  if (true) {
    var x = 10;
  }
  console.log(x); // 10 — var leaks out of the if block
}

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100); // 3, 3, 3 — not 0, 1, 2
}

Block Scoping with let

let is block-scoped, meaning it only exists within the nearest set of curly braces. This makes it safe for loops, conditionals, and any block structure where you need a variable to be isolated.

function letExample() {
  if (true) {
    let y = 20;
  }
  // console.log(y); // ReferenceError: y is not defined
}

for (let j = 0; j < 3; j++) {
  setTimeout(() => console.log(j), 100); // 0, 1, 2 — each iteration gets its own j
}

Const for Immutability References

const declares a variable that cannot be reassigned. It does not make the value itself immutable — objects and arrays declared with const can still have their contents modified. Use const by default and only switch to let when reassignment is genuinely needed.

const API_URL = 'https://api.example.com/v2';
// API_URL = 'https://other.com'; // TypeError: Assignment to constant variable

const user = { name: 'Alice' };
user.name = 'Bob'; // This works — the object's contents are mutable
// user = {}; // TypeError: Assignment to constant variable

const items = [1, 2, 3];
items.push(4); // Works — array contents are mutable
items = [5, 6]; // TypeError

Temporal Dead Zone (TDZ)

Both let and const are hoisted but are not initialized until their declaration is evaluated. Accessing them before declaration throws a ReferenceError. This period between hoisting and initialization is called the Temporal Dead Zone.

// console.log(a); // ReferenceError: Cannot access 'a' before initialization
let a = 5;

// This is why using a variable before it is declared is called the TDZ
function tdzDemo() {
  // The TDZ makes debugging easier than var's silent undefined
  let message = 'hello';
}

When to Use Each

Use const for all declarations by default. Switch to let only when you need to reassign the variable, such as loop counters, accumulators, or variables that are updated conditionally. Never use var in modern JavaScript — it creates bugs that are hard to trace due to its function-scoping and hoisting behavior.

Arrow Functions and Default Parameters

Arrow Functions and Default Parameters

Arrow Function Syntax

Arrow functions provide a shorter syntax for writing functions. They have two main forms: expression-bodied functions for single return values, and block-bodied functions when you need multiple statements.

// Traditional function
function add(a, b) {
  return a + b;
}

// Arrow function — expression body
const add = (a, b) => a + b;

// Single parameter — parentheses are optional
const double = n => n * 2;

// No parameters require empty parentheses
const greet = () => 'Hello!';

// Block body — must use explicit return
const processUser = (user) => {
  const name = user.name.trim();
  const email = user.email.toLowerCase();
  return { name, email };
};

Lexical this Binding

Arrow functions do not have their own this context. They inherit this from the enclosing lexical scope. This makes them ideal for callbacks, event handlers, and methods where you want to preserve the outer this.

const team = {
  name: 'Engineering',
  members: ['Alice', 'Bob', 'Charlie'],
  listMembers() {
    // Arrow function inherits `this` from listMembers
    this.members.forEach((member) => {
      console.log(`${member} is on ${this.name}`);
    });
  },
  createGreeting() {
    // Arrow function captures `this` from createGreeting
    return () => `Welcome to ${this.name}!`;
  },
};

const greeting = team.createGreeting();
greeting(); // 'Welcome to Engineering!'

Default Parameters

Default parameters allow you to specify fallback values when arguments are not provided or are explicitly undefined. They are evaluated left-to-right and can reference earlier parameters.

function createUser(name, role = 'viewer', active = true) {
  return { name, role, active };
}

createUser('Alice');           // { name: 'Alice', role: 'viewer', active: true }
createUser('Bob', 'admin');    // { name: 'Bob', role: 'admin', active: true }
createUser('Charlie', undefined, false); // uses default role

// Defaults can reference earlier params
function buildUrl(base, version = 1, path = `/api/v${version}`) {
  return `${base}${path}`;
}

buildUrl('https://api.com');              // 'https://api.com/api/v1'
buildUrl('https://api.com', 2);           // 'https://api.com/api/v2'
buildUrl('https://api.com', 2, '/users'); // 'https://api.com/users'

Higher-Order Functions with Arrows

Arrow functions pair naturally with array methods like map, filter, and reduce to create concise, readable data transformations.

const products = [
  { name: 'Laptop', price: 999, inStock: true },
  { name: 'Mouse', price: 29, inStock: true },
  { name: 'Monitor', price: 349, inStock: false },
  { name: 'Keyboard', price: 79, inStock: true },
];

const availableProducts = products
  .filter(product => product.inStock)
  .map(product => ({
    ...product,
    displayPrice: `$${product.price}`,
  }));

// [{ name: 'Laptop', price: 999, inStock: true, displayPrice: '$999' }, ...]

const totalValue = products
  .filter(p => p.inStock)
  .reduce((sum, p) => sum + p.price, 0); // 1107

Destructuring, Spread, and Rest

Destructuring, Spread, and Rest

Object Destructuring

Destructuring lets you extract values from objects and arrays into distinct variables. You can rename variables, provide defaults, and nest deeply.

const user = {
  name: 'Alice',
  age: 30,
  address: { city: 'Seattle', state: 'WA' },
  preferences: { theme: 'dark', language: 'en' },
};

// Basic destructuring
const { name, age } = user; // name = 'Alice', age = 30

// Rename variables
const { name: userName, age: userAge } = user;

// Provide defaults
const { role = 'viewer' } = user; // role = 'viewer' (not on user)

// Nested destructuring
const { address: { city, state } } = user; // city = 'Seattle'

// Deep nesting with rename
const { preferences: { theme: currentTheme } } = user; // currentTheme = 'dark'

Array Destructuring

Array destructuring uses position rather than names. You can skip elements and use rest syntax to capture remaining items.

const colors = ['red', 'green', 'blue', 'yellow', 'purple'];

// Skip elements
const [primary, , tertiary] = colors; // primary = 'red', tertiary = 'blue'

// Collect rest into array
const [first, second, ...remaining] = colors;
// first = 'red', second = 'green', remaining = ['blue', 'yellow', 'purple']

// Swap variables without temp
let a = 1, b = 2;
[a, b] = [b, a]; // a = 2, b = 1

// Function return values
function getUserCoords() {
  return { lat: 47.6, lng: -122.3 };
}
const { lat, lng } = getUserCoords();

Spread Operator

The spread operator (...) expands arrays, objects, or iterables. It creates a shallow copy, which is useful for immutability patterns.

// Array spread
const arr1 = [1, 2, 3];
const arr2 = [...arr1, 4, 5]; // [1, 2, 3, 4, 5]

// Merge arrays
const defaults = ['a', 'b', 'c'];
const custom = [...defaults, 'd', 'e']; // ['a', 'b', 'c', 'd', 'e']

// Object spread
const baseConfig = { host: 'localhost', port: 3000 };
const devConfig = { ...baseConfig, debug: true, port: 3001 };
// { host: 'localhost', port: 3001, debug: true } — later properties win

// Shallow clone for immutability
const original = { x: 1, y: { z: 2 } };
const clone = { ...original, x: 2 };
// clone.y === original.y (same reference — shallow copy)

Rest Parameters

Rest syntax collects remaining elements into an array. It works in function parameters and destructuring.

// Rest in function parameters
function sum(...numbers) {
  return numbers.reduce((total, n) => total + n, 0);
}
sum(1, 2, 3, 4); // 10

// Rest in destructuring
const { name, ...profileData } = user;
// name = 'Alice', profileData = { age: 30, address: {...}, preferences: {...} }

// Filter out a property
const { password, ...safeUser } = user;
// safeUser has everything except password

// Pass rest to another function
function logAll(first, ...rest) {
  console.log('First:', first);
  console.log('Rest:', rest);
}
logAll('a', 'b', 'c', 'd'); // First: a, Rest: ['b', 'c', 'd']

Template Literals and String Methods

Template Literals and String Methods

Template Literals Basics

Template literals use backticks instead of quotes. They support multi-line strings, embedded expressions, and tagged functions for custom parsing.

const name = 'World';
const greeting = `Hello, ${name}!`; // 'Hello, World!'

// Multi-line strings without concatenation
const html = `
  <div class="card">
    <h2>${user.name}</h2>
    <p>${user.email}</p>
    <span>Member since ${new Date().getFullYear()}</span>
  </div>
`;

// Expression interpolation
const price = 49.99;
const quantity = 3;
const total = `Total: $${(price * quantity).toFixed(2)}`; // 'Total: $149.97'

// Nested ternaries and function calls
const status = `User ${user.name} is ${user.active ? 'active' : 'inactive'} since ${user.createdAt.toLocaleDateString()}`;

Tagged Template Literals

Tagged templates let you process a template literal through a custom function. The function receives the raw string parts and interpolated values separately, enabling safe HTML templating, internationalization, or syntax highlighting.

// SQL template tag for safe queries
function sql(strings, ...values) {
  const query = strings.reduce((result, str, i) => {
    const value = i < values.length ? values[i] : '';
    return result + str + (value !== undefined ? value : '');
  }, '');
  return query;
}

const userId = 42;
const query = sql`SELECT * FROM users WHERE id = ${userId}`;
// 'SELECT * FROM users WHERE id = 42'

// HTML escaping tag
function safeHtml(strings, ...values) {
  const escape = (str) => String(str)
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;');
  return strings.reduce((result, str, i) => {
    return result + str + (i < values.length ? escape(values[i]) : '');
  }, '');
}

const userInput = '<script>alert("xss")</script>';
const safe = safeHtml`<p>User said: ${userInput}</p>`;
// '<p>User said: &lt;script&gt;alert("xss")&lt;/script&gt;</p>'

Modern String Methods

ES6+ introduced several string methods that make working with text cleaner and more predictable.

const str = '  Hello, World!  ';

// startsWith, endsWith, includes
str.startsWith('  Hello'); // true
str.endsWith('!  ');       // true
str.includes('World');      // true

// repeat
'ha'.repeat(3); // 'hahaha'

// padStart, padEnd (great for formatting)
'42'.padStart(5, '0');   // '00042'
'hi'.padEnd(10, '.');    // 'hi........'

// trimStart, trimEnd
'  hello  '.trimStart(); // 'hello  '
'  hello  '.trimEnd();   // '  hello'

// String.prototype.replaceAll (ES2021)
'hello-world-foo'.replaceAll('-', '_'); // 'hello_world_foo'

// at() method (ES2022)
'hello'.at(0);  // 'h'
'hello'.at(-1); // 'o'

ES Modules and Code Organization

ES Modules and Code Organization

Named and Default Exports

ES modules let you split code into separate files. Each file is a module with its own scope. Named exports export multiple values, while default exports export a single main value per file.

// utils/math.js — named exports
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
export const multiply = (a, b) => a * b;

export default class Calculator {
  result = 0;
  add(n) { this.result += n; return this; }
  subtract(n) { this.result -= n; return this; }
  getValue() { return this.result; }
}

// app.js — imports
import Calculator from './utils/math.js';        // default
import { add, subtract } from './utils/math.js'; // named
import { add as addNumbers } from './utils/math.js'; // rename on import
import * as MathUtils from './utils/math.js';    // namespace all exports

const calc = new Calculator();
calc.add(5).add(10);
console.log(calc.getValue());  // 15
console.log(addNumbers(3, 4)); // 7
console.log(MathUtils.multiply(2, 5)); // 10

Dynamic Imports for Code Splitting

Dynamic imports load modules on demand, reducing initial bundle size. This is essential for route-based splitting and lazy loading heavy features.

// Button that loads a chart library on first click
const chartButton = document.getElementById('load-chart');
chartButton.addEventListener('click', async () => {
  const { Chart, LineChart } = await import('./chart.js');
  const chart = new LineChart(canvas, chartData);
  chart.render();
});

// Route-based code splitting
const routes = {
  '/dashboard': () => import('./pages/Dashboard.js'),
  '/settings': () => import('./pages/Settings.js'),
  '/analytics': () => import('./pages/Analytics.js'),
};

async function loadRoute(path) {
  const loader = routes[path];
  if (!loader) throw new Error(`Route ${path} not found`);
  const module = await loader();
  return module.default;
}

// Conditional module loading
if (process.env.NODE_ENV === 'development') {
  const { DevTools } = await import('./dev/DevTools.js');
  DevTools.init();
}

Module Patterns and Best Practices

Use barrel exports to re-export from a single entry point. Keep modules focused on one responsibility. Avoid circular dependencies by extracting shared interfaces into separate files.

// components/index.js — barrel export
export { Button } from './Button.js';
export { Input } from './Input.js';
export { Modal } from './Modal.js';
export { Card } from './Card.js';

// Usage — import from barrel
import { Button, Input, Modal } from './components';

// Re-export with renaming
export { default as TextInput } from './Input.js';

// Type re-exports (for TypeScript users)
export type { User, Product } from './types.js';

Top-Level Await

Modern JavaScript engines support await at the top level of a module. This simplifies initialization without wrapping everything in an async IIFE.

// config.js — top-level await
const response = await fetch('/config.json');
export const config = await response.json();

// database.js — initialize connection
export const db = await connectDatabase(config.databaseUrl);

// app.js — import ready-to-use values
import { config } from './config.js';
import { db } from './database.js';
// Both are already initialized when used

Quiz

1. What is the key difference between `let` and `var` in terms of scoping?

Question 1 options

2. What does the following code output? ```javascript const fn = (x) => { return { value: x }; }; console.log(fn(5)); ```

Question 2 options

3. Which of the following is true about ES modules?

Question 3 options

Flashcards

Question

What is the Temporal Dead Zone (TDZ) and how does it affect `let` and `const`?

Answer

The TDZ is the period between when a variable is hoisted and when its declaration is evaluated. Accessing a `let` or `const` variable in the TDZ throws a ReferenceError. Unlike `var`, which is initialized to `undefined` during hoisting, `let` and `const` remain uninitialized until their declaration line is reached.

Question

When should you use `const` over `let`, and why is `var` considered harmful?

Answer

`const` should be the default choice for all declarations since it prevents accidental reassignment. Use `let` only when you genuinely need to reassign a variable (loop counters, accumulators). `var` is harmful because it is function-scoped instead of block-scoped, gets hoisted with an `undefined` initialization, and can be redeclared, all of which lead to hard-to-trace bugs.

Question

How does the spread operator differ from the rest parameter, and what do they have in common?

Answer

Both use the `...` syntax. The spread operator expands an iterable (array, object, string) into individual elements — useful for copying, merging, and passing arguments. The rest parameter collects remaining elements into an array — useful in function parameters and destructuring. Spread goes from collection to elements, rest goes from elements to collection.

Revision Notes

Key Takeaways

  • 1. Use `const` by default and `let` only when reassignment is needed; never use `var` in modern code
  • 2. Arrow functions provide concise syntax and lexical `this` binding — ideal for callbacks and higher-order functions
  • 3. Destructuring extracts values from objects and arrays into named variables; spread expands, rest collects
  • 4. Template literals support multi-line strings, expression interpolation, and tagged functions for custom processing
  • 5. ES modules use `import`/`export` syntax for clean dependency management; dynamic `import()` enables code splitting

Interview Tips

  • Explain the difference between `var`, `let`, and `const` with concrete code examples showing scoping behavior
  • Demonstrate how arrow functions differ from regular functions, especially regarding `this` binding and when you cannot use them (object methods, constructors, generators)
  • Show how to use destructuring to extract nested properties and rename variables in a single statement
  • Discuss when to use dynamic imports and how code splitting improves application performance
  • Explain the difference between spread and rest operators — spread expands collections, rest collects them

Cheat Sheet

Variables

const for constants, let for reassignable, never var

Arrow Functions

const fn = (params) => expression — lexical this, no own arguments

Destructuring

const { a, b: renamed } = obj; const [x, , z] = arr;

Spread

[...arr] expands arrays; { ...obj } expands objects; creates shallow copies

Rest

...args collects remaining params into an array; used in function signatures and destructuring

Template Literals

Hello, ${name} — interpolation, multi-line, tagged functions

Modules

export const/value; import { name } from './module.js'; import('./lazy.js') for dynamic

Optional Chaining

user?.address?.city — returns undefined instead of throwing on null/undefined

Nullish Coalescing

value ?? default — uses default only for null/undefined, not 0 or empty string