ForgeFrontend — Prepare, Practice, Crack
Secure checkout
Lifetime access
Instant PDF download
Free updates forever

Prepare · Practice · Crack

ES6 Interview Questions and Answers

Short answer

ES6 (ES2015) introduced let and const, arrow functions, template literals, destructuring, spread and rest, default parameters, classes, modules, promises, Map and Set. Interviews rarely ask for the list — they ask what each one changed, and where the new syntax behaves differently from the old.

FeatureReplacedThe reason it exists
let / constvarBlock scope and no accidental redeclaration
Arrow functionsfunction + .bind(this)Lexical this, shorter callbacks
Template literalsString concatenationInterpolation and multi-line
DestructuringManual property accessExtract many values in one line
Spread / restapply, arguments, concatOne syntax for expanding and collecting
Default parametersa = a || fallbackOnly applies to undefined, not falsy
ClassesPrototype boilerplateFamiliar syntax over the same prototypes
ModulesIIFEs and script tagsReal imports, static analysis, tree shaking
Map / SetObjects and arraysAny key type, real size, no prototype clashes
What each headline feature actually replaced.

What is the difference between let, const and var?

`var` is function-scoped and hoisted with an initial value of `undefined`, so reading it before its declaration is legal and returns `undefined`. `let` and `const` are block-scoped and hoisted without initialisation, which puts them in the temporal dead zone — accessing them early throws a ReferenceError rather than silently giving you a wrong value. `const` additionally prevents reassignment of the binding.

The three behaviours that get testedjavascript
1console.log(a); var a = 1;
2// → undefined      hoisted and initialised
3
4console.log(b); let b = 2;
5// → ReferenceError: Cannot access 'b' before initialization
6
7const user = { name: "Arun" };
8user.name = "Karthikeyan";     // allowed — mutation
9console.log(user.name);
10// → Karthikeyan
11user = {};                     // not allowed — reassignment
12// → TypeError: Assignment to constant variable.

The point people miss is that `const` freezes the binding, not the value. A const object's properties are freely mutable, which is why `const` is safe to use as the default even for arrays and objects you intend to update immutably. Use `let` only where a value genuinely has to be reassigned, and treat any remaining `var` in a codebase as legacy.

How do arrow functions differ from normal functions?

Arrow functions are not just shorter syntax. They have no `this` of their own, so `this` resolves lexically to the enclosing scope; they have no `arguments` object; they cannot be used as constructors with `new`; and they have no `prototype` property. That lexical `this` is the whole reason they exist — it removes the `const self = this` and `.bind(this)` boilerplate that callbacks used to need.

Where each one is rightjavascript
1const timer = {
2  seconds: 0,
3  startBroken() {
4    setInterval(function () {
5      this.seconds++;          // `this` is not `timer` here
6    }, 1000);
7  },
8  startWorking() {
9    setInterval(() => {
10      this.seconds++;          // inherits `this` from startWorking
11    }, 1000);
12  },
13};
14
15// But as a METHOD, an arrow function is wrong:
16const bad = {
17  name: "x",
18  greet: () => `hi ${this.name}`,
19};
20console.log(bad.greet());
21// → hi undefined   `this` is the module/global scope, not `bad`

So the rule is simple to state and worth stating in an interview: use arrow functions for callbacks and anything that should inherit the surrounding `this`; use regular functions for object methods, for constructors, and for anything that needs its own `this` or `arguments`. Getting that distinction right in one sentence answers three follow-up questions before they are asked.

The interview framing to have ready is that arrow functions did not replace regular functions — they gave you a choice you did not previously have. Before ES6, every function got its own `this`, so callbacks needed `.bind(this)`, a `self` variable, or the little-known second argument some array methods accept. Arrow functions made "inherit the surrounding this" the default for the case where it is almost always what you want.

How does destructuring work, including defaults and renaming?

Destructuring extracts values from arrays by position and from objects by key, in a single statement. You can rename as you extract, supply defaults, nest patterns, and use rest to collect whatever is left. It is one of the features that most changes how everyday code reads, which is why interviews test the edge cases rather than the basics.

