What happens when this code runs?
let a = 1;
const b = 2;
a = 3;
b = 4;
console.log(a, b);let allows reassignment but const does not. Reassigning b throws TypeError before any console.log runs.
Each JavaScript question with the correct answer and a clear explanation.
let a = 1;
const b = 2;
a = 3;
b = 4;
console.log(a, b);let allows reassignment but const does not. Reassigning b throws TypeError before any console.log runs.
console.log(0 == "0");
console.log(0 === "0");Loose equality (==) coerces types, so 0 == "0" is true. Strict equality (===) compares without coercion, so 0 === "0" is false.
const arr = [1, 2, 3];
arr[10] = 99;
console.log(arr.length);Assigning to index 10 expands the array. Length becomes 11; indices 3–9 are sparse (empty slots).
const s = "hello";
console.log(s[0], s.length);Strings support index access (s[0] = 'h') and have a length property. "hello" has 5 characters.
const obj = { name: "Ada" };
const { name, age = 30 } = obj;
console.log(name, age);Destructuring with a default value uses the default only when the property is undefined. age isn't on obj, so it falls back to 30.
console.log(typeof null);typeof null returns "object" — a long-standing JavaScript quirk preserved for compatibility.
console.log(NaN === NaN);
console.log(Number.isNaN(NaN));NaN is the only value not equal to itself. Use Number.isNaN (or Object.is) to detect it.
console.log(null == undefined);
console.log(null === undefined);
console.log(null == 0);== treats null and undefined as equal to each other but to nothing else (no coercion to 0).
const a = [1, 2, 3, 4, 5];
a.length = 2;
console.log(a);Assigning a smaller value to length truncates the array — extra elements are removed entirely.
const a = new Array(3);
const b = Array.of(3);
console.log(a.length, b.length);new Array(3) treats a single number as the length (creates 3 empty slots). Array.of(3) treats it as a single element.
let s = "hello";
s[0] = "H";
console.log(s);Strings are immutable. The assignment fails silently (in strict mode it throws) — the string stays "hello".
const x = 10;
console.log(`x is ${x > 5 ? "big" : "small"}`);Template literals evaluate any expression inside ${...}, including ternaries.
const o = { 2: "a", 1: "b", "x": "c" };
console.log(Object.keys(o));Integer-like keys come first in ascending order, then string keys in insertion order.
function fn(...args) { return args.length; }
const arr = [1, 2, 3];
console.log(fn(...arr));Spread (...arr) at the call site passes elements as individual arguments; rest (...args) collects them in the function.
const x = 5;
const label = x > 0 ? "+" : x < 0 ? "-" : "0";
console.log(label);Nested ternaries are evaluated right-to-left. x > 0 is true, so the first branch wins: "+".
const a = 0 || "fallback";
const b = "" || "fallback";
const c = "value" || "fallback";
console.log(a, b, c);|| returns the first truthy operand or the last one. 0 and "" are falsy, so the fallback wins for a and b.
const a = 0 ?? "fallback";
const b = null ?? "fallback";
const c = undefined ?? "fallback";
console.log(a, b, c);?? only falls back on null or undefined — 0 is preserved (unlike with ||).
console.log(Boolean(""), Boolean("0"), Boolean([]), Boolean({}));Falsy values: "", 0, NaN, null, undefined, false. Non-empty strings AND any object (including [] and {}) are truthy.
console.log(1 + "2" + 3);
console.log(1 + 2 + "3");+ is left-associative. Once a string operand appears, the rest concatenate. "1"+"2"+3 → "123"; 1+2 then +"3" → "33".
console.log(parseInt("08"));
console.log(parseInt("08", 10));Modern parseInt defaults to radix 10 for non-0x strings (the old octal default for leading 0 was dropped in ES5+). Always pass a radix anyway for clarity.
const a = Array.from({ length: 3 }, (_, i) => i * 2);
console.log(a);Array.from with a length-only object plus a mapper is the canonical way to build an array of N computed values.
const a = [1, NaN, 3];
console.log(a.indexOf(NaN), a.includes(NaN));indexOf uses === (NaN never matches itself). includes uses SameValueZero, which considers NaN equal to NaN.
const name = "Ada", age = 36;
const user = { name, age, role: "admin" };
console.log(user.name, user.role);Property shorthand { name } is equivalent to { name: name } — uses the variable's value.
const o = { x: 1 };
o.x = 2;
o.y = 3;
console.log(o.x, o.y);const protects the binding (you can't reassign o), but the object itself is still mutable.
const arr = ["a", "b", "c"];
arr.extra = "x";
const ofs = []; for (const v of arr) ofs.push(v);
const ins = []; for (const k in arr) ins.push(k);
console.log(ofs.length, ins.length);for...of iterates the array's indexed values (3). for...in walks all enumerable string keys, including the added "extra" (4).
function makeCounter() {
let n = 0;
return () => ++n;
}
const c = makeCounter();
c(); c();
console.log(c());The closure captures n. Each call increments it; the third invocation returns 3.
console.log(typeof a);
var a = 1;
console.log(typeof b);var declarations are hoisted (initialised to undefined). typeof on an undeclared name b returns 'undefined' without throwing.
const obj = {
name: "Ada",
greet() { return `hi ${this.name}`; }
};
const g = obj.greet;
console.log(g());Detached methods lose their this. In a module / strict context this is undefined and accessing .name throws; in non-strict browser globals it's window, where window.name defaults to '' but typically reads as undefined for our purposes — most modern environments give 'hi undefined'.
const a = [1, 2];
const b = [3, 4];
const c = [...a, ...b, 5];
console.log(c.length, c[2]);Spread flattens both arrays into c, then appends 5: [1, 2, 3, 4, 5]. Length 5, index 2 is 3.
function f(a, b = 2, ...rest) {
return [a, b, rest];
}
console.log(f(1, undefined, 3, 4));Default values fire on undefined, so b becomes 2. The rest parameter collects remaining args into an array.
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}var is function-scoped — all callbacks close over the same i, which is 3 by the time they run.
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}let creates a fresh binding per iteration, so each callback closes over its own i.
const obj = {
name: "Ada",
greet: () => `hi ${this?.name}`,
};
console.log(obj.greet());Arrow functions don't bind their own this — it stays whatever it was in the enclosing scope (here, top-level → undefined or globalThis).
class Counter {
n = 0;
inc() { this.n++; }
}
const c = new Counter();
const f = c.inc;
f();
console.log(c.n);Class bodies are strict mode by default. The detached f() has this === undefined, so this.n++ throws TypeError.
const a = { x: { y: 1 } };
const b = { ...a };
b.x.y = 99;
console.log(a.x.y);Spread does a shallow copy — nested objects are shared by reference. Use structuredClone for a deep copy.
console.log(typeof foo);
console.log(typeof bar);
function foo() {}
var bar = function() {};Function declarations are hoisted with their body. Function expressions assigned to var hoist only the var binding (initially undefined).
const x = (function() {
return 42;
})();
console.log(x);An IIFE — defined and immediately invoked. The trailing () calls the function, so x receives the return value.
function add(a, b) { return a + b; }
const add5 = add.bind(null, 5);
console.log(add5(3));bind partially applies arguments after this. add5 has a fixed at 5, so add5(3) computes 5 + 3.
const sum = [1, 2, 3, 4]
.filter(n => n % 2 === 0)
.map(n => n * 10)
.reduce((acc, n) => acc + n, 0);
console.log(sum);filter → [2,4]; map ×10 → [20,40]; reduce + → 60.
const o = {
_x: 1,
get x() { return this._x; },
set x(v) { this._x = v * 2; },
};
o.x = 5;
console.log(o.x);The setter doubles the value before storing it (this._x = 10). The getter then returns it as-is.
const data = { user: { profile: { age: 25 } } };
const { user: { profile: { age } } } = data;
console.log(age);Nested destructuring follows the structure of the object — only age is bound as a variable.
let calls = 0;
function id() { calls++; return calls; }
function f(x = id()) { return x; }
f(); f(10); f();
console.log(calls);Default-param expressions only evaluate when the arg is undefined. f(10) skips the default, so id() runs only twice.
console.log([10, 1, 2].sort());Default sort is lexicographic — items are compared as strings. Use .sort((a,b) => a-b) for numeric order.
const a = { x: 1, y: 2 };
const b = { ...a, x: 99 };
console.log(b);Properties listed later override earlier ones. Order matters: { x: 99, ...a } would let a.x win.
const u = { name: "Ada", contact: null };
const email = u?.contact?.email ?? "n/a";
console.log(email);?. short-circuits on null/undefined, returning undefined; ?? then falls back to "n/a".
const fns = [];
for (let i = 0; i < 3; i++) {
fns.push(() => i);
}
console.log(fns[0](), fns[1](), fns[2]());let creates a fresh binding per iteration, so each closure captures its own i.
const o = {
vals: [1, 2, 3],
sum() {
let total = 0;
this.vals.forEach(function(v) { total += v; });
return total;
}
};
console.log(o.sum());total is captured via closure (not this), so the inner function's this doesn't matter — the sum is 6.
function head(first, ...rest) { return [first, rest.length]; }
console.log(head("a", "b", "c", "d"));rest collects everything after first into an array of 3.
const a = Symbol("id");
const b = Symbol("id");
console.log(a === b, a.description === b.description);Every Symbol() call yields a unique value. The optional description is just a label and isn't used for equality.
const m = new Map();
m.set(2, "a"); m.set(1, "b"); m.set("x", "c");
console.log([...m.keys()]);Map preserves insertion order for any key type — unlike plain objects, where integer-like keys are reordered ascending.
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
console.log("4");Synchronous code first (1, 4). Then microtasks (Promise.then → 3). Then macrotasks (setTimeout → 2).
function* gen() {
yield 1;
yield 2;
return 3;
}
const g = gen();
console.log(g.next().value, g.next().value, g.next().value);Each next() returns { value, done }. The third call hits the return statement, exposing the returned 3 with done=true.
class A {
greet() { return "A"; }
}
class B extends A {
greet() { return "B" + super.greet(); }
}
console.log(new B().greet());B.greet returns 'B' concatenated with super.greet() (A's implementation = 'A'), giving 'BA'.
const target = { x: 1 };
const handler = {
get(obj, prop) { return prop in obj ? obj[prop] : 0; }
};
const p = new Proxy(target, handler);
console.log(p.x, p.y);The Proxy's get trap returns the real value when the prop exists, else 0. p.x → 1, p.y → 0 (y is not on target).
const obj = {};
console.log(Object.getPrototypeOf(obj) === Object.prototype);
console.log(Object.getPrototypeOf(Object.prototype));Plain objects inherit from Object.prototype. Object.prototype itself sits at the top of the chain, so its prototype is null.
function* g() {
yield 1;
yield 2;
return 3;
}
const it = g();
console.log(it.next().value, it.next().value, it.next().value, it.next().value);yield emits values; return is the final value (with done=true). After that, .next() yields { value: undefined, done: true }.
const target = { x: 1 };
const proxy = new Proxy(target, {
get(t, p) { return p in t ? t[p] : "default"; }
});
console.log(proxy.x, proxy.y);The get trap intercepts property reads. Existing properties go through; missing ones return "default".
const range = {
from: 1, to: 3,
[Symbol.iterator]() {
let cur = this.from, end = this.to;
return {
next: () => cur <= end ? { value: cur++, done: false } : { value: undefined, done: true }
};
}
};
console.log([...range]);Implementing [Symbol.iterator] makes the object iterable — spread invokes it and collects values until done is true.
const o = { x: 1 };
console.log(Reflect.get(o, "x"), Reflect.has(o, "y"));Reflect mirrors operators — Reflect.get is like obj[p]; Reflect.has is like "p" in obj.
const wm = new WeakMap();
let key = {};
wm.set(key, "hello");
console.log(wm.get(key));
key = null;
console.log(wm.has({}));WeakMap keys must be objects compared by reference. {} is a fresh object — not the original key — so .has returns false.
console.log("A");
Promise.resolve().then(() => console.log("B"));
Promise.resolve().then(() => console.log("C"));
console.log("D");Sync code runs first (A, D), then microtasks in FIFO order (B, C).
console.log("1");
queueMicrotask(() => console.log("2"));
Promise.resolve().then(() => console.log("3"));
console.log("4");Both queueMicrotask and Promise.then schedule microtasks. They run after sync code, in scheduling order.
class Box {
#value;
constructor(v) { this.#value = v; }
get() { return this.#value; }
}
const b = new Box(42);
console.log(b.get(), b["#value"]);Private fields (#) are accessible only inside the class body; b["#value"] reads a normal string property that doesn't exist.
class A {
constructor() { this.tag = "A"; }
}
class B extends A {
constructor() {
super();
this.tag = "B";
}
}
console.log(new B().tag);super() runs A's constructor (sets this.tag = "A"); the next assignment overwrites it to "B".
class Animal {
static [Symbol.hasInstance]() { return true; }
}
console.log({} instanceof Animal);instanceof delegates to Symbol.hasInstance — a custom implementation can return whatever it likes.
const o = Object.create(null);
console.log(Object.getPrototypeOf(o));
console.log(o.toString);Object.create(null) makes a prototype-less object — there's no Object.prototype.toString to inherit.
function show() { return [this.tag, ...arguments]; }
console.log(show.call({ tag: "A" }, 1, 2));
console.log(show.apply({ tag: "B" }, [3, 4]));call takes args as a list; apply takes them as an array. Both set this to the first argument.
const proto = {
get hi() { return `hi ${this.name}`; }
};
const o = Object.create(proto);
o.name = "Ada";
console.log(o.hi);Getters defined on the prototype run with this set to the receiver — o, which has name = "Ada".
const o = {};
Object.defineProperty(o, "x", { value: 1, configurable: false });
try {
Object.defineProperty(o, "x", { value: 2 });
console.log(o.x);
} catch (e) {
console.log("error");
}A non-configurable data descriptor still allows changing value (and toggling writable from true to false). Other attributes are locked.
const o = { a: 1 };
Object.defineProperty(o, "b", { value: 2, enumerable: false });
console.log(Object.keys(o), Object.getOwnPropertyNames(o));Object.keys returns enumerable own properties only. getOwnPropertyNames returns all own string-keyed properties.
function makeIter(arr) {
let i = 0;
return {
next: () => i < arr.length
? { value: arr[i++], done: false }
: { value: undefined, done: true },
[Symbol.iterator]() { return this; },
};
}
console.log([...makeIter(["a","b"])]);An iterator that returns itself from [Symbol.iterator] is also iterable, so spread can consume it.
async function* nums() {
yield 1; yield 2;
}
const out = [];
for await (const n of nums()) out.push(n);
console.log(out);for-await-of awaits each yielded value, so out collects the unwrapped numbers.
const safe = new Proxy({ x: 1 }, {
has(t, p) { return p === "x"; }
});
console.log("x" in safe, "y" in safe);The has trap intercepts the in operator. We allow only "x".
const o = Object.freeze({ x: 1, nested: { y: 2 } });
o.x = 99;
o.nested.y = 99;
console.log(o.x, o.nested.y);Object.freeze is shallow — top-level x is locked, but the nested object is still mutable.
class A {}
A.prototype.constructor = function() { return { tag: "fake" }; };
console.log(new A().tag);new A() calls A directly (the original class constructor), not the constructor property on the prototype.
async function f() { return 42; }
const r = f();
console.log(r);Calling an async function returns a Promise. Since the function returns synchronously, the Promise is already fulfilled with 42 (printed as Promise { 42 }).
Promise.all([
Promise.resolve(1),
Promise.reject("err"),
Promise.resolve(3),
]).then((v) => console.log("ok", v))
.catch((e) => console.log("fail", e));Promise.all rejects on the first rejection — the catch fires with that reason, and the fulfilled values are discarded.
async function main() {
console.log("a");
await Promise.resolve();
console.log("b");
}
main();
console.log("c");Synchronous code in main runs first ('a'), then await yields control back. 'c' prints next, then the microtask resumes main and prints 'b'.
Promise.allSettled([
Promise.resolve(1),
Promise.reject("x"),
]).then((arr) => console.log(arr.map(r => r.status)));Promise.allSettled resolves with an array of { status, value | reason }. Status is 'fulfilled' or 'rejected'.
After each macrotask the engine empties the microtask queue before the next macrotask or rendering. setTimeout(0) is a macrotask and waits at least one cycle.
const a = new Promise(r => setTimeout(() => r("a"), 50));
const b = new Promise(r => setTimeout(() => r("b"), 10));
const winner = await Promise.race([a, b]);
console.log(winner);Promise.race resolves with whichever input settles first — b settles after 10ms, a after 50ms.
async function f() { return 5; }
const r = f();
console.log(r instanceof Promise, await r);An async function always returns a Promise — even if the body returns a plain value, it's wrapped.
async function f() {
try {
await Promise.reject(new Error("boom"));
} catch (e) {
return "caught:" + e.message;
}
}
console.log(await f());await on a rejected promise throws synchronously inside the async function — try/catch handles it.
async function f() {
throw new Error("x");
}
try {
f();
console.log("after call");
} catch (e) {
console.log("caught");
}An async function returning a rejection is NOT a synchronous throw — without await, the error becomes an unhandled rejection.
try {
await Promise.all([
Promise.resolve(1),
Promise.reject(new Error("nope")),
Promise.resolve(3),
]);
} catch (e) {
console.log("caught:", e.message);
}Promise.all rejects as soon as any input rejects — the rejection short-circuits the wait.
try {
await Promise.any([
Promise.reject("a"),
Promise.reject("b"),
]);
} catch (e) {
console.log(e.constructor.name, e.errors);
}Promise.any rejects with an AggregateError holding all rejection reasons only when every input rejects.
setTimeout(() => console.log("timeout"), 0);
Promise.resolve().then(() => console.log("microtask"));
console.log("sync");Sync runs first, then microtasks (Promise.then), then macrotasks (setTimeout).
const r = await Promise.resolve(1)
.then(v => v + 1)
.then(v => v * 10);
console.log(r);Each .then sees the previous return value: 1 → 2 → 20.
async function getX() { return 5; }
async function run() {
const x = getX();
console.log(typeof x.then, x + 1);
}
await run();Forgetting await leaves x as a Promise. x + 1 coerces it via toString() → "[object Promise]1".
async function run() {
const items = [1, 2, 3];
const out = [];
for (const n of items) {
out.push(await Promise.resolve(n * 2));
}
return out;
}
console.log(await run());await inside for…of runs sequentially. Each iteration waits for the previous promise — out gets the unwrapped values.
async function run() {
const items = [1, 2, 3];
return Promise.all(items.map(async n => n * 2));
}
console.log(await run());Map+async runs the async functions in parallel, each returning a Promise. Promise.all unwraps them all.
const out = await Promise.allSettled([
Promise.resolve(1),
Promise.reject("x"),
]);
console.log(out.map(r => r.status));Promise.allSettled never rejects — it returns objects of shape { status: 'fulfilled' | 'rejected', value | reason }.
function delay(ms) {
return new Promise(r => setTimeout(r, ms));
}
const start = Date.now();
await Promise.all([delay(50), delay(50), delay(50)]);
const elapsed = Date.now() - start;
console.log(elapsed >= 50 && elapsed < 200);All three delays run concurrently, so total time is roughly the longest single one (~50ms), not 150ms.
try {
await Promise.resolve(1).then(v => { throw new Error("oops"); });
} catch (e) {
console.log("caught:" + e.message);
}Throwing inside a .then handler converts the chain to a rejection, which await turns back into a sync throw.
async function* gen() { yield 1; yield 2; yield 3; }
const out = [];
for await (const n of gen()) out.push(n);
console.log(out);An async generator yields promise-wrapped values; for-await-of awaits each one.
console.log("before");
const p = new Promise((res) => {
console.log("executor");
res();
});
console.log("after");
await p;
console.log("done");The Promise executor runs synchronously when the Promise is constructed.
const thenable = { then(res) { res("hi"); } };
const out = await thenable;
console.log(out);await accepts any thenable. The engine calls .then(resolve, reject) and uses the resolved value.
// Imagine: window.addEventListener('unhandledrejection', e => console.log('unhandled'));
Promise.reject(new Error("boom"));A rejection without a .catch / await triggers the unhandledrejection event on window (or process in Node).
function defer() {
let resolve;
const promise = new Promise(r => { resolve = r; });
return { promise, resolve };
}
const d = defer();
setTimeout(() => d.resolve(42), 10);
console.log(await d.promise);Capturing the resolver lets you settle a Promise from outside the executor — the classic deferred pattern.
let a;
queueMicrotask(() => a = "qm");
Promise.resolve().then(() => a = "p");
await Promise.resolve();
console.log(a);Both schedule microtasks. They run in FIFO order: queueMicrotask first sets "qm", then the .then callback overwrites with "p".
const user = { name: "Ada", profile: null };
console.log(user?.profile?.email ?? "n/a");Optional chaining short-circuits at profile (null) yielding undefined. ?? falls back to 'n/a' because the left side is nullish.
console.log(0 ?? "fallback");
console.log("" ?? "fallback");
console.log(null ?? "fallback");?? falls back only on null or undefined. 0 and "" are kept as-is (unlike ||, which would replace them).
const fn = null;
console.log(fn?.());Optional call (?.()) short-circuits to undefined when the callee is null/undefined instead of throwing TypeError.
const entries = [["a", 1], ["b", 2]];
const obj = Object.fromEntries(entries);
console.log(obj.a, obj.b);Object.fromEntries inverts Object.entries — it builds an object from key/value pairs.
const a = { x: { y: 1 } };
const b = structuredClone(a);
b.x.y = 99;
console.log(a.x.y);structuredClone produces a deep copy. Mutating b.x.y doesn't affect a, so a.x.y stays 1. Available in modern Node and browsers without polyfills.
const obj = { greet: null };
console.log(obj.greet?.());
console.log(obj.missing?.());?.() short-circuits to undefined when the value before it is null or undefined — no error thrown.
const arr = [1];
const [a, b = 99, c = 100] = arr;
console.log(a, b, c);Array destructuring defaults fire on undefined — both b and c get their defaults.
const m = new Map([["a", 1], ["b", 2]]);
const o = Object.fromEntries(m);
console.log(o.a, o.b);Object.fromEntries accepts any iterable of [key, value] pairs — including Maps.
let a = "";
let b = "value";
a ||= "fallback";
b ||= "fallback";
console.log(a, b);||= assigns only when the left side is falsy. "" is falsy → a becomes "fallback"; "value" is truthy → b is unchanged.
let a = 0;
let b = null;
a ??= 99;
b ??= 99;
console.log(a, b);??= assigns only when the left side is null or undefined. 0 is preserved; null is replaced.
const big = 1_000_000;
console.log(big === 1000000);Numeric separators (_) are syntactic sugar — they're stripped at parse time and don't affect the value.
const a = 9007199254740993n;
const b = a + 1n;
console.log(b);BigInt handles arbitrary-size integers exactly. The trailing 'n' marks a BigInt literal.
const arr = [10, 20, 30];
console.log(arr.at(-1), arr.at(-2));Array.prototype.at supports negative indices, counting from the end.
console.log("a-b-c".replaceAll("-", "/"));String.prototype.replaceAll replaces every match without needing a global regex.
const o = { x: 1 };
console.log(Object.hasOwn(o, "x"), Object.hasOwn(o, "toString"));Object.hasOwn checks own properties only — "toString" is inherited from Object.prototype.
console.log([1, [2, [3, [4]]]].flat(2));flat(depth) flattens up to depth levels. flat(2) leaves the deepest [4] alone. Use flat(Infinity) for full flatten.
console.log([1, 2, 3].flatMap(n => [n, n * 10]));flatMap is map followed by flat(1) — useful for one-to-many transformations.
const winner = await Promise.any([
Promise.reject("a"),
Promise.resolve("b"),
Promise.resolve("c"),
]);
console.log(winner);Promise.any resolves with the first fulfilled value, ignoring rejections.
globalThis.__token = 42;
console.log(globalThis.__token);globalThis is the standardized handle on the global object — works in browser, Node, workers, and Deno alike.
// In an ES module:
// const data = await fetch("/api").then(r => r.json());
console.log("module loaded after data");Top-level await suspends the module's evaluation. Importing modules wait for it before running their own bodies.
const items = [1, 2, 3, 4];
const grouped = Object.groupBy(items, n => n % 2 === 0 ? "even" : "odd");
console.log(grouped.even, grouped.odd);Object.groupBy (ES2024) groups elements by the return value of the callback, returning a plain object keyed by group.
let target = { name: "Ada" };
const ref = new WeakRef(target);
console.log(ref.deref()?.name);WeakRef.deref() returns the target while it's still reachable. Once the GC collects it, deref() returns undefined.
const d = new Date(2026, 0, 15);
const c = structuredClone({ when: d });
console.log(c.when instanceof Date, c.when.getTime() === d.getTime());structuredClone preserves built-ins like Date, Map, Set, RegExp, ArrayBuffer — not just plain objects.
console.log([1, 2, 3, 4].findLast(n => n < 3));findLast scans from right to left and returns the last matching element — here, 2 (the last value < 3).
function sum(a, b, c) { return a + b + c; }
const args = [1, 2, 3];
console.log(sum(...args));Spread at a call site expands the iterable into individual arguments — a=1, b=2, c=3.