Modern Syntax

Learn the core ES6+ syntax features that modern JavaScript code is written with: let/const, arrow functions, template literals, destructuring, spread/rest, optional chaining, nullish coalescing, and default parameters.

ECMAScript 2015 (ES6) and the yearly editions since then reshaped how everyday JavaScript is written. This lesson is a fast, practical tour of the syntax you will see in essentially every modern codebase — knowing it fluently is a prerequisite for reading real-world JavaScript.

let and const: block-scoped variables

Before ES6, the only way to declare a variable was var, which is function-scoped and gets "hoisted" in ways that surprise beginners. ES6 introduced let and const, both of which are block-scoped — meaning a variable declared inside { } (an if-block, a for-loop, etc.) does not leak outside that block.

let declares a variable that can be reassigned later. const declares a variable that cannot be reassigned after its initial value is set. const does NOT make the value immutable, though — if the value is an object or array, its contents can still be changed; only the binding (the variable name pointing to that value) is locked.

Modern style guides recommend using const by default and switching to let only when you know the variable's value needs to change.

Arrow functions

Arrow functions (=>) are a shorter syntax for writing functions, and they behave differently from regular functions in one important way: they do not have their own this — they inherit this from the surrounding (lexical) scope. This makes them especially convenient for callbacks inside methods and class code.

A single-expression arrow function can omit both the curly braces and the return keyword (an "implicit return"). With multiple statements, you need braces and an explicit return.

• const add = (a, b) => a + b; // implicit return • const square = x => x * x; // parentheses optional with exactly one parameter • const greet = () => { console.log("hi"); }; // block body, no return value

Template literals