Renaming, defaults, nesting and swappingjavascript
1const user = { id: 1, profile: { city: "Chennai" }, tags: ["a", "b"] };
2
3const { id: userId, profile: { city }, role = "student" } = user;
4console.log(userId, city, role);
5// → 1 Chennai student      role defaulted, nothing named "role" existed
6
7const [first, ...rest] = user.tags;
8console.log(first, rest);
9// → a [ 'b' ]
10
11// Swap without a temp variable
12let x = 1, y = 2;
13[x, y] = [y, x];
14console.log(x, y);
15// → 2 1
16
17// Defaults apply ONLY to undefined, never to null or 0
18const { count = 10 } = { count: null };
19console.log(count);
20// → null

That last case is the one that catches people out and it applies to default parameters too. A default fires only when the value is `undefined` — passing `null`, `0`, `false` or an empty string uses the value you passed. This is deliberate and it is the difference from the old `x = x || fallback` idiom, which also replaced every falsy value and quietly broke on legitimate zeros.

Destructuring is also what makes React function components read the way they do. Writing `function Button({ label, variant = "primary", ...rest })` is object destructuring with a default and a rest collection, all in the parameter list — and the `rest` there is what gets spread onto the underlying element. Being able to name each part of that signature is a small thing that reads as fluency rather than pattern-matching.

What is the difference between spread and rest?

Same three dots, opposite directions, distinguished by where they appear. Rest collects remaining items into an array or object and only appears in a parameter list or on the left of a destructuring assignment. Spread expands an iterable into individual elements and appears in a call, an array literal or an object literal.

Collecting versus expandingjavascript
1// REST — collecting, on the left
2function sum(first, ...others) { return first + others.length; }
3console.log(sum(1, 2, 3, 4));
4// → 4          others = [2, 3, 4]
5
6const { id, ...restProps } = { id: 1, a: 2, b: 3 };
7console.log(restProps);
8// → { a: 2, b: 3 }
9
10// SPREAD — expanding, on the right
11console.log(Math.max(...[3, 9, 2]));
12// → 9          replaces Math.max.apply(null, arr)
13console.log([...[1, 2], ...[3]]);
14// → [ 1, 2, 3 ]
15console.log({ ...{ a: 1 }, b: 2 });
16// → { a: 1, b: 2 }
17
18// Both spreads here are SHALLOW — nested objects stay shared

One practical warning about spread that comes up in code review more than in interviews: order matters in object spread, and later keys win. `{ ...defaults, ...overrides }` behaves as you would hope, while the reverse silently discards the overrides. The same rule makes `{ ...props, className: x }` a deliberate override and `{ className: x, ...props }` an accident waiting to happen.

This is 1 of 80+ questions in the JavaScript Interview Kit

Get every question with detailed answers, follow-ups and real code — plus coding challenges and a last-minute revision sheet. One-time payment, instant access.

⚡ Get the JavaScript Interview Kit → ₹299

Are ES6 classes just syntactic sugar?

Mostly, but not entirely — and the exceptions are what get asked. A class still creates a constructor function with methods on its prototype, so the underlying model is unchanged. What differs is that class declarations are not hoisted in a usable way, class bodies always run in strict mode, methods are non-enumerable, and calling a class without `new` throws instead of silently misbehaving.

Same prototypes underneath, stricter rules on topjavascript
1class User {
2  #secret = "hidden";              // truly private field
3  static count = 0;
4
5  constructor(name) { this.name = name; User.count++; }
6  greet() { return `hi ${this.name}`; }
7}
8
9console.log(typeof User);
10// → function                       still a function
11console.log(Object.getPrototypeOf(new User("a")) === User.prototype);
12// → true
13
14User("x");
15// → TypeError: Class constructor User cannot be invoked without 'new'
16
17console.log(Object.keys(User.prototype));
18// → []                             methods are non-enumerable

Private fields are the newest part of that list and the one worth volunteering. A `#name` field is enforced by the language rather than by convention, so it is genuinely unreachable from outside the class — unlike the old underscore prefix, which was only ever a request. Accessing one from outside is a syntax error at parse time, not a runtime undefined, which is a meaningfully stronger guarantee.

What is the difference between ES modules and CommonJS?

