Quick Answer

Use const and let instead of var, arrow functions for callbacks, destructuring to pull values out of objects and arrays, spread to copy and merge, and optional chaining with nullish coalescing to handle missing values safely.

let and const, and the loop bug var causes

The clearest demonstration of why var was replaced:

var fns = [];
for (var i = 0; i < 3; i++) fns.push(() => i);
console.log(fns.map(f => f()));   // [3, 3, 3]

let fns2 = [];
for (let j = 0; j < 3; j++) fns2.push(() => j);
console.log(fns2.map(f => f())); // [0, 1, 2]

var is function-scoped, so all three closures capture the same i, which is 3 by the time they run. let is block-scoped and creates a fresh binding each iteration, so each closure captures its own value.

This is a classic interview question and a real bug that used to appear constantly in loops attaching event handlers. Default to const, use let when you must reassign, and treat var as legacy.

Note that const prevents reassignment, not mutation — const arr = [1,2]; arr.push(3); is legal, because the binding still points at the same array.

Destructuring

const [a, b, ...rest] = [1, 2, 3, 4, 5];
console.log(a, b, rest);        // 1 2 [3, 4, 5]

const user = { name: "Asha", city: "Pune" };
const { name, city: town } = user;
console.log(name, town);       // Asha Pune

Arrays destructure by position, objects by property name. city: town renames on the way out, which is useful when the incoming name would clash with something you already have.

It is used most in function parameters, where it documents what the function needs:

function greet({ name, city = "unknown" }) {
  return `${name} from ${city}`;
}

A caller reading that signature immediately knows which fields matter, and city has a default if absent.

Spread and rest

const user = { name: "Asha", city: "Pune" };
const merged = { ...user, marks: 91 };
console.log(JSON.stringify(merged));
// {"name":"Asha","city":"Pune","marks":91}

Three dots mean "expand this here". It copies arrays and objects, merges them, and passes arrays as separate arguments. Later keys win, so { ...defaults, ...overrides } is the standard way to apply options.

One caveat worth knowing: this is a shallow copy. Nested objects are still shared between the copy and the original, so mutating a nested property changes both. For genuinely independent copies use structuredClone(obj).

The same syntax in a parameter list collects arguments instead: function sum(...nums) gives you a real array, unlike the old arguments object.

Arrow functions and template literals

const greet = (n) => `Hello ${n}`;
console.log(greet("Ravi"));           // Hello Ravi

console.log(((x = 10) => x * 2)());   // 20

const arr = [1, 2, 3];
console.log(arr.map(n => n * 2));     // [2, 4, 6]
console.log(arr.reduce((s, n) => s + n, 0));  // 6

Backticks allow interpolation with ${...} and real multi-line strings, which removes most string concatenation.

Arrow functions are shorter, and they differ from regular functions in one important way: they do not have their own this. They inherit it from the surrounding scope, which is exactly what you want in a callback inside a class method — and exactly what you do not want for an object method that needs to refer to the object itself.

Optional chaining and nullish coalescing

These two fix real bugs rather than just shortening code.

const user = { name: "Asha", city: "Pune" };
console.log(user?.address?.pin);   // undefined, no crash

Without ?., reading user.address.pin when address is missing throws "Cannot read properties of undefined" — one of the most common errors in JavaScript. Optional chaining stops and returns undefined instead.

console.log(0 ?? 99);   // 0
console.log(0 || 99);   // 99

This distinction matters. || falls back for any falsy value, so a legitimate 0, empty string or false gets replaced by the default. ?? falls back only for null and undefined. A quantity of zero or a stored setting of false being silently overwritten is a genuinely nasty bug, and ?? prevents it.

While on surprising behaviour: 0 == "0" is true but 0 === "0" is false, and typeof null is "object". Use === always.

Frequently Asked Questions

Should I still use var? No, in new code. let and const are block-scoped and avoid the closure and hoisting surprises var causes. You will still meet var in older codebases, so it is worth understanding.
What is the difference between || and ?? || falls back on any falsy value including 0, empty string and false. ?? falls back only on null and undefined. Use ?? when zero or false are legitimate values.
When should I not use an arrow function? For object methods that need their own this, and for constructors, which arrows cannot be. Arrows inherit this from the enclosing scope, which is ideal for callbacks and wrong for methods.
Does spread make a deep copy? No, it is shallow. Nested objects remain shared between the copy and the original. Use structuredClone for a fully independent copy.
Do I need Babel to use these features? Not for modern browsers and Node, which support all of the above. Transpilation only matters when you must support genuinely old environments, which is increasingly rare.