Template literals use backticks (` `) instead of quotes and support two useful features: embedded expressions with ${ }, and real multi-line strings without needing \n` or string concatenation.

Template literals fully replace the old pattern of building strings with +, and they make interpolated output far more readable.

Destructuring

Destructuring lets you unpack values from arrays or properties from objects directly into named variables in a single statement, instead of accessing them one at a time.

Array destructuring unpacks by position: const [first, second] = [10, 20];. Object destructuring unpacks by property name: const { name, age } = person; — and you can rename while destructuring with const { name: fullName } = person;, and provide fallback values with const { role = "guest" } = user;.

Spread and rest

The ... syntax has two opposite jobs depending on where it's used. As spread, it expands an array or object out into individual elements/properties — useful for copying arrays ([...arr]), merging objects ({...a, ...b}), or passing an array as individual function arguments (Math.max(...numbers)).

As rest, it does the opposite: it gathers multiple remaining items into a single array, most commonly in function parameters (function sum(...nums) { }) or destructuring (const [first, ...rest] = arr;).

Optional chaining and nullish coalescing

Optional chaining (?.) safely accesses a nested property without throwing if an intermediate value is null or undefined — it short-circuits and returns undefined instead of crashing. user?.address?.city reads city only if user and user.address both exist.

Nullish coalescing (??) returns its right-hand value only when the left-hand value is specifically null or undefined — unlike ||, it does NOT treat 0, "", or false as "missing". const count = input ?? 10; keeps 0 if input is 0, whereas input || 10 would incorrectly replace it with 10.

Default parameters

Default parameters let a function parameter fall back to a specified value when no argument (or undefined) is passed for it: function greet(name = "Guest") { return Hello, ${name}; }. This removes the old pattern of manually checking if (name === undefined) name = "Guest"; inside the function body.

Quick reference

Prefer const over let; use let only when reassignment is genuinely needed; avoid var in modern code. ?. and ?? are both ES2020 features — supported in all current browsers and Node versions. Rest parameters must be the last parameter in a function's parameter list.

// variables
let count = 0;
const PI = 3.14159;

// arrow function
const multiply = (a, b) => a * b;

// template literal
const msg = `Total: ${count} items`;

// destructuring
const { name, age } = user;
const [first, ...restOfArray] = list;

// spread
const merged = { ...defaults, ...overrides };

// optional chaining + nullish coalescing
const city = user?.address?.city ?? "Unknown";

// default parameter
function greet(name = "Guest") {
  return `Hi, ${name}!`;
}

Code Examples

Object destructuring pulls name and age straight out of user into their own variables, and the template literal interpolates them into a sentence. Expected result: Maya is 28 years old.

const { name, age } = user; unpacks two properties in a single line.

This combines default parameters with destructuring: the second parameter is destructured directly in the function signature, and = {} guards against calling the function with no second argument at all. Expected result: dev01 (member) — active dev02 (admin) — active

{ role = "member", active = true } = {} destructures with fallbacks and defaults to an empty object if omitted.

This realistic example chains together optional chaining (safe access into a possibly-missing shipping object), nullish coalescing (fallback defaults), array destructuring with rest, spread (to copy the original order into the returned object), and reduce (covered elsewhere) to total the prices. Expected result: { id: 1, items: [... ], shippingCity: 'no address on file', firstItem: 'Mouse', otherItemCount: 1, total: 30 } order.shipping?.address?.city ?? "no address on file" never throws even if shipping is entirely missing.

const user = { name: "Maya", age: 28 };
const { name, age } = user;
console.log(`${name} is ${age} years old.`);
function buildProfile(username, { role = "member", active = true } = {}) {
  return `${username} (${role}) — ${active ? "active" : "inactive"}`;
}

console.log(buildProfile("dev01"));
console.log(buildProfile("dev02", { role: "admin" }));
function summarizeOrder(order) {
  const shippingCity = order.shipping?.address?.city ?? "no address on file";
  const items = order.items ?? [];
  const [firstItem, ...otherItems] = items;
  const total = items.reduce((sum, item) => sum + item.price, 0);

  return {
    ...order,
    shippingCity,
    firstItem: firstItem?.name ?? "none",
    otherItemCount: otherItems.length,
    total
  };
}

console.log(summarizeOrder({ id: 1, items: [{ name: "Mouse", price: 20 }, { name: "Pad", price: 10 }] }));

Important Rules

Use const by default; use let only for variables that are genuinely reassigned; avoid var.

Arrow functions do not have their own this, arguments, or super — do not use them as object methods that rely on this, or as constructors.

?? checks specifically for null/undefined, while || also treats 0, "", NaN, and false as falsy — pick the one that matches your intent.

Rest parameters/elements must come last in a parameter list or destructuring pattern.

Where this shows up

Nearly all modern frontend frameworks (React, Vue, Svelte) and Node.js backend code are written almost entirely in this syntax. Destructuring props, spreading state updates, optional chaining into API responses, and arrow-function callbacks are everyday patterns you will use in essentially every JavaScript file you write on EngiLipi's coding tracks.

const total = input || 100; // input is 0 const total = input ?? 100;

|| treats 0 as falsy and would incorrectly replace a legitimate value of 0 with the fallback 100; ?? only falls back on null or undefined, so a real 0 is preserved.

const obj = { method: () => { console.log(this.name); } } const obj = { method() { console.log(this.name); } }

An arrow function used as an object method does not bind its own this, so this.name inside it refers to the outer scope's this, not obj — use a regular method shorthand instead.

[Multiple Choice] What does const count = 0 ?? 5; evaluate count to? A. 5 B. 0 C. undefined D. It throws an error

B. 0 — ?? only falls back to the right-hand side when the left-hand side is null or undefined. 0 is neither, so count stays 0.

[True / False] Arrow functions have their own this binding, separate from the surrounding scope. A. True B. False

B. False — False. Arrow functions do not bind their own this — they use this from the enclosing lexical scope, which is exactly why they are unsuitable as object methods that need to reference the object itself.

Write a function formatUser(user) that takes an object possibly shaped like { name, contact: { email } }. Use destructuring, optional chaining, and nullish coalescing to return a template-literal string like "Name: Alex, Email: none on file" when contact or email is missing.

function formatUser({ name, contact } = {}) { const email = contact?.email ?? "none on file"; return Name: ${name ?? "Unknown"}, Email: ${email}; }

Destructure name directly from the parameter, but access email safely with user.contact?.email ?? "none on file".