ES modules use `import` and `export` and are statically analysable: the imports are known before the code runs, which is what makes tree shaking possible. CommonJS uses `require` and `module.exports`, resolves at runtime, and can therefore be called conditionally inside a function. ES module imports are also live bindings — if the exporting module reassigns the value, importers see the new one — whereas `require` copies the value at the moment it runs.

Static versus dynamic, and the live bindingjavascript
1// counter.mjs
2export let count = 0;
3export const inc = () => count++;
4
5// main.mjs
6import { count, inc } from "./counter.mjs";
7inc();
8console.log(count);
9// → 1        live binding — the import reflects the change
10
11// CommonJS copies the value:
12// const { count, inc } = require("./counter.js");
13// inc(); console.log(count);   → 0
14
15// Dynamic import gives you runtime loading in ESM too:
16const { default: chart } = await import("./heavy-chart.js");
17// → loads only when this line runs, which is how route splitting works

Modules also changed two things people forget to mention: module code is always strict mode, and top-level `this` is `undefined` rather than the global object. Both catch out scripts copied from an older codebase into a module without adjustment, and both are reasonable follow-ups if you claim familiarity with the difference.

A useful way to answer the modules question is to say what each one made possible rather than listing syntax. Static imports are why bundlers can drop unused exports, why your editor can rename a symbol across files reliably, and why a circular import is detectable at build time instead of producing a half-initialised object at runtime. Those consequences are what the interviewer is checking you understand.

When should you use Map and Set instead of objects and arrays?

Use a `Map` when keys are not strings, when insertion order matters, when you add and remove entries frequently, or when key names could collide with inherited properties. Use a `Set` for uniqueness. Objects remain the right choice for records with known fields and for anything you need to serialise to JSON, since neither Map nor Set survives `JSON.stringify`.

The differences that decide itjavascript
1const m = new Map();
2m.set(1, "number one").set("1", "string one");
3console.log(m.get(1), m.size);
4// → number one 2        keys keep their type
5
6const o = {};
7o[1] = "number one"; o["1"] = "string one";
8console.log(Object.keys(o));
9// → [ '1' ]             object keys are coerced to strings
10
11console.log(({}).constructor);
12// → [Function: Object]  inherited names can collide with your keys
13
14console.log([...new Set([1, 2, 2, 3])]);
15// → [ 1, 2, 3 ]         the standard dedupe
16
17console.log(JSON.stringify(new Map([["a", 1]])));
18// → {}                  Maps do not serialise

One more distinction worth having ready: `Set` gives you uniqueness by the same rules as `===`, with the single exception that it treats `NaN` as equal to itself. So a Set deduplicates primitives perfectly but does nothing for objects with identical contents, because those are still separate references. If someone asks you to dedupe an array of objects, the answer is a Map keyed on whatever field defines identity, not a Set.

What other ES6+ features come up?

The query says ES6 but interviewers usually mean modern JavaScript generally, so the newer additions are fair game. Optional chaining and nullish coalescing in particular appear constantly, because they replaced defensive code that was both verbose and subtly wrong.

The post-2015 additions worth knowingjavascript
1const user = { profile: null };
2
3console.log(user.profile?.city);
4// → undefined      no throw, unlike user.profile.city
5
6console.log(user.count ?? 10);
7// → 10             only null/undefined trigger the fallback
8console.log(0 ?? 10, 0 || 10);
9// → 0 10           ?? keeps a legitimate zero, || does not
10
11console.log(Object.entries({ a: 1 }).flat());
12// → [ 'a', 1 ]
13console.log([[1, [2]]].flat(2));
14// → [ 1, 2 ]
15console.log(Object.groupBy([1, 2, 3], n => n % 2 ? "odd" : "even"));
16// → { odd: [ 1, 3 ], even: [ 2 ] }

Two of those deserve emphasis because they changed everyday code the most. Optional chaining removed the long `a && a.b && a.b.c` guards that used to precede every nested read, and nullish coalescing fixed the bug those guards often introduced by treating a legitimate zero as missing. Generators and iterators round out the list and are worth a sentence even if you never write one. Any object implementing `Symbol.iterator` works with `for…of`, spread and destructuring — which is why those three all work on Maps, Sets and strings but not on plain objects. That single fact explains a lot of otherwise arbitrary-looking behaviour.

Is ES6 the same as ES2015?

