Skip to main content
Why does JavaScript code written in 2015 look so different from code written today? How do developers write such concise, readable code without all the boilerplate?
// The old way (pre-ES6)
var city = user && user.address && user.address.city;  // undefined if missing
var copy = arr.slice();
var merged = Object.assign({}, obj1, obj2);

// The modern way
const city = user?.address?.city;  // undefined if missing
const copy = [...arr];
const merged = { ...obj1, ...obj2 };
The answer is ES6 (ECMAScript 2015) and the yearly updates that followed. These additions didn’t just add features. They transformed how we write JavaScript. Features like destructuring, arrow functions, and optional chaining are now everywhere: in tutorials, open-source projects, and job interviews. According to the State of JS 2023 survey, features like destructuring and arrow functions have reached near-universal adoption, with over 95% of respondents using them regularly.
What you’ll learn in this guide:
  • Arrow functions and how they handle this differently
  • Destructuring objects and arrays to extract values cleanly
  • Spread operator (...) for copying and merging
  • Rest parameters for collecting function arguments
  • Template literals for string interpolation
  • Optional chaining (?.) to avoid “cannot read property of undefined”
  • Nullish coalescing (??) vs logical OR (||)
  • Logical assignment operators (??=, ||=, &&=)
  • Default parameters for functions
  • Enhanced object literals (shorthand syntax)
  • Map, Set, and Symbol basics
  • The for...of loop for iterating values
Prerequisite: This guide touches on let, const, and var briefly. For a deep dive into how they differ (block scope, hoisting, temporal dead zone), read our Scope and Closures guide first.

A Quick Note on let, const, and var

Before ES6, var was the only way to declare variables. Now we have let and const, which were introduced in the ECMAScript 2015 specification and behave differently:
Featurevarletconst
ScopeFunctionBlockBlock
HoistingYes (undefined)Yes (TDZ)Yes (TDZ)
RedeclarationAllowedErrorError
ReassignmentAllowedAllowedError
// var is function-scoped (can cause bugs)
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// Output: 3, 3, 3

// let is block-scoped (each iteration gets its own i)
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// Output: 0, 1, 2
The modern rule: Use const by default. Use let when you need to reassign. Avoid var. For the full explanation of scope, hoisting, and the temporal dead zone, see Scope and Closures.

Arrow Functions

Arrow functions provide a shorter syntax for writing functions. But the real difference is how they handle this. As MDN documents, arrow functions do not have their own this, arguments, super, or new.target bindings — they inherit these from the enclosing lexical scope.

The Syntax

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

// Arrow function variations
const add = (a, b) => a + b;              // Implicit return (single expression)
const add = (a, b) => { return a + b; };  // Block body (explicit return needed)
const square = x => x * x;                // Single param: parentheses optional
const greet = () => 'Hello!';             // No params: parentheses required

Arrow Functions and this

Here’s the big difference: arrow functions don’t have their own this. They inherit this from the surrounding code (lexical scope).
// Problem with regular functions
const counter = {
  count: 0,
  start: function() {
    setInterval(function() {
      this.count++;  // 'this' is NOT the counter object!
      console.log(this.count);
    }, 1000);
  }
};
counter.start();  // NaN, NaN, NaN...

// Solution with arrow functions
const counter = {
  count: 0,
  start: function() {
    setInterval(() => {
      this.count++;  // 'this' IS the counter object
      console.log(this.count);
    }, 1000);
  }
};
counter.start();  // 1, 2, 3...
For a complete exploration of this binding rules, see this, call, apply and bind.

When NOT to Use Arrow Functions

Arrow functions aren’t always the right choice:
// ❌ DON'T use as object methods
const user = {
  name: 'Alice',
  greet: () => {
    console.log(`Hi, I'm ${this.name}`);  // 'this' is NOT user!
  }
};
user.greet();  // "Hi, I'm undefined"

// ✓ USE regular function for methods
const user = {
  name: 'Alice',
  greet() {
    console.log(`Hi, I'm ${this.name}`);
  }
};
user.greet();  // "Hi, I'm Alice"

// ❌ DON'T use as constructors
const Person = (name) => { this.name = name; };
new Person('Alice');  // TypeError: Person is not a constructor

// ❌ Arrow functions don't have their own 'arguments'
const logArgs = () => console.log(arguments);  // ReferenceError (use ...rest instead)

The Object Literal Trap

