ES6+ Features
Classes, modules, template literals, destructuring, spread/rest, default parameters, dynamic imports, optional chaining, and nullish coalescing.
13 questions
JuniorTheoryVery commonWhat is destructuring assignment, and how does it let you swap two variables?
What is destructuring assignment, and how does it let you swap two variables?
Destructuring unpacks values out of arrays or properties out of objects into distinct variables in one expression. Array form [a, b] = arr binds by position; object form {x, y} = obj binds by matching property name. To swap two variables you write [a, b] = [b, a]: the right side builds a temporary array, then its elements are assigned back, so no extra temp variable is needed.
Common mistakes
- ✗Thinking array destructuring binds by name rather than by position
- ✗Believing a swap with
[a, b] = [b, a]still needs a temporary variable - ✗Assuming destructuring mutates or removes keys from the source
Follow-up questions
- →How does object destructuring decide which variable each value goes into?
- →What value does a destructured variable get if the source slot is missing?
JuniorTheoryVery commonWhat does the spread ... operator do in array literals, object literals, and call arguments?
What does the spread ... operator do in array literals, object literals, and call arguments?
Spread ... expands an iterable or object's contents in place. In an array literal [...iterable] it lays out each element, so [...'ab'] yields ['a', 'b']; in a call f(...arr) it passes each element as a separate argument; in an object literal {...obj} it copies own enumerable properties (a shallow copy). It reads from a source — unlike rest ..., which collects multiple items into one.
Common mistakes
- ✗Confusing spread (expands one into many) with rest (collects many into one)
- ✗Believing
{...obj}deep-clones rather than making a shallow copy - ✗Thinking spread only works on arrays, not strings or other iterables
Follow-up questions
- →What is the difference between spread
...and rest...syntax? - →Is
{...obj}a shallow or a deep copy of the object?
JuniorTheoryCommonHow do you declare an ES6 class, and how do you create an instance from it?
How do you declare an ES6 class, and how do you create an instance from it?
Use the class keyword with a name, an optional constructor for per-instance setup, and method definitions directly in the body (no commas, no function keyword). The constructor runs when you call new ClassName(args), which builds a fresh object, binds it to this, and returns it. Methods written in the body are shared, not copied per instance.
Common mistakes
- ✗Separating class members with commas like an object literal — class bodies use no commas
- ✗Calling a
classwithoutnew, which throws aTypeError - ✗Thinking body methods are copied onto each instance rather than shared
Follow-up questions
- →What happens if you call a
classconstructor without thenewkeyword? - →Where do the methods you write in the class body actually live?
JuniorTheoryCommonWhat are default function parameters in ES6, and when does the default actually apply?
What are default function parameters in ES6, and when does the default actually apply?
Default parameters let you write function f(x = 10) so a parameter gets a fallback value when the argument is missing. The default applies only when the argument is undefined — either omitted entirely or passed explicitly as undefined. Other falsy values like 0, '', null, or false are real arguments and suppress the default. The default expression is evaluated lazily, once per call, only when needed.
Common mistakes
- ✗Thinking any falsy argument like
0or''triggers the default — onlyundefineddoes - ✗Believing an explicit
undefinedargument bypasses the default - ✗Assuming the default expression is evaluated once at definition rather than per call
Follow-up questions
- →Does passing
nullto a parameter with a default trigger the default value? - →Can a later default parameter reference an earlier parameter's value?
JuniorTheoryCommonHow do import and export work in ES modules, and what scope do module bindings have?
How do import and export work in ES modules, and what scope do module bindings have?
An ES module exposes bindings with export (named, or one default) and pulls them in elsewhere with import. Anything not exported stays private to the file — module scope is the top level, not the global scope, so top-level declarations do not become global variables. Each module runs once, has its own scope, and is always in strict mode automatically, with no 'use strict' directive needed.
Common mistakes
- ✗Thinking top-level module declarations become global variables
- ✗Believing a module re-executes on each
importrather than running once - ✗Forgetting that modules are always in strict mode automatically
Follow-up questions
- →What is the difference between a
defaultexport and a namedexport? - →How many times does a module's top-level code run if several files import it?
MiddleTheoryCommonHow do extends, super, static, and private #fields work in ES6 class inheritance?
How do extends, super, static, and private #fields work in ES6 class inheritance?
class B extends A makes B a subclass of A. In B's constructor you must call super(args) before using this, which runs the parent constructor; super.method() invokes a parent method. static members belong to the class itself, not instances, and are inherited by subclasses. A #field is truly private — accessible only inside the declaring class body, invisible to subclasses and outside code, and not the same as a naming convention.
Common mistakes
- ✗Using
thisbefore callingsuper()in a subclass constructor - ✗Thinking
staticmembers are per-instance or not inherited by subclasses - ✗Treating a
#fieldas a convention that subclasses or outside code can read
Follow-up questions
- →Why must
super()run beforethisis touched in a subclass constructor? - →Can a subclass access a parent class's
#privatefield?
MiddleTheoryCommonWhat is the difference between for...of and for...in loops in JavaScript?
What is the difference between for...of and for...in loops in JavaScript?
for...of iterates the values of an iterable (Array, String, Map, Set, etc.), so on ['a','b'] it yields 'a' then 'b'. for...in iterates the enumerable string property keys of any object, including inherited ones and extra properties added to an array, so on an array it yields index strings like '0', '1' plus any custom keys. Use for...of for ordered values, for...in for object keys.
Common mistakes
- ✗Using
for...inon an array expecting values — it yields key strings - ✗Forgetting
for...inalso visits inherited enumerable properties - ✗Believing
for...ofworks on any plain object, not only iterables
Follow-up questions
- →Why does
for...inover an array yield string keys rather than numbers? - →How can you loop a plain object's entries with
for...of?
MiddleTheoryCommonWhat does the nullish coalescing operator ?? do, and how does it differ from ||?
What does the nullish coalescing operator ?? do, and how does it differ from ||?
a ?? b returns a unless a is null or undefined, in which case it returns b. It differs from ||, which returns the right side for any falsy left operand — 0, '', NaN, and false included. So 0 ?? 5 is 0 but 0 || 5 is 5. This makes ?? the correct choice for defaulting only on missing values while keeping legitimate falsy ones like 0 or ''.
Common mistakes
- ✗Treating
??and||as interchangeable, ignoring how||reacts to0and'' - ✗Thinking
??falls back only onundefined, not onnull - ✗Using
||to default a numeric value, wrongly replacing a valid0
Follow-up questions
- →What does
0 ?? 'x'evaluate to versus0 || 'x'? - →Why must
??be parenthesized when mixed with&&or||?
MiddleTheoryCommonWhat does the optional chaining operator ?. do when an intermediate reference is nullish?
What does the optional chaining operator ?. do when an intermediate reference is nullish?
?. reads a property, calls a method, or indexes safely: if the value on its left is null or undefined, the whole expression short-circuits and evaluates to undefined instead of throwing. So a?.b.c returns undefined when a is nullish, skipping the rest of the chain. Forms include obj?.prop, obj?.[key], and fn?.(). It only guards against nullish — other falsy values are accessed normally.
Common mistakes
- ✗Thinking
?.short-circuits on any falsy value rather than onlynull/undefined - ✗Believing
?.guards the whole chain rather than just the access it precedes - ✗Assuming it returns
nullinstead ofundefinedon short-circuit
Follow-up questions
- →How do you supply a fallback other than
undefinedafter an optional chain? - →Does
a?.b.cprotect againstbbeing nullish, or onlya?
MiddleTheoryOccasionalWhat is dynamic import(), and how does it differ from a static import statement?
What is dynamic import(), and how does it differ from a static import statement?
Dynamic import('./mod') is a function-like form that loads a module at runtime and returns a promise resolving to the module namespace object. Unlike a static top-level import, it can run conditionally, inside functions, with a specifier computed at runtime, and you await it or chain .then(). Bundlers use it for code splitting and lazy loading, so a chunk is fetched only when actually needed rather than upfront.
Common mistakes
- ✗Thinking
import()returns the module directly rather than a promise - ✗Believing dynamic imports cannot use a runtime-computed specifier
- ✗Assuming it loads everything eagerly, missing its code-splitting purpose
Follow-up questions
- →How do you read a named export off the object that
import()resolves to? - →Why does dynamic
import()enable code splitting that staticimportdoes not?
SeniorTheoryOccasionalIn what sense are ES6 classes syntactic sugar over prototypes, and how do they still differ?
In what sense are ES6 classes syntactic sugar over prototypes, and how do they still differ?
A class desugars to a constructor function plus prototype wiring: methods written in the body live on Constructor.prototype, shared by instances, exactly like manual prototype assignment. But classes are not pure sugar — their methods are non-enumerable (so they don't show up in for...in), a class declaration is not hoisted in a usable way (referencing it before declaration is a ReferenceError, unlike a function), and the whole body always runs in strict mode.
Common mistakes
- ✗Thinking class methods are own enumerable properties visible in
for...in - ✗Believing a
classdeclaration is hoisted and usable before its line - ✗Assuming classes are pure sugar with no strict-mode or enumerability differences
Follow-up questions
- →Why does referencing a class before its declaration throw rather than give
undefined? - →How can you confirm a class method is non-enumerable in practice?
SeniorTheoryOccasionalHow do nested destructuring, inline defaults, rest, aliasing, and parameter destructuring combine?
How do nested destructuring, inline defaults, rest, aliasing, and parameter destructuring combine?
Destructuring composes recursively: const {a: {b}} = obj reaches into nested objects, aliasing renames with {x: y} (binding y), and = value supplies a default applied only when the slot is undefined. Rest ...rest gathers the remaining array elements or object properties into one binding (object rest must be last). You can destructure directly in a parameter list, e.g. function f({x = 1, y} = {}), combining defaults at both the property and the whole-parameter level.
Common mistakes
- ✗Thinking
{x: y}bindsxrather thany— the right name is the binding - ✗Believing inline destructuring defaults fire on any falsy value, not just
undefined - ✗Placing object rest anywhere but last in the pattern
Follow-up questions
- →What happens with
function f({x} = {})whenf()is called with no argument? - →Why must the rest element be the last item in a destructuring pattern?
SeniorTheoryOccasionalWhat are ESM live bindings and static structure, and how does ESM differ from CommonJS?
What are ESM live bindings and static structure, and how does ESM differ from CommonJS?
ESM imports are live read-only bindings: an importer sees the exporter's current value, so a variable updated in the module reflects on the import side, unlike CommonJS, which copies the value at require time. ESM's import/export are static — resolvable before execution — which enables tree-shaking. It also supports top-level await. CommonJS (require/module.exports) is dynamic, synchronous, and value-copying, loaded at call time rather than parsed up front.
Common mistakes
- ✗Thinking ESM imports copy values like CommonJS rather than being live bindings
- ✗Believing ESM
import/exportis dynamic rather than statically analyzable - ✗Assuming imported bindings are writable by the importing module
Follow-up questions
- →Why does static
import/exportstructure enable tree-shaking butrequiredoes not? - →How does a CommonJS value copy differ in practice from an ESM live binding?