Yes — ES6 was the working name and ES2015 is the published one, after TC39 moved to annual releases named by year. In interviews "ES6" is usually shorthand for modern JavaScript in general rather than that specific edition, so do not be surprised by questions about optional chaining or Object.entries.

Why do arrow functions not work as object methods?

Because they have no `this` of their own and inherit it from the surrounding scope, which for a method defined in an object literal is the module or global scope — not the object. Use shorthand method syntax instead, which behaves like a regular function and binds `this` to the receiver.

Does spread create a deep copy?

No, it is shallow. { ...obj } gives a new top-level object whose properties still reference the same nested objects and arrays, so mutating anything below the first level affects the original. Use structuredClone when you need genuine independence.

What is the temporal dead zone?

The window between entering a scope and executing a let or const declaration. The binding exists — it was hoisted — but has no value, so any access throws. It exists to turn a whole class of ordering mistakes into loud errors instead of silent undefined values.

When would you still use var?

In new code, never. Every behaviour var offers is either matched by let or is the behaviour you are trying to avoid. You still need to read it in older codebases, and knowing why it was replaced is the actual interview question.

Frequently asked questions

What is ES6 in JavaScript?
ES6, published as ES2015, was the largest revision of the language — adding let and const, arrow functions, template literals, destructuring, spread and rest, default parameters, classes, modules, promises, Map, Set and generators. In interviews the term is usually used loosely to mean modern JavaScript.
What is the difference between let, const and var?
var is function-scoped and hoisted as undefined. let and const are block-scoped and sit in the temporal dead zone until declared, so reading them early throws. const also blocks reassignment of the binding, though the object it points at can still be mutated.
What is the difference between arrow functions and regular functions?
Arrow functions have no own this, arguments, prototype or ability to be called with new. this resolves lexically to the enclosing scope, which makes them ideal for callbacks and wrong for object methods and constructors.
What is destructuring in JavaScript?
Syntax for extracting values from arrays by position or objects by key in one statement, with support for renaming, defaults, nesting and rest collection. Defaults apply only when the value is undefined, so null and 0 pass through unchanged.
What is the difference between spread and rest operators?
They use the same three dots and are told apart by position. Rest appears in a parameter list or on the left of a destructuring assignment and collects the remainder into an array or object. Spread appears in a call or literal on the right and expands an iterable into individual items.
Are ES6 classes just syntactic sugar over prototypes?
Largely, but not entirely. A class still produces a constructor function with methods on its prototype, so the model is unchanged. Classes additionally run in strict mode, make methods non-enumerable, are not usefully hoisted, and throw if called without new.
What is the difference between ES modules and CommonJS?
ES modules use import/export, are resolved statically before execution — which enables tree shaking — and provide live bindings to the exported values. CommonJS uses require/module.exports, resolves at runtime so it can be called conditionally, and copies values at the moment of the call.
When should you use a Map instead of an object?
When keys are not strings, when insertion order matters, when you add and delete frequently, or when key names might collide with inherited properties like constructor. Objects are better for fixed-shape records and are the only option if the data must be JSON-serialised.
What is the difference between ?? and ||?
|| falls back for every falsy value including 0, empty string and false. ?? falls back only for null and undefined. Use ?? for defaults where zero or an empty string is a legitimate value, which is most of the time.
What is optional chaining?
The ?. operator, which short-circuits to undefined instead of throwing when the value before it is null or undefined. It works on properties, array indices and function calls, and it replaced long chains of && guards.
Is ES6 supported by all browsers?
Yes, in every browser released in the last several years. Transpiling with Babel is now about supporting old enterprise environments rather than the mainstream web, and most build setups target a much more recent baseline than they did five years ago.
What are generators used for?
Functions that can pause and resume with yield, producing values on demand. In application code they are rare, but they underpin async iteration, infinite sequences, and libraries like redux-saga. Knowing that any object with Symbol.iterator works with for…of and spread is the more useful takeaway.

This is 1 of 80+ questions in the JavaScript Interview Kit

Get every question with detailed answers, follow-ups and real code — plus coding challenges and a last-minute revision sheet. One-time payment, instant access.

⚡ Get the JavaScript Interview Kit → ₹299
Written by Arun Karthikeyan · Last updated

Full kit

JavaScript Interview Kit · ₹299

Get it →