Returning an object literal requires parentheses:
// ❌ WRONG - curly braces are interpreted as function body
const createUser = name => { name: name };
console.log(createUser('Alice'));  // undefined (it's a labeled statement!)

// ❌ ALSO WRONG - adding more properties causes a SyntaxError
// const createUser = name => { name: name, active: true };  // SyntaxError!

// ✓ CORRECT - wrap object literal in parentheses
const createUser = name => ({ name: name, active: true });
console.log(createUser('Alice'));  // { name: 'Alice', active: true }

Destructuring Assignment

Destructuring lets you unpack values from arrays or properties from objects into distinct variables.

Array Destructuring

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

// Basic destructuring
const [first, second, third] = colors;
console.log(first);   // "red"
console.log(second);  // "green"

// Skip elements with empty slots
const [primary, , tertiary] = colors;
console.log(tertiary);  // "blue"

// Default values
const [a, b, c, d = 'yellow'] = colors;
console.log(d);  // "yellow"

// Rest pattern (collect remaining elements)
const [head, ...tail] = colors;
console.log(head);  // "red"
console.log(tail);  // ["green", "blue"]
Swap variables without a temp:
let x = 1;
let y = 2;

[x, y] = [y, x];

console.log(x);  // 2
console.log(y);  // 1

Object Destructuring

const user = {
  name: 'Alice',
  age: 25,
  address: {
    city: 'Portland',
    country: 'USA'
  }
};

// Basic destructuring
const { name, age } = user;
console.log(name);  // "Alice"

// Rename variables
const { name: userName, age: userAge } = user;
console.log(userName);  // "Alice"

// Default values
const { name, role = 'guest' } = user;
console.log(role);  // "guest"

// Nested destructuring
const { address: { city } } = user;
console.log(city);  // "Portland"

// Rest pattern
const { name, ...rest } = user;
console.log(rest);  // { age: 25, address: { city: 'Portland', country: 'USA' } }

Destructuring in Function Parameters

This pattern is everywhere in modern JavaScript:
// Without destructuring
function createUser(options) {
  const name = options.name;
  const age = options.age || 18;
  const role = options.role || 'user';
  return { name, age, role };
}

// With destructuring
function createUser({ name, age = 18, role = 'user' }) {
  return { name, age, role };
}

// With default for the entire parameter (prevents error if called with no args)
function greet({ name = 'Guest' } = {}) {
  return `Hello, ${name}!`;
}

greet();                  // "Hello, Guest!"
greet({ name: 'Alice' }); // "Hello, Alice!"

Common Mistake: Destructuring to Existing Variables

let name, age;

// ❌ WRONG - JavaScript thinks {} is a code block
{ name, age } = user;  // SyntaxError

// ✓ CORRECT - wrap in parentheses
({ name, age } = user);
┌─────────────────────────────────────────────────────────────────────────┐
│                    DESTRUCTURING VISUALIZED                              │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│   ARRAY DESTRUCTURING                 OBJECT DESTRUCTURING               │
│   ───────────────────                 ────────────────────               │
│                                                                          │
│   const [a, b, c] = [1, 2, 3]        const {x, y} = {x: 10, y: 20}      │
│                                                                          │
│   [1, 2, 3]                           { x: 10, y: 20 }                   │
│    │  │  │                              │       │                        │
│    │  │  └──► c = 3                     │       └──► y = 20              │
│    │  └─────► b = 2                     └──────────► x = 10              │
│    └────────► a = 1                                                      │
│                                                                          │
│   Position matters!                   Property name matters!             │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

Spread and Rest Operators

The ... syntax does two different things depending on context:
ContextNameWhat It Does
Function call, array/object literalSpreadExpands an iterable into individual elements
Function parameter, destructuringRestCollects multiple elements into an array

Spread Operator

Spreading arrays:
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];

// Combine arrays
const combined = [...arr1, ...arr2];
console.log(combined);  // [1, 2, 3, 4, 5, 6]

// Copy an array
const copy = [...arr1];
console.log(copy);  // [1, 2, 3]

// Insert elements
const withMiddle = [0, ...arr1, 4];
console.log(withMiddle);  // [0, 1, 2, 3, 4]

// Pass array as function arguments
console.log(Math.max(...arr1));  // 3
Spreading objects:
const defaults = { theme: 'light', fontSize: 14 };
const userPrefs = { theme: 'dark' };

// Merge objects (later properties override earlier)
const settings = { ...defaults, ...userPrefs };
console.log(settings);  // { theme: 'dark', fontSize: 14 }

// Copy and update
const updated = { ...user, name: 'Bob' };

// Copy an object (shallow!)
const copy = { ...original };

Rest Parameters

// Collect all arguments into an array
function sum(...numbers) {
  return numbers.reduce((total, n) => total + n, 0);
}
console.log(sum(1, 2, 3, 4));  // 10

// Collect remaining arguments
function logFirst(first, ...rest) {
  console.log('First:', first);
  console.log('Rest:', rest);
}
logFirst('a', 'b', 'c', 'd');
// First: a
// Rest: ['b', 'c', 'd']
Rest in destructuring:
// Arrays
const [first, second, ...others] = [1, 2, 3, 4, 5];
console.log(others);  // [3, 4, 5]

// Objects
const { id, ...otherProps } = { id: 1, name: 'Alice', age: 25 };
console.log(otherProps);  // { name: 'Alice', age: 25 }

The Shallow Copy Trap

Spread creates shallow copies. Nested objects are still referenced:
const original = {
  name: 'Alice',
  address: { city: 'Portland' }
};

const copy = { ...original };

// Modifying nested object affects both!
copy.address.city = 'Seattle';
console.log(original.address.city);  // "Seattle" — oops!

// For deep copies, use structuredClone (modern) or JSON (with limitations)
const deepCopy = structuredClone(original);

Template Literals

Template literals use backticks (`) instead of quotes and support string interpolation and multi-line strings.

Basic Interpolation

const name = 'Alice';
const age = 25;

// Old way
const message = 'Hello, ' + name + '! You are ' + age + ' years old.';

// Template literal
const message = `Hello, ${name}! You are ${age} years old.`;

// Expressions work too
const price = 19.99;
const tax = 0.1;
const total = `Total: $${(price * (1 + tax)).toFixed(2)}`;
console.log(total);  // "Total: $21.99"

Multi-line Strings

// Old way (awkward)
const html = '<div>\n' +
  '  <h1>Title</h1>\n' +
  '  <p>Content</p>\n' +
  '</div>';

// Template literal (natural)
const html = `
  <div>
    <h1>${title}</h1>
    <p>${content}</p>
  </div>
`;

Tagged Templates

Tagged templates let you process template literals with a function:
function highlight(strings, ...values) {
  return strings.reduce((result, str, i) => {
    const value = values[i] ? `<mark>${values[i]}</mark>` : '';
    return result + str + value;
  }, '');
}

const query = 'JavaScript';
const count = 42;

const result = highlight`Found ${count} results for ${query}`;
console.log(result);
// "Found <mark>42</mark> results for <mark>JavaScript</mark>"
Tagged templates power libraries like styled-components (CSS-in-JS) and GraphQL query builders.

Optional Chaining (?.)

Optional chaining lets you safely access nested properties without checking each level for null or undefined.

The Problem It Solves

const user = {
  name: 'Alice',
  // address is undefined
};

// Old way (verbose and error-prone)
const city = user && user.address && user.address.city;

// Old way (slightly better)
const city = user.address ? user.address.city : undefined;

// Modern way
const city = user?.address?.city;  // undefined (no error!)

Three Syntax Forms

// Property access
const city = user?.address?.city;

// Bracket notation (for dynamic keys)
const prop = 'address';
const value = user?.[prop]?.city;

// Function calls (only call if function exists)
const result = user?.getName?.();

Short-Circuit Behavior

When the left side is null or undefined, evaluation stops immediately and returns undefined:
const user = null;

// Without optional chaining
user.address.city;  // TypeError: Cannot read property 'address' of null

// With optional chaining
user?.address?.city;  // undefined (evaluation stops at user)

Don’t Overuse It

// ❌ BAD - if user should always exist, you're hiding bugs
function processUser(user) {
  return user?.name?.toUpperCase();  // Silently returns undefined
}

// ✓ GOOD - fail fast when data is invalid
function processUser(user) {
  if (!user) throw new Error('User is required');
  return user.name.toUpperCase();
}

// ✓ GOOD - use when null/undefined is a valid possibility
const displayName = apiResponse?.data?.user?.displayName ?? 'Anonymous';

Nullish Coalescing (??)

The nullish coalescing operator returns the right-hand side when the left-hand side is null or undefined. This is different from ||, which returns the right-hand side for any falsy value.

?? vs ||

Valuevalue || 'default'value ?? 'default'
null'default''default'
undefined'default''default'
0'default'0
'''default'''
false'default'false
NaN'default'NaN
// Problem with ||
const count = response.count || 10;
// If response.count is 0, this incorrectly returns 10!

// Solution with ??
const count = response.count ?? 10;
// Only returns 10 if count is null or undefined
// Returns 0 if count is 0 (which is what we want)

// Common use cases
const port = process.env.PORT ?? 3000;
const username = inputValue ?? 'guest';
const timeout = options.timeout ?? 5000;

Combining with Optional Chaining

These two operators work great together:
const city = user?.address?.city ?? 'Unknown';
const count = response?.data?.items?.length ?? 0;

Logical Assignment Operators

ES2021 added assignment versions of logical operators:
// Nullish coalescing assignment
user.name ??= 'Anonymous';
// Only assigns if user.name is null or undefined
// (short-circuits: skips assignment if value already exists)

// Logical OR assignment
options.debug ||= false;
// Only assigns if options.debug is falsy

// Logical AND assignment
user.lastLogin &&= new Date();
// Only assigns if user.lastLogin is truthy
// Practical example: initializing config
function configure(options = {}) {
  options.retries ??= 3;
  options.timeout ??= 5000;
  options.cache ??= true;
  return options;
}

configure({});                    // { retries: 3, timeout: 5000, cache: true }
configure({ retries: 0 });        // { retries: 0, timeout: 5000, cache: true }
configure({ timeout: null });     // { retries: 3, timeout: 5000, cache: true }

Default Parameters

Default parameters let you specify fallback values for function arguments.
// Old way
function greet(name, greeting) {
  name = name || 'Guest';
  greeting = greeting || 'Hello';
  return `${greeting}, ${name}!`;
}

// Modern way
function greet(name = 'Guest', greeting = 'Hello') {
  return `${greeting}, ${name}!`;
}

greet();                    // "Hello, Guest!"
greet('Alice');             // "Hello, Alice!"
greet('Alice', 'Hi');       // "Hi, Alice!"

Only undefined Triggers Defaults

function example(value = 'default') {
  return value;
}

example(undefined);  // "default"
example(null);       // null (NOT "default"!)
example(0);          // 0
example('');         // ''
example(false);      // false

Defaults Can Reference Earlier Parameters

function createRect(width, height = width) {
  return { width, height };
}

createRect(10);       // { width: 10, height: 10 }
createRect(10, 20);   // { width: 10, height: 20 }

Defaults Can Be Expressions

function createId(prefix = 'id', timestamp = Date.now()) {
  return `${prefix}_${timestamp}`;
}

// Date.now() is called each time (not once at definition)
createId();  // "id_1704067200000"
createId();  // "id_1704067200001" (different!)

Enhanced Object Literals

ES6 added several shortcuts for creating objects.

Property Shorthand

When the property name matches the variable name:
const name = 'Alice';
const age = 25;

// Old way
const user = { name: name, age: age };

// Shorthand
const user = { name, age };
console.log(user);  // { name: 'Alice', age: 25 }

Method Shorthand

// Old way
const calculator = {
  add: function(a, b) {
    return a + b;
  }
};

// Shorthand
const calculator = {
  add(a, b) {
    return a + b;
  },
  
  // Works with async too
  async fetchData(url) {
    const response = await fetch(url);
    return response.json();
  }
};

Computed Property Names

Use expressions as property names:
const key = 'dynamicKey';
const index = 0;

const obj = {
  [key]: 'value',
  [`item_${index}`]: 'first item',
  ['get' + 'Name']() {
    return this.name;
  }
};

console.log(obj.dynamicKey);  // "value"
console.log(obj.item_0);      // "first item"
Practical example:
function createState(key, value) {
  return {
    [key]: value,
    [`set${key.charAt(0).toUpperCase() + key.slice(1)}`](newValue) {
      this[key] = newValue;
    }
  };
}

const state = createState('count', 0);
console.log(state);  // { count: 0, setCount: [Function] }
state.setCount(5);
console.log(state.count);  // 5

Map, Set, and Symbol

ES6 introduced new built-in data structures and a new primitive type.

Map

Map is a collection of key-value pairs where keys can be any type (not just strings).
const map = new Map();

// Any value can be a key
const objKey = { id: 1 };
map.set('string', 'value1');
map.set(42, 'value2');
map.set(objKey, 'value3');

console.log(map.get(objKey));  // "value3"
console.log(map.size);         // 3
console.log(map.has('string')); // true

// Iteration (maintains insertion order)
for (const [key, value] of map) {
  console.log(key, value);
}

// Convert to/from arrays
const arr = [...map];  // [['string', 'value1'], [42, 'value2'], ...]
const map2 = new Map([['a', 1], ['b', 2]]);
When to use Map vs Object:
Use CaseObjectMap
Keys are strings
Keys are any type
Need insertion order✓ (string keys)
Need size property
Frequent add/removeSlowerFaster
JSON serialization

Set

Set is a collection of unique values.
const set = new Set([1, 2, 3, 3, 3]);
console.log(set);  // Set { 1, 2, 3 }

set.add(4);
set.delete(1);
console.log(set.has(2));  // true
console.log(set.size);    // 3

// Remove duplicates from array
const numbers = [1, 2, 2, 3, 3, 3];
const unique = [...new Set(numbers)];
console.log(unique);  // [1, 2, 3]

// Set operations
const a = new Set([1, 2, 3]);
const b = new Set([2, 3, 4]);

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

Symbol

Symbol is a primitive type for unique identifiers.
// Every Symbol is unique
const sym1 = Symbol('description');
const sym2 = Symbol('description');
console.log(sym1 === sym2);  // false

// Use as object keys (hidden from normal iteration)
const ID = Symbol('id');
const user = {
  name: 'Alice',
  [ID]: 12345
};

console.log(user[ID]);        // 12345
console.log(Object.keys(user));  // ['name'] (Symbol not included)

// Well-known Symbols customize object behavior
const collection = {
  items: [1, 2, 3],
  [Symbol.iterator]() {
    let i = 0;
    return {
      next: () => ({
        value: this.items[i],
        done: i++ >= this.items.length
      })
    };
  }
};

for (const item of collection) {
  console.log(item);  // 1, 2, 3
}

for…of Loop

The for…of loop iterates over iterable objects (arrays, strings, Maps, Sets, etc.).
// Arrays
const colors = ['red', 'green', 'blue'];
for (const color of colors) {
  console.log(color);  // "red", "green", "blue"
}

// Strings
for (const char of 'hello') {
  console.log(char);  // "h", "e", "l", "l", "o"
}

// Maps
const map = new Map([['a', 1], ['b', 2]]);
for (const [key, value] of map) {
  console.log(key, value);  // "a" 1, "b" 2
}

// Sets
const set = new Set([1, 2, 3]);
for (const num of set) {
  console.log(num);  // 1, 2, 3
}

// With destructuring
const users = [
  { name: 'Alice', age: 25 },
  { name: 'Bob', age: 30 }
];
for (const { name, age } of users) {
  console.log(`${name} is ${age}`);
}

for…of vs for…in

for...offor...in
Iterates overValuesKeys (property names)
Works withIterables (Array, String, Map, Set)Objects
Array indicesUse .entries()Yes (as strings)
const arr = ['a', 'b', 'c'];

for (const value of arr) {
  console.log(value);  // "a", "b", "c" (values)
}

for (const index in arr) {
  console.log(index);  // "0", "1", "2" (keys as strings)
}

Key Takeaways

The key things to remember about modern JavaScript syntax:
  1. Arrow functions inherit this from the enclosing scope. Don’t use them as object methods or constructors.
  2. Destructuring extracts values from arrays (by position) and objects (by property name). Use it for cleaner function parameters.
  3. Spread (...) expands, rest (...) collects. Same syntax, different contexts.
  4. ?? checks for null/undefined only. Use it when 0, '', or false are valid values. Use || when you want fallback for any falsy value.
  5. Optional chaining (?.) prevents “cannot read property of undefined” errors. Don’t overuse it or you’ll hide bugs.
  6. Template literals use backticks and support ${expressions} and multi-line strings.
  7. Default parameters trigger only on undefined, not null or other falsy values.
  8. Map keys can be any type, maintain insertion order, and have a .size property. Use Map when Object doesn’t fit.
  9. Set stores unique values. Spread a Set to deduplicate an array: [...new Set(arr)].
  10. for...of iterates values, for...in iterates keys. Use for...of for arrays.

Test Your Knowledge

Answer:
  • 0 ?? 'default' returns 0
  • 0 || 'default' returns 'default'
The nullish coalescing operator (??) only returns the right side for null or undefined. Since 0 is neither, it returns 0.The logical OR (||) returns the right side for any falsy value. Since 0 is falsy, it returns 'default'.
// Use ?? when 0 is a valid value
const count = response.count ?? 10;

// Use || when any falsy value should trigger default
const name = input || 'Anonymous';
Answer:Wrap the object literal in parentheses:
// ❌ WRONG - braces interpreted as function body
const createUser = name => { name, active: true };
// Returns undefined

// ✓ CORRECT - parentheses make it an expression
const createUser = name => ({ name, active: true });
// Returns { name: '...', active: true }
Without parentheses, JavaScript interprets { } as a function body block, not an object literal. The parentheses force it to be treated as an expression.
Answer:They use the same ... syntax but do opposite things:Spread expands an iterable into individual elements:
const arr = [1, 2, 3];
console.log(...arr);        // 1 2 3 (individual values)
const copy = [...arr];      // [1, 2, 3] (new array)
Math.max(...arr);           // 3 (arguments spread)
Rest collects multiple elements into an array:
function sum(...numbers) {  // Collects all args
  return numbers.reduce((a, b) => a + b, 0);
}

const [first, ...rest] = [1, 2, 3, 4];
// first = 1, rest = [2, 3, 4]
Rule of thumb: In a function definition or destructuring pattern, it’s rest. Everywhere else (function calls, array/object literals), it’s spread.
Answer:Arrow functions don’t have their own this. They inherit this from the enclosing lexical scope, which is usually the global object or undefined (in strict mode).
const user = {
  name: 'Alice',
  
  // ❌ Arrow function - 'this' is NOT the user object
  greetArrow: () => {
    console.log(`Hi, I'm ${this.name}`);
  },
  
  // ✓ Regular function - 'this' IS the user object
  greetRegular() {
    console.log(`Hi, I'm ${this.name}`);
  }
};

user.greetArrow();   // "Hi, I'm undefined"
user.greetRegular(); // "Hi, I'm Alice"
Use regular functions (or method shorthand) for object methods when you need access to this.
Answer:Use array destructuring:
let a = 1;
let b = 2;

[a, b] = [b, a];

console.log(a);  // 2
console.log(b);  // 1
This creates a temporary array [b, a] (which is [2, 1]), then destructures it back into a and b in the new order.
Answer:It returns 'Unknown'.Here’s the evaluation:
  1. user?.addressuser is null, so optional chaining short-circuits and returns undefined
  2. undefined?.city — This never runs because we already got undefined
  3. undefined ?? 'Unknown'undefined is nullish, so we get 'Unknown'
const user = null;
const city = user?.address?.city ?? 'Unknown';
console.log(city);  // "Unknown"

// Without optional chaining, this would throw:
// TypeError: Cannot read property 'address' of null

Frequently Asked Questions

ES6 (ECMAScript 2015) was the largest single update to JavaScript, introducing let/const, arrow functions, classes, template literals, destructuring, modules, Promises, and more. It transformed JavaScript from a scripting language into a modern programming language. Since ES6, the TC39 committee releases yearly specification updates with incremental features.
Arrow functions have shorter syntax and don’t bind their own this, arguments, super, or new.target. They inherit this from the enclosing scope, making them ideal for callbacks and closures. Regular functions are needed for object methods, constructors, and any context requiring dynamic this binding. See MDN’s arrow function reference for the full list of differences.
Destructuring is syntax for extracting values from arrays or properties from objects into distinct variables. Instead of const name = user.name, you write const { name } = user. It works with nested objects, arrays, default values, and renamed variables. The ECMAScript specification defines it as a destructuring assignment pattern.
Both use the ... syntax but in different contexts. The spread operator expands an iterable into individual elements: [...arr] copies an array. The rest operator collects multiple elements into a single array: function(...args) gathers all arguments. Spread appears in expressions; rest appears in function parameters and destructuring patterns.
Optional chaining (?.) safely accesses deeply nested properties without checking each level for null or undefined. Instead of user && user.address && user.address.city, you write user?.address?.city. It returns undefined if any part of the chain is nullish. Introduced in ES2020, it pairs naturally with the nullish coalescing operator (??) for providing defaults.


Reference

Articles

Videos

Last modified on February 17, 2026