JavaScript and Node Core
This file is the language-and-runtime half of your refresher: what JavaScript values actually are, how
scope and this resolve, how the prototype chain and class sugar relate, how iterators and generators
work, and then the two topics senior interviews spend the most time on — the event loop (with Node’s
libuv phases, real “predict the output” puzzles, and a spec-faithful Promise built from scratch) and the
V8 execution model (tiers, hidden classes, inline caches, elements kinds, GC). Everything here was
executed on Node 22.22.2 / V8 12.4 and the pasted output is real. Where a feature is specified in
ES2025/ES2026 but not yet in Node 22’s V8, it is labelled; see
Feature availability at the end.
Table of contents
- 1. Values, types and coercion
- 2. Scope, hoisting, closures and
this - 3. Objects and prototypes
- 4. Functions
- 5. Iterators and generators
- 6. Asynchrony and the event loop
- 7. Memory and garbage collection
- 8. The V8 performance model
- 9. Modules
- 10. Node specifics
- 11. Modern syntax you should be fluent in
- 12. Interview questions
- 13. Feature availability on Node 22
Cross-language note. Python made the opposite choice on loop-variable scoping, and its
nonlocal/closure rules are the natural counterpart to this chapter’s material. See 20 §5.4 and 20 §6.3.
1. Values, types and coercion
1.1 Primitives vs objects
Intuition. JavaScript has exactly seven primitive types and one non-primitive type. Primitives are immutable values compared by value; objects are references compared by identity. Every “method call on a primitive” is a temporary boxing operation that is thrown away immediately.
| Type | typeof | Wrapper | Notes |
|---|---|---|---|
undefined | "undefined" | — | absence of a value; default for uninitialised bindings |
null | "object" | — | historical bug preserved for web compat |
boolean | "boolean" | Boolean | |
number | "number" | Number | IEEE-754 binary64 |
bigint | "bigint" | BigInt | arbitrary precision integers, ES2020 |
string | "string" | String | UTF-16 code units, immutable |
symbol | "symbol" | Symbol | unique property keys |
object | "object" / "function" | — | functions are callable objects |
const s = 'abc';
s.toUpperCase(); // boxes to a temporary String object, calls, discards the box
s.custom = 1; // silently dropped in sloppy mode, TypeError in strict mode
console.log(s.custom); // undefined
typeof null; // 'object' <- the famous bug
typeof function () {}; // 'function' <- the only non-'object' object
typeof document?.all; // 'undefined' in browsers: [[IsHTMLDDA]], the one falsy object
Because typeof null === 'object', the reliable null check is x === null, and the reliable
“is it a plain object” check is:
const isPlainObject = (v) =>
typeof v === 'object' && v !== null && (Object.getPrototypeOf(v) === Object.prototype || Object.getPrototypeOf(v) === null);
For general tagging, Object.prototype.toString.call(v) still beats typeof:
const tag = (v) => Object.prototype.toString.call(v).slice(8, -1);
// tag([]) === 'Array', tag(null) === 'Null', tag(new Date()) === 'Date', tag(/x/) === 'RegExp'
1.2 Numbers are IEEE-754 doubles
Intuition. Every number is a 64-bit float: 1 sign bit, 11 exponent bits, 52 stored mantissa bits
(53 effective, because of the implicit leading 1). Decimal fractions like 0.1 have no exact binary
representation, exactly the way 1/3 has no exact decimal representation. Integers are exact only up to
2^53.
63 62 ......... 52 51 ................................................ 0
┌───┬──────────────────┬──────────────────────────────────────────────────┐
│ s │ exponent (11) │ mantissa / fraction (52) │
└───┴──────────────────┴──────────────────────────────────────────────────┘
value = (-1)^s x 1.fraction x 2^(exponent - 1023)
// 01-numbers.js
console.log('0.1 + 0.2 =', 0.1 + 0.2);
console.log('(0.1+0.2).toFixed(20) =', (0.1 + 0.2).toFixed(20));
console.log('Number.EPSILON =', Number.EPSILON);
console.log('nearlyEqual(0.1+0.2, 0.3) =', Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON);
console.log('MAX_SAFE_INTEGER =', Number.MAX_SAFE_INTEGER);
console.log('2**53 =', 2 ** 53);
console.log('2**53 + 1 =', 2 ** 53 + 1);
console.log('2**53 === 2**53 + 1 =', 2 ** 53 === 2 ** 53 + 1);
console.log('isSafeInteger(2**53) =', Number.isSafeInteger(2 ** 53));
console.log('MAX_VALUE * 2 =', Number.MAX_VALUE * 2);
console.log('(0.1+0.7)*10 =', (0.1 + 0.7) * 10);
console.log('Math.floor((0.1+0.7)*10) =', Math.floor((0.1 + 0.7) * 10));
0.1 + 0.2 = 0.30000000000000004
(0.1+0.2).toFixed(20) = 0.30000000000000004441
Number.EPSILON = 2.220446049250313e-16
nearlyEqual(0.1+0.2, 0.3) = true
MAX_SAFE_INTEGER = 9007199254740991
2**53 = 9007199254740992
2**53 + 1 = 9007199254740992
2**53 === 2**53 + 1 = true
isSafeInteger(2**53) = false
MAX_VALUE * 2 = Infinity
(0.1+0.7)*10 = 7.999999999999999
Math.floor((0.1+0.7)*10) = 7
Why 2**53 + 1 fails. At magnitude 2^53 the gap between consecutive representable doubles (the ULP)
is exactly 1.0. At 2^53 + 1 the gap becomes 2.0, so 2^53 + 1 is not representable at all — it rounds to
nearest-even, which is 2^53. Number.MAX_SAFE_INTEGER is 2^53 − 1 precisely because that is the last
integer n for which n and n + 1 are both representable.
Number.EPSILON (2^-52) is the ULP at 1.0, so Math.abs(a - b) < Number.EPSILON is only a valid
“nearly equal” test for values near 1. The general form scales it:
const nearlyEqual = (a, b, eps = Number.EPSILON) =>
Math.abs(a - b) <= eps * Math.max(1, Math.abs(a), Math.abs(b));
For money, do not use doubles at all: use integer minor units (cents as a number while under 2^53, or
BigInt), or a decimal library. Math.sumPrecise (ES2026) gives an exactly-rounded sum of an iterable
of doubles but is not in Node 22.
1.3 BigInt
const big = 2n ** 53n;
console.log(big + 1n); // 9007199254740993n <- exact
console.log(typeof 1n); // 'bigint'
console.log(1n == 1, 1n === 1); // true false
console.log(BigInt(Number.MAX_SAFE_INTEGER) * 2n); // 18014398509481982n
1n + 1; // TypeError: Cannot mix BigInt and other types
9007199254740993n
bigint
true false
18014398509481982n
1n + 1 throws = TypeError: Cannot mix BigInt and other types, use explicit conversions
BigInt is arbitrary precision but has no fractional part, cannot be mixed with number in arithmetic
(deliberately: implicit conversion would silently lose precision), is not accepted by Math.*, and is
not serialisable by JSON.stringify without a toJSON/replacer. Relational comparison (<, >, ==)
does work across the two types because it cannot lose precision.
1.4 -0 and NaN
IEEE-754 has two zeros and a quiet NaN with no identity.
console.log(-0 === 0); // true <- === cannot see the sign
console.log(Object.is(-0, 0)); // false <- Object.is can
console.log(1 / -0); // -Infinity <- the usual way to detect it before Object.is
console.log(String(-0)); // '0' <- ToString normalises
console.log(JSON.stringify(-0)); // '0'
console.log([-0].includes(0)); // true <- SameValueZero
console.log([-0].indexOf(0)); // 0 <- strict equality
console.log(Math.round(-0.4)); // -0 <- an easy way to produce one accidentally
console.log(Math.sign(-0)); // -0
console.log(NaN === NaN); // false
console.log(Object.is(NaN, NaN)); // true
console.log([NaN].includes(NaN)); // true <- SameValueZero
console.log([NaN].indexOf(NaN)); // -1 <- strict equality
console.log(new Set([NaN, NaN]).size); // 1 <- SameValueZero
console.log(isNaN('foo'), Number.isNaN('foo')); // true false
console.log(Math.max(), Math.min()); // -Infinity Infinity
isNaN coerces its argument first (isNaN('foo') is isNaN(NaN)), which is almost never what you want.
Number.isNaN does not coerce. Same story for parseInt/parseFloat vs Number.
1.5 The three (four) equality algorithms
| Algorithm | Used by | 0 vs -0 | NaN vs NaN | cross-type |
|---|---|---|---|---|
Abstract equality (==) | ==, != | equal | not equal | coerces |
Strict equality (===) | ===, !==, indexOf, switch | equal | not equal | never equal |
| SameValueZero | includes, Map/Set keys, Array.prototype.fill search | equal | equal | never equal |
| SameValue | Object.is, defineProperty change detection | not equal | equal | never equal |
a, b | == | === | Object.is
------------------+-------+-------+----------
0, -0 | true | true | false
NaN, NaN | false | false | true
1, "1" | true | false | false
null, undefined | true | false | false
Note that Map and Set use SameValueZero, and they normalise -0 to +0 on insert:
const m = new Map();
m.set(NaN, 'nan').set(-0, 'minus zero');
m.get(NaN); // 'nan'
m.get(0); // 'minus zero'
Object.is([...m.keys()][1], 0); // true — stored as +0
1.6 The abstract equality algorithm, in full
x == y (ECMAScript IsLooselyEqual) runs top to bottom; the first matching rule wins:
- If
Type(x)isType(y), returnx === y. null == undefinedandundefined == null→ true.Number == String→x == ToNumber(y).String == Number→ToNumber(x) == y.BigInt == String→StringToBigInt(y); if it fails, false.Booleanon either side →ToNumber(boolean) == other(sotrue→ 1,false→ 0).Number|BigInt|String|Symbol == Object→x == ToPrimitive(y).Object == Number|BigInt|String|Symbol→ToPrimitive(x) == y.BigIntvsNumber→ mathematically equal (no precision loss), false for NaN/±Infinity.- Otherwise false.
Two consequences worth memorising: null/undefined are loosely equal only to each other and to
nothing else (null == 0 is false — rule 2 fires before any numeric coercion), and an object is never
loosely equal to a boolean without going through ToPrimitive then ToNumber.
Measured surprises (02-equality.js):
expression | result
--------------------------+--------
[] == ![] | true
[] == false | true
[] == "" | true
[] == 0 | true
[[]] == 0 | true
[0] == false | true
[1] == true | true
[2] == true | false
null == undefined | true
null === undefined | false
null == 0 | false
null >= 0 | true
null > 0 | false
undefined == 0 | false
NaN == NaN | false
"" == 0 | true
"0" == 0 | true
"" == "0" | false
" \t\n" == 0 | true
false == "false" | false
false == "0" | true
true == "1" | true
null == false | false
undefined == false | false
{} == "[object Object]" | true
new String("a") == "a" | true
new String("a") === "a" | false
Symbol() == Symbol() | false
1n == 1 | true
Walk through [] == ![] out loud in an interview:
![]— every object is truthy, so this isfalse.[] == false— rule 6: the boolean becomes0, giving[] == 0.- Rule 8:
ToPrimitive([])with default hint →valueOfreturns the array itself (not primitive) →toString→[].join(',')→"". "" == 0— rule 4:ToNumber("")is0.0 == 0→ true.
And null >= 0 === true while null > 0 === false and null == 0 === false: relational operators use
ToNumber (null → 0, so 0 >= 0), but == has that special-cased rule 2 that short-circuits before
any numeric coercion. This also means == is not transitive:
"" == 0 -> true
0 == "0" -> true
"" == "0" -> false
1.7 ToPrimitive and Symbol.toPrimitive
Intuition. Whenever an object must become a primitive, the engine calls ToPrimitive(obj, hint)
where hint is "number", "string" or "default". If obj[Symbol.toPrimitive] exists it is used;
otherwise the engine tries valueOf then toString for the number/default hints, and toString then
valueOf for the string hint. Date is the one built-in that treats "default" as "string".
const traced = {
[Symbol.toPrimitive](hint) {
console.log(' hint:', hint);
return hint === 'number' ? 42 : 'forty-two';
},
};
+traced; // hint: number -> 42
`${traced}`; // hint: string -> 'forty-two'
traced + ''; // hint: default -> 'forty-two'
traced * 2; // hint: number -> 84
String(traced); // hint: string
traced == 42; // hint: default -> 'forty-two' == 42 -> false
Symbol.toPrimitive called with hint: number
+traced -> 42
Symbol.toPrimitive called with hint: string
`${traced}` -> forty-two
Symbol.toPrimitive called with hint: default
traced + "" -> forty-two
Symbol.toPrimitive called with hint: number
traced * 2 -> 84
Symbol.toPrimitive called with hint: string
String(traced)-> forty-two
Symbol.toPrimitive called with hint: default
traced == 42 -> false
Without Symbol.toPrimitive:
Without Symbol.toPrimitive, valueOf then toString (default/number hint):
valueOf
vt + 1 -> 8
toString
`${vt}` -> seven
valueOf
vt * 2 -> 14
toString
[vt] + ""-> seven
Which operators use which hint:
| Operation | Hint |
|---|---|
+ (binary), ==, Date in + | default |
-, *, /, %, **, unary +, <, >, bitwise | number |
Template literals, String(x), property keys, ${} | string |
Template literal coercion always uses the string hint and calls ToString, not String() on
symbols — `${Symbol()}` throws a TypeError while String(Symbol()) works. Arrays stringify by
join(','), and null/undefined inside an array join to the empty string:
[].toString() -> ""
[1,2].toString() -> "1,2"
[null, undefined].toString() -> ","
[[1,[2]],3].toString() -> "1,2,3"
Pitfalls
parseIntwithout a radix used to be octal-sensitive; modern engines default to 10 except for a0xprefix, but['1','7','11'].map(parseInt)is still[1, NaN, 3]becausemappasses the index as the second argument (the radix).x == nullis the one loose comparison worth using deliberately: it means “null or undefined” and nothing else. Most style guides allow it explicitly.+new Date()andnew Date() - 0are numbers;new Date() + 0is a string. That is thedefaulthint special case.JSON.stringifydropsundefined, functions and symbols in objects, and turns them intonullinside arrays. It also turnsNaN/Infinityintonull.- Sorting numbers without a comparator sorts them as strings:
[10, 9, 1].sort()is[1, 10, 9].
Interview follow-ups
Q: Why is typeof null === 'object'?
A: In the original implementation values were tagged in the low bits of a machine word; the object
tag was 000 and the null pointer was all-zero bits, so it read as an object. Fixing it was proposed
and rejected as web-breaking.
Q: How do you check “is this an integer” safely?
A: Number.isInteger(x) for representability, Number.isSafeInteger(x) if you also need x ± 1 to
be distinguishable. x % 1 === 0 also accepts Infinity-adjacent nonsense and coerces strings.
Q: Why does 0.1 + 0.2 !== 0.3 but 0.5 + 0.25 === 0.75?
A: 0.5 and 0.25 are negative powers of two, so they are exact in binary; 0.1 and 0.2 are repeating binary fractions and get rounded to the nearest double, and the rounding errors do not cancel.
Q: When is == acceptable?
A: x == null. Everywhere else use === — the coercion table is not something a reviewer should
have to hold in their head.
Q: What is the difference between Object.is and ===?
A: Only two cases: Object.is(NaN, NaN) is true where === is false, and Object.is(0, -0) is
false where === is true. Everything else is identical.
Q: Why does new String('a') == 'a' but !== 'a'?
A: == applies ToPrimitive to the wrapper object (rule 8) producing the primitive 'a'; ===
compares types first and an object is never strictly equal to a string.
2. Scope, hoisting, closures and this
2.1 var, let, const and the temporal dead zone
var | let | const | |
|---|---|---|---|
| Scope | function (or global) | block | block |
| Hoisted | yes, initialised to undefined | yes, uninitialised (TDZ) | yes, uninitialised (TDZ) |
| Redeclarable in same scope | yes | no | no |
| Reassignable | yes | yes | no |
Creates a property on globalThis | yes (at top level, script) | no | no |
typeof before declaration | 'undefined' | throws | throws |
Intuition. All declarations are hoisted — the binding is created when the scope is entered. The
difference is initialisation. var bindings are initialised to undefined immediately; let/const
bindings stay in the temporal dead zone until control flow reaches the declaration. The TDZ is a
runtime state, not a syntactic region: a function defined before the let but called after it works
fine.
// 03-scope.js
console.log('typeof hoistedFn :', typeof hoistedFn); // function — full declaration hoisting
console.log('varX before decl :', typeof varX, varX); // undefined undefined
try { letY; } catch (e) { console.log(e.constructor.name + ': ' + e.message); }
function hoistedFn() {}
var varX = 1;
let letY = 2;
console.log('typeof neverDeclared :', typeof neverDeclared); // 'undefined' — safe
try { typeof tdzVar; } catch (e) { console.log('typeof tdzVar :', e.constructor.name); }
let tdzVar = 1;
typeof hoistedFn : function
varX before decl : undefined undefined
letY before decl : ReferenceError: Cannot access 'letY' before initialization
typeof neverDeclared : undefined
typeof tdzVar : ReferenceError
That last pair is the point people miss: typeof is not a safe probe for a TDZ binding, only for a
completely undeclared identifier.
const freezes the binding, not the value:
const object mutated : { a: 2 }
reassign const : TypeError
frozen : true
write to frozen : TypeError (strict); silent no-op in sloppy
after frozen write : { a: 2 }
2.2 Function scope vs block scope, and function declarations in blocks
blockFn in block : block
blockFn after block : undefined
In a strict-mode script/module a function declaration inside a block is block-scoped. In sloppy mode,
Annex B web-compat semantics hoist a var-like binding to the enclosing function scope and assign it
when the declaration is evaluated — which is why the name exists but is undefined after the block in
some configurations and defined in others. Never rely on it; use const fn = () => {} inside blocks.
2.3 Closures
Intuition. A closure is a function together with the environment record it captured. V8 allocates a
Context object on the heap for exactly the variables an inner function references (it does escape
analysis at parse time via the pre-parser), and every closure created in the same scope shares that one
context. Two calls to the enclosing function create two contexts.
scope chain at call time
[ inner function ]
│ [[Environment]]
▼
[ Context: { count: 2 } ] ← shared by every closure from the same call
│ outer
▼
[ module/script scope ]
│ outer
▼
[ global scope ] → null
function counter() {
let count = 0;
return { inc: () => ++count, get: () => count };
}
const c1 = counter(), c2 = counter();
c1.inc(); c1.inc(); c2.inc();
console.log(c1.get(), c2.get()); // 2 1 — independent contexts
2.4 The classic for (var i) bug and three fixes
const varFns = [];
for (var i = 0; i < 3; i++) varFns.push(() => i);
varFns.map((f) => f()); // [3, 3, 3]
// fix 1: let — the spec creates a NEW binding per iteration and copies the value forward
const letFns = [];
for (let j = 0; j < 3; j++) letFns.push(() => j);
letFns.map((f) => f()); // [0, 1, 2]
// fix 2: IIFE — an explicit new scope per iteration
const iifeFns = [];
for (var k = 0; k < 3; k++) iifeFns.push(((captured) => () => captured)(k));
iifeFns.map((f) => f()); // [0, 1, 2]
// fix 3: bind — freeze the value as a bound argument
const bindFns = [];
for (var m = 0; m < 3; m++) bindFns.push(((x) => x).bind(null, m));
bindFns.map((f) => f()); // [0, 1, 2]
var loop (bug):
[ 3, 3, 3 ]
let loop (fix 1 - per-iteration binding):
[ 0, 1, 2 ]
IIFE (fix 2):
[ 0, 1, 2 ]
bind (fix 3):
[ 0, 1, 2 ]
The mechanism behind fix 1 is worth knowing precisely: for a let head, the spec runs
CreatePerIterationEnvironment before each iteration, which creates a fresh binding and copies the
previous iteration’s value into it. That is why the increment j++ still works across iterations even
though each closure sees a different binding. const in a for(;;) head is a SyntaxError for exactly
this reason (you cannot increment it); for (const x of xs) is fine because there is no increment.
2.5 IIFE and module scope
Before modules, the IIFE was the only way to get a private scope:
var MyLib = (function () {
var privateState = 0;
return { bump: function () { return ++privateState; } };
})();
ESM made this obsolete: every module has its own scope, var at module top level does not touch
globalThis, and modules are always strict. In CommonJS the module body is wrapped by Node in a
function with (exports, require, module, __filename, __dirname), so module scope is function scope —
which is why this at CJS top level is module.exports (an object) while in ESM it is undefined.
arrow as method : module.exports = {} // CJS
module-scope this : undefined // ESM
2.6 this: the five binding rules
Resolve them in this priority order at the call site:
newbinding —new Fn()creates a fresh object whose prototype isFn.prototypeand bindsthisto it.- Explicit binding —
fn.call(o),fn.apply(o),fn.bind(o). A bound function’sthiscannot be re-bound later. - Implicit binding —
o.fn();thisiso. Only the last property access matters. - Default binding — a bare
fn().undefinedin strict mode/modules,globalThisin sloppy. - Arrow functions — no own
thisat all; they close over thethisof the enclosing lexical scope, and this beats every rule above (you cannotcall/bindan arrow’sthis).
// 04-this.js
const obj = { name: 'obj', who() { return this?.name; } };
obj.who(); // 'obj'
const detached = obj.who;
detached(); // undefined (strict)
(0, obj.who)(); // undefined — the comma operator discards the Reference
obj.who.call({ name: 'call' });// 'call'
const hard = obj.who.bind({ name: 'first' });
hard.call({ name: 'second' }); // 'first' — hard binding wins
function Person(name) { this.name = name; }
const BoundPerson = Person.bind({ name: 'ignored' });
new BoundPerson('still-works').name; // 'still-works' — new beats bind
strict default this : undefined
sloppy default this : globalThis
obj.who() : obj
detached() : undefined
wrapper.inner.who() : obj
(0, obj.who)() : undefined
call/apply/bind : call apply bind
rebind a bound fn : first
new binding : newed
new on bound ctor : still-works
arrow in method : lex
new Arrow : TypeError
detached method : undefined
detached field arrow : widget
static block ran : true | static field: Widget
static this === class : true
The key insight for this questions: this is determined by how a function is called, not where it
is defined — except for arrows, which are the exact opposite.
2.7 Losing this, and the four fixes
class Timer {
ticks = 0;
incBroken() { this.ticks++; } // prototype method — needs a receiver
incArrow = () => { this.ticks++; }; // instance field — captures `this` at construction
}
const t = new Timer();
[1].forEach(t.incBroken); // TypeError: this is undefined
[1].forEach(t.incBroken.bind(t)); // fix 1: bind
[1].forEach(() => t.incBroken()); // fix 2: arrow wrapper
[1].forEach(t.incArrow); // fix 3: class field arrow
[1].forEach(t.incBroken, t); // fix 4: the thisArg parameter many array methods take
forEach(t.incBroken) -> TypeError
bind -> ticks=1
arrow wrap -> ticks=2
field arrow -> ticks=3
thisArg -> ticks=4
bind vs class-field arrow. A prototype method exists once per class and lives on
Class.prototype, so it is shared, patchable, spy-able in tests, and visible to super. A class-field
arrow is created per instance during construction: N instances means N closures and N property
slots, it is not on the prototype (so super.method() and subclass overrides do not see it), and it
cannot be stubbed via the prototype. Prefer prototype methods plus bind in the constructor when you
need a stable identity, or bind at the call site. Use field arrows when the ergonomics of React-style
handlers matter more than the memory.
2.8 new.target
new.target is undefined in a normal call and the constructor function in a new call. Inside a
derived constructor after super(), it is the most-derived constructor.
function Guard() {
if (new.target === undefined) return 'called without new';
return 'constructed as ' + new.target.name;
}
class Base { constructor() { this.builtBy = new.target.name; } }
class Derived extends Base {}
new Derived().builtBy; // 'Derived'
Guard() : called without new
new.target in super : Derived
This is how you write an abstract class (if (new.target === Abstract) throw ...) and how Babel’s
class downlevelling implements the “class constructor cannot be invoked without new” check.
Pitfalls
- A
letinside a loop body (not the head) is a fresh binding per iteration too; aletin the head of awhileloop is not —whilehas no per-iteration environment. thisinside a plain callback passed tosetTimeoutisTimeoutin Node andwindowin browsers, not your object.- Class fields are initialised in order, after
super(), so a field cannot reference a field declared below it, and a base-class constructor cannot see a derived class’s fields (they do not exist yet). This is a real source ofundefinedin TypeScript classes withstrictPropertyInitialization. - Closures over loop variables inside
setTimeoutstill surprise people in code review; if you seevarin a loop with any async callback, it is a bug or about to be one. - Arrow functions have no
arguments, noprototype, cannot benewed, and cannot be generators.
Interview follow-ups
Q: Is the TDZ a compile-time or runtime concept?
A: Runtime. The binding exists from scope entry but is marked uninitialised; any read before the
declaration executes throws ReferenceError. That is why a function that closes over a let can be
defined earlier as long as it is called later.
Q: What does let cost compared to var?
A: Effectively nothing in modern V8 for straight-line code. In loops, the per-iteration environment can mean an extra context allocation per iteration if something captures the binding; if nothing captures it, V8 optimises the copy away.
Q: How do you make truly private state?
A: #private fields (hard-private, enforced by the engine, brand-checked), a closure, or a
WeakMap keyed by the instance. _underscore is a convention, not privacy.
Q: Why does (0, obj.method)() lose this?
A: obj.method evaluates to a Reference with a base of obj. The comma operator calls
GetValue on it, producing the bare function value and discarding the base, so the call uses default
binding.
Q: What is the difference between fn.call(null) in strict and sloppy mode?
A: Sloppy mode applies ToObject to the thisArg and substitutes globalThis for
null/undefined; strict mode passes the value through untouched.
Q: Can you bind an arrow function?
A: You can call .bind() on it — it returns a new function — but the this argument is ignored,
because an arrow has no this binding to set. Bound arguments still work.
3. Objects and prototypes
3.1 Property descriptors
Every own property is a descriptor, one of two shapes:
| Kind | Fields |
|---|---|
| Data descriptor | value, writable, enumerable, configurable |
| Accessor descriptor | get, set, enumerable, configurable |
Literal syntax creates writable: true, enumerable: true, configurable: true.
Object.defineProperty defaults every omitted flag to false.
const o = {};
o.a = 1; // all true
Object.defineProperty(o, 'b', { value: 2 }); // all false
console.log(Object.getOwnPropertyDescriptor(o, 'a'));
console.log(Object.getOwnPropertyDescriptor(o, 'b'));
console.log(Object.keys(o)); // 'b' is non-enumerable
o.b = 99; // silently ignored in sloppy mode, TypeError in strict
console.log(o.b);
{ value: 1, writable: true, enumerable: true, configurable: true }
{ value: 2, writable: false, enumerable: false, configurable: false }
[ 'a' ]
2
configurable: false is the only irreversible flag: you cannot make a property configurable again,
and you cannot delete it or change it from data to accessor. The one legal mutation is
writable: true -> false.
Freezing levels, weakest to strongest:
| Call | New properties | Delete | Reassign existing | Reconfigure |
|---|---|---|---|---|
Object.preventExtensions | no | yes | yes | yes |
Object.seal | no | no | yes | no |
Object.freeze | no | no | no | no |
All three are shallow. A deep freeze is a recursive walk (watch for cycles).
3.2 The prototype chain
[[Prototype]] is an internal slot. Three different things share the word “prototype”:
| Expression | Meaning |
|---|---|
obj.__proto__ | legacy accessor on Object.prototype for the [[Prototype]] slot |
Object.getPrototypeOf(obj) | the modern, correct way to read that slot |
Fn.prototype | an ordinary property on a function, used as the [[Prototype]] of objects new Fn() creates |
Lookup walks the chain on read, but assignment does not: obj.x = 1 creates an own property on
obj even if x exists up the chain — unless the inherited property is a setter or a non-writable
data property.
const d = new Dog('Rex')
d ──[[Proto]]──> Dog.prototype ──[[Proto]]──> Animal.prototype ──[[Proto]]──> Object.prototype ──> null
own: name own: bark own: speak own: toString, hasOwnProperty, ...
function Animal(name) { this.name = name; }
Animal.prototype.speak = function () { return `${this.name} makes a noise`; };
function Dog(name) { Animal.call(this, name); }
Dog.prototype = Object.create(Animal.prototype); // link
Dog.prototype.constructor = Dog; // repair
Dog.prototype.bark = function () { return `${this.name}: woof`; };
const d = new Dog('Rex');
console.log(d.bark(), '|', d.speak());
console.log(Object.getPrototypeOf(d) === Dog.prototype);
console.log(d instanceof Animal, Object.prototype.hasOwnProperty.call(d, 'speak'));
Rex: woof | Rex makes a noise
true
true false
Dog.prototype = Animal.prototype (a common mistake) would make Dog.prototype.bark also visible on
every Animal, because there is only one object.
3.3 instanceof and Symbol.hasInstance
a instanceof B asks: is B.prototype anywhere in a’s prototype chain? It is therefore a question
about objects, not constructors, and it breaks across realms (an array from an iframe or a Node
vm context is not instanceof Array in your realm — use Array.isArray).
class Even { static [Symbol.hasInstance](x) { return typeof x === 'number' && x % 2 === 0; } }
console.log(4 instanceof Even, 5 instanceof Even); // true false
3.4 class is sugar — but not only sugar
What class gives you beyond the ES5 pattern:
- The body is always strict mode.
- Methods are non-enumerable (
Object.keys(Dog.prototype)is empty for a class). - The constructor cannot be called without
new(TypeError). - Class declarations are hoisted but in the TDZ, so no use-before-declaration.
extendsalso links the constructors (Object.getPrototypeOf(Dog) === Animal), which is how static members are inherited. The ES5 pattern needs an explicit second link.- Derived constructors have no
thisuntilsuper()returns; the base constructor is what actually allocates the object, which is whyextends Arrayworks and the ES5 trick does not. #privatefields are enforced by the engine, not by convention, and are not visible toObject.keys,JSON.stringify,Proxytraps, or the debugger’s normal property view.
class Counter {
#n = 0; // private instance field
static #instances = 0; // private static
static { Counter.registry = new Set(); } // static initialization block
id = ++Counter.#instances; // public field, runs per-instance before ctor body
constructor() { Counter.registry.add(this); }
inc() { return ++this.#n; }
get value() { return this.#n; }
static has(o) { return #n in o; } // ergonomic brand check
}
const c = new Counter();
c.inc(); c.inc();
console.log(c.value, c.id, Counter.has(c), Counter.has({}));
console.log(Object.keys(c)); // fields are enumerable; methods are not
2 1 true false
[ 'id' ]
Field-vs-method ordering trap. Public/private fields are installed in order, before the
constructor body, and each initializer sees this. A field initialized with an arrow function is a
per-instance property (bound this, costs memory per instance); a method is one shared function on
the prototype (unbound this, cheap). That is the real trade-off behind onClick = () => {} in class
components.
3.5 super needs a home object
super is not dynamic. It resolves through the method’s [[HomeObject]], set when the method is
defined with shorthand syntax. Assign a function to a property instead and super is a SyntaxError.
const base = { greet() { return 'base'; } };
const good = { __proto__: base, greet() { return super.greet() + '+good'; } };
console.log(good.greet()); // base+good
// const bad = { __proto__: base, greet: function () { return super.greet(); } }; // SyntaxError
3.6 Mixins
There is no multiple inheritance, so the idiom is a function from class to class:
type Ctor<T = {}> = new (...args: any[]) => T;
const Serializable = <T extends Ctor>(Base: T) =>
class extends Base { toJSON() { return { ...this }; } };
const Timestamped = <T extends Ctor>(Base: T) =>
class extends Base { createdAt = new Date(0); };
class Model {}
class User extends Serializable(Timestamped(Model)) {
constructor(public name: string) { super(); }
}
console.log(JSON.stringify(new User('ana')));
{"createdAt":"1970-01-01T00:00:00.000Z","name":"ana"}
Each mixin adds a real link in the prototype chain, so instanceof on the mixin is not available
(that is what Symbol.hasInstance is for).
Pitfalls
for...inwalks enumerable inherited properties. UseObject.keys/entries, orObject.hasOwn(obj, k)(ES2022) as the guard. Neverobj.hasOwnProperty(k)on untrusted objects.- Mutating
Object.prototype(orArray.prototype) breaks everyfor...inon the page and deoptimizes property access globally. Object.assigncopies values, invoking getters and losing descriptors.Object.defineProperties(target, Object.getOwnPropertyDescriptors(src))is the faithful copy.- Spread and
Object.assignonly copy own enumerable string and symbol keys — not the prototype. JSON.stringifydropsundefined, functions, and symbols; turnsDateinto a string; throws on cycles and onBigInt; and callstoJSONif present.delete obj.kis fine on plain objects but on arrays it creates a hole (see elements kinds).
Interview follow-ups
Q: What is the difference between Object.create(null) and {}?
A: The former has no prototype: no toString, no __proto__ accessor, no
hasOwnProperty. That makes it the correct choice for a string-keyed dictionary because a key named
__proto__ or constructor cannot collide with anything or trigger prototype pollution.
Q: How do you implement new yourself?
A: function myNew(Ctor, ...args) { const obj = Object.create(Ctor.prototype); const r = Ctor.apply(obj, args); return (typeof r === 'object' && r !== null) || typeof r === 'function' ? r : obj; } — the return-value rule is the part people forget.
Q: Why is Object.freeze not enough for immutability?
A: It is shallow, it does not stop mutation of objects reachable through frozen properties, and it
does not prevent Map/Set contents from changing. Use a recursive freeze or a persistent-data-structure library.
Q: What does class A extends null do?
A: It is legal syntax but nearly unusable: you cannot call super(), so the only way to construct
an instance is to return an object explicitly from the constructor.
Q: How would you check whether an object has a private field without throwing?
A: The #field in obj brand check (ES2022), typically wrapped in a static method as shown above.
Q: Prototype vs class inheritance — is there a performance difference?
A: No meaningful one; class compiles to the same shapes. The real performance lever is keeping
object shapes consistent so inline caches stay monomorphic.
4. Functions
4.1 Parameters, arity, and arguments
function f(a, b = 2, ...rest) {}
console.log(f.length); // 1 — stops at the first default or rest param
arguments is array-like, not an array; it is absent in arrow functions and in any function with
default/rest/destructured parameters it is unmapped (no live link to the named parameters). Use rest
parameters instead — they are a real array and do not force V8 to materialize an arguments object.
4.2 Currying and partial application
type AnyFn = (...args: any[]) => any;
function curry<F extends AnyFn>(fn: F) {
const arity = fn.length;
return function curried(...args: any[]): any {
return args.length >= arity ? fn(...args) : (...more: any[]) => curried(...args, ...more);
};
}
const volume = (l: number, w: number, h: number) => l * w * h;
const cv = curry(volume);
console.log(cv(2)(3)(4), cv(2, 3)(4), cv(2)(3, 4), cv(2, 3, 4)); // 24 24 24 24
Partial application is the weaker, cheaper cousin: fn.bind(null, 2, 3).
4.3 Composition
const pipe = <T>(...fns: Array<(x: T) => T>) => (x: T) => fns.reduce((v, f) => f(v), x);
const compose = <T>(...fns: Array<(x: T) => T>) => (x: T) => fns.reduceRight((v, f) => f(v), x);
const inc = (n: number) => n + 1, dbl = (n: number) => n * 2;
console.log(pipe(inc, dbl)(5), compose(inc, dbl)(5)); // 12 11
Properly variadic typed pipe needs variadic tuple types — see
TypeScript: tuples and variadic types.
4.4 Memoization
function memoize<A, R>(fn: (a: A) => R, keyFn: (a: A) => unknown = (a) => a) {
const cache = new Map<unknown, R>();
return (a: A): R => {
const k = keyFn(a);
if (cache.has(k)) return cache.get(k)!;
const v = fn(a);
cache.set(k, v);
return v;
};
}
// Object keys without leaking: WeakMap lets the key be collected.
function memoizeObject<T extends object, R>(fn: (t: T) => R) {
const cache = new WeakMap<T, R>();
return (t: T): R => {
let v = cache.get(t);
if (v === undefined && !cache.has(t)) { v = fn(t); cache.set(t, v); }
return v as R;
};
}
The three real-world questions an interviewer will push on: what is the cache key for multiple
arguments (JSON.stringify is correct-ish and slow; a nested Map trie is correct and fast), how do
you bound the cache (LRU — see Data structures),
and how do you avoid leaking (WeakMap for object keys, TTL/size caps otherwise).
4.5 Debounce and throttle
function debounce<F extends (...a: any[]) => void>(fn: F, ms: number, immediate = false) {
let t: ReturnType<typeof setTimeout> | null = null;
return function (this: unknown, ...args: Parameters<F>) {
const callNow = immediate && t === null;
if (t) clearTimeout(t);
t = setTimeout(() => { t = null; if (!immediate) fn.apply(this, args); }, ms);
if (callNow) fn.apply(this, args);
};
}
function throttle<F extends (...a: any[]) => void>(fn: F, ms: number) {
let last = 0, pending: Parameters<F> | null = null, timer: any = null;
return function (this: unknown, ...args: Parameters<F>) {
const now = Date.now();
if (now - last >= ms) { last = now; fn.apply(this, args); }
else { // trailing edge
pending = args;
timer ??= setTimeout(() => {
last = Date.now(); timer = null;
if (pending) { fn.apply(this, pending); pending = null; }
}, ms - (now - last));
}
};
}
| Fires | Use for | |
|---|---|---|
| Debounce | once, ms after the last call | search-as-you-type, resize end, autosave |
| Throttle | at most once per ms window | scroll position, mousemove, analytics beacons |
4.6 Interview follow-ups
Q: Implement Function.prototype.bind.
A:
Function.prototype.myBind = function (thisArg, ...bound) {
const target = this;
if (typeof target !== 'function') throw new TypeError('not callable');
function Bound(...args) {
// called with `new`: ignore thisArg, keep the prototype chain
return target.apply(this instanceof Bound ? this : thisArg, [...bound, ...args]);
}
Bound.prototype = Object.create(target.prototype || null);
return Bound;
};
Q: Why can’t you tail-call-optimize in Node?
A: ES2015 specifies proper tail calls, but only JavaScriptCore ever shipped them. V8 removed its implementation (stack-trace and debugging costs, plus a syntactic-vs-implicit design argument), so deep recursion in Node overflows around 11k frames. Convert to an explicit loop or an explicit stack.
Q: What is a pure function and why does it matter here?
A: Same inputs -> same output, no observable side effects. It is the precondition for memoization, for safe parallelism, and for the referential-transparency arguments you will use when justifying a refactor.
Q: fn.call vs fn.apply vs fn.bind?
A: call invokes now with spread arguments, apply invokes now with an array, bind invokes
later and returns a new function with this and leading arguments fixed. bind is the only one that
survives being passed around.
5. Iterators and generators
5.1 The protocol
An object is iterable if it has a [Symbol.iterator]() method returning an iterator: an object
with next() returning { value, done }. for...of, spread, destructuring, Array.from,
Promise.all, new Map(...) and yield* all consume it.
class Range implements Iterable<number> {
constructor(private lo: number, private hi: number, private step = 1) {}
[Symbol.iterator](): Iterator<number> {
let i = this.lo;
const { hi, step } = this;
return {
next: () => (i < hi ? { value: (i += step) - step, done: false } : { value: undefined, done: true }),
return: () => { i = hi; return { value: undefined, done: true }; }, // called on break
};
}
}
console.log([...new Range(0, 10, 3)]); // [ 0, 3, 6, 9 ]
The return() method is the cleanup hook: break, throw, and early return inside for...of all
call it. That is how using-style resource release works for iterators.
5.2 Generators
A generator function returns an object that is both an iterator and an iterable, and whose execution
suspends at each yield.
function* fib() { let [a, b] = [0, 1]; for (;;) { yield a; [a, b] = [b, a + b]; } }
function* take(it, n) { let i = 0; for (const v of it) { if (i++ >= n) return; yield v; } }
console.log([...take(fib(), 10)]); // [0,1,1,2,3,5,8,13,21,34]
Two-way communication makes generators coroutines, not just lazy lists:
function* accumulator() {
let total = 0;
while (true) {
const x = yield total; // the value passed to next() lands here
if (x === undefined) continue;
total += x;
}
}
const acc = accumulator();
acc.next(); // prime: run to the first yield
console.log(acc.next(5).value, acc.next(10).value, acc.next(1).value); // 5 15 16
gen.next(v)— resume,yieldevaluates tov.gen.throw(e)— resume by raisingeat the suspendedyield(catchable inside).gen.return(v)— resume as if areturn vexecuted there;finallyblocks still run.yield*delegates to another iterable and evaluates to its return value.
function* inner() { yield 1; yield 2; return 'inner done'; }
function* outer() { const r = yield* inner(); yield r; }
console.log([...outer()]); // [ 1, 2, 'inner done' ]
5.3 Async iteration
Symbol.asyncIterator + for await...of. Each next() returns a promise.
async function* pages(total) {
for (let p = 1; p <= total; p++) {
await new Promise(r => setTimeout(r, 10)); // pretend fetch
yield { page: p, items: [p * 10, p * 10 + 1] };
}
}
(async () => {
for await (const { page, items } of pages(3)) console.log(page, items);
})();
1 [ 10, 11 ]
2 [ 20, 21 ]
3 [ 30, 31 ]
for await also accepts a sync iterable of promises, awaiting each in turn — which is a common source
of accidental serialization. If you want concurrency, collect promises and Promise.all them.
5.4 Iterator helpers (ES2026, shipped in Node 22)
Iterator helpers put lazy map/filter/take/drop/flatMap/reduce/toArray/some/every/find
on Iterator.prototype, so they work on generators, Map.keys(), NodeList iterators — anything.
function* naturals() { let n = 1; while (true) yield n++; }
let evaluated = 0;
const out = naturals()
.map(n => { evaluated++; return n * n; })
.filter(n => n % 2 === 1)
.take(4)
.toArray();
console.log(out, 'source values pulled:', evaluated);
[ 1, 9, 25, 49 ] source values pulled: 7
Seven pulls, not infinity — the pipeline is demand-driven, and no intermediate array is ever built.
Compare with array.map().filter().slice(), which allocates two full intermediates. Iterator.from(x)
wraps a bare iterator so the helpers are available.
Pitfalls
- Generators are single-pass.
const g = gen(); [...g]; [...g]gives you the values then[]. Return a fresh generator from[Symbol.iterator]()if you need repeat iteration. yieldinside a non-generator callback (arr.forEach(x => yield x)) is aSyntaxError; use afor...ofloop.returninside atrywith afinallythat also returns: thefinallywins, silently.- Spreading an infinite iterator hangs the process.
takefirst. for awaitover an array of promises awaits sequentially.Promise.allis what you usually want.
Interview follow-ups
Q: Difference between an iterable and an iterator?
A: An iterable has [Symbol.iterator](); an iterator has next(). Generators are both, which is
why for...of over a generator works and why you can only do it once.
Q: How do you make an object work with spread and for...of?
A: Give it [Symbol.iterator]. Object spread ({...o}) is different — it copies own enumerable
keys and needs no protocol.
Q: Where would you actually use a generator in production?
A: Paginated API traversal, streaming parsers, ID generators, tree/graph traversal you want to consume lazily, backtracking search where you want the first N solutions, and test-data builders. Redux-saga made the coroutine use case famous.
Q: What is the memory advantage?
A: O(1) instead of O(n): you never materialize the sequence. Combined with iterator helpers you get a full lazy pipeline. See the Python equivalent in Python core.
6. Asynchrony and the event loop
6.1 The model
graph TD
A["Synchronous script"] --> B["process.nextTick queue"]
B --> C["Microtask queue"]
C --> D["timers<br/>(setTimeout/setInterval)"]
D -->|drain nextTick + microtasks| E["pending callbacks"]
E -->|drain nextTick + microtasks| F["poll<br/>(I/O)"]
F -->|drain nextTick + microtasks| G["check<br/>(setImmediate)"]
G -->|drain nextTick + microtasks| H["close callbacks"]
H -->|drain nextTick + microtasks| B
JavaScript has one call stack. Anything asynchronous is a job enqueued for later. There are two queue tiers, and the rule that explains almost every puzzle is:
After every macrotask, and after the synchronous script finishes, the engine drains the entire microtask queue — including microtasks enqueued by microtasks — before taking the next macrotask.
| Tier | Enqueued by | Drained |
|---|---|---|
| Microtask | promise reactions, queueMicrotask, await resumption, MutationObserver | fully, after each macrotask |
| Macrotask | setTimeout, setInterval, setImmediate, I/O completions, UI events | one per loop turn |
Node adds one queue above microtasks: process.nextTick. Priority is
nextTick queue -> microtask queue -> next libuv phase.
6.2 Node’s libuv phases
┌───────────────────────────┐
┌─>│ timers │ setTimeout / setInterval callbacks whose time has come
│ └────────────┬──────────────┘
│ ┌────────────┴──────────────┐
│ │ pending callbacks │ deferred I/O callbacks (e.g. some TCP errors)
│ └────────────┬──────────────┘
│ ┌────────────┴──────────────┐
│ │ idle, prepare │ internal
│ └────────────┬──────────────┘ ┌───────────────┐
│ ┌────────────┴──────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │ waits for I/O; this is where the
│ └────────────┬──────────────┘ │ data, ... │ process actually blocks
│ ┌────────────┴──────────────┐ └───────────────┘
│ │ check │ setImmediate callbacks
│ └────────────┬──────────────┘
│ ┌────────────┴──────────────┐
└──┤ close callbacks │ socket.on('close'), etc.
└───────────────────────────┘
Between EVERY phase transition: drain nextTick queue, then drain microtask queue.
Practical consequences:
setTimeout(fn, 0)is really “at least 1 ms” and runs in the timers phase — a full loop turn away.setImmediate(fn)runs in the check phase, i.e. after the current poll phase, which for I/O callbacks means sooner thansetTimeout(fn, 0). From the main module the ordering ofsetTimeout(0)vssetImmediateis famously nondeterministic (it depends on how long process startup took relative to the 1 ms timer threshold).process.nextTickruns before any promise callback and can starve the loop if it recurses.
6.3 Predict-the-output puzzles
Puzzle 1 — every queue at once.
console.log('1 sync');
setTimeout(() => console.log('2 setTimeout 0'), 0);
setImmediate(() => console.log('3 setImmediate'));
process.nextTick(() => console.log('4 nextTick'));
Promise.resolve().then(() => console.log('5 microtask'));
queueMicrotask(() => console.log('6 queueMicrotask'));
(async () => { console.log('7 async body sync'); await null; console.log('8 after await'); })();
console.log('9 sync end');
1 sync
7 async body sync
9 sync end
4 nextTick
5 microtask
6 queueMicrotask
8 after await
2 setTimeout 0
3 setImmediate
Read it as three groups: everything synchronous (1, 7, 9 — note the async function body runs
synchronously up to its first await), then nextTick, then microtasks in FIFO order (5, 6, 8 — the
await null resumption is just another microtask, queued third), then macrotasks.
Puzzle 2 — await costs one tick, not two.
async function a() { console.log('a1'); await b(); console.log('a2'); }
async function b() { console.log('b1'); }
console.log('start');
a();
new Promise(r => { console.log('p exec'); r(); }).then(() => console.log('p then'));
console.log('end');
start
a1
b1
p exec
end
a2
p then
a2 beats p then. Since the ES2019 “await optimization”, awaiting a native promise costs one
microtask tick, not three — so a’s continuation is queued before the .then registered afterwards.
Interviewers who learned this pre-2019 sometimes expect the opposite; if the awaited value is a
thenable (not a native promise) you pay the extra ticks and the order flips.
Puzzle 3 — interleaving nextTick and microtasks.
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => { console.log('mt1'); process.nextTick(() => console.log('tick in mt')); });
process.nextTick(() => { console.log('tick1'); Promise.resolve().then(() => console.log('mt in tick')); });
tick1
mt1
mt in tick
tick in mt
timeout
The nextTick queue is drained first (tick1), then microtasks (mt1, then mt in tick which was
just enqueued), and only when both are empty does Node re-check nextTick (tick in mt) and finally
move to the timers phase.
6.4 Promise combinators
| Combinator | Settles when | Result | Rejects when |
|---|---|---|---|
all | all fulfil | array of values, input order | first rejection (fail fast) |
allSettled | all settle | array of `{status, value | reason}` |
any | first fulfilment | that value | all reject -> AggregateError |
race | first settle, either way | that value/reason | first settle is a rejection |
Promise.try(fn) | — | wraps a sync-or-async fn so a synchronous throw becomes a rejection | — |
Two things to say out loud in an interview: Promise.all does not cancel the other operations on
rejection (there is no cancellation in the promise model — that is what AbortController is for), and
race with an already-settled promise in the list resolves in one tick regardless of timers.
6.5 A spec-faithful Promise from scratch
This passes the test suite below, including thenable adoption, the chaining-cycle check, and single-settlement.
const PENDING = 'pending', FULFILLED = 'fulfilled', REJECTED = 'rejected';
class MyPromise {
#state = PENDING; #value; #cbs = [];
constructor(executor) {
const resolve = v => this.#settle(FULFILLED, v);
const reject = r => this.#settle(REJECTED, r);
try { executor(v => this.#resolveWith(v, resolve, reject), reject); }
catch (e) { reject(e); }
}
// 2.3 of the Promises/A+ spec: the Promise Resolution Procedure
#resolveWith(v, resolve, reject) {
if (v === this) return reject(new TypeError('Chaining cycle detected'));
if (v && (typeof v === 'object' || typeof v === 'function')) {
let then;
try { then = v.then; } catch (e) { return reject(e); }
if (typeof then === 'function') {
let called = false;
try {
return then.call(v,
y => { if (called) return; called = true; this.#resolveWith(y, resolve, reject); },
r => { if (called) return; called = true; reject(r); });
} catch (e) { if (!called) reject(e); return; }
}
}
resolve(v);
}
#settle(state, value) {
if (this.#state !== PENDING) return; // settle exactly once
this.#state = state; this.#value = value;
for (const cb of this.#cbs) queueMicrotask(cb);
this.#cbs = [];
}
then(onFul, onRej) {
return new MyPromise((resolve, reject) => {
const run = () => queueMicrotask(() => {
const handler = this.#state === FULFILLED ? onFul : onRej;
if (typeof handler !== 'function') { // pass-through
this.#state === FULFILLED ? resolve(this.#value) : reject(this.#value);
return;
}
try { resolve(handler(this.#value)); } catch (e) { reject(e); }
});
this.#state === PENDING ? this.#cbs.push(run) : run();
});
}
catch(onRej) { return this.then(undefined, onRej); }
finally(fn) { return this.then(v => { fn(); return v; }, r => { fn(); throw r; }); }
static resolve(v) { return v instanceof MyPromise ? v : new MyPromise(res => res(v)); }
static reject(r) { return new MyPromise((_, rej) => rej(r)); }
static all(iter) {
return new MyPromise((resolve, reject) => {
const items = [...iter]; const out = new Array(items.length); let left = items.length;
if (!left) return resolve([]);
items.forEach((p, i) =>
MyPromise.resolve(p).then(v => { out[i] = v; if (--left === 0) resolve(out); }, reject));
});
}
static race(iter) {
return new MyPromise((res, rej) => { for (const p of iter) MyPromise.resolve(p).then(res, rej); });
}
static allSettled(iter) {
return MyPromise.all([...iter].map(p => MyPromise.resolve(p).then(
value => ({ status: 'fulfilled', value }), reason => ({ status: 'rejected', reason }))));
}
}
PASS resolve/then
PASS reject/catch
PASS thenable adoption
PASS nested promise
PASS all
PASS race
PASS allSettled
PASS throw in handler propagates
PASS settle once
PASS finally passthrough
PASS async ordering
The three details interviewers look for: then always returns a new promise (that is what makes
chaining work), handlers always run in a microtask even if the promise is already settled (so ordering
is deterministic), and resolving with a thenable adopts its state recursively.
6.6 async/await is generators plus a driver
async function desugars to a generator whose yields are awaits, driven by a trampoline:
function toAsync(genFn) {
return function (...args) {
const it = genFn.apply(this, args);
return new Promise((resolve, reject) => {
const step = (method, arg) => {
let r;
try { r = it[method](arg); } catch (e) { return reject(e); }
if (r.done) return resolve(r.value);
Promise.resolve(r.value).then(v => step('next', v), e => step('throw', e));
};
step('next');
});
};
}
const load = toAsync(function* () {
const a = yield Promise.resolve(1);
const b = yield Promise.resolve(a + 1);
return a + b;
});
load().then(console.log); // 3
That is exactly what Babel’s regenerator emitted, and it is the answer to “how does await work under
the hood”.
6.7 The error-handling traps
// 1. await inside forEach does nothing — forEach ignores the returned promise
[1, 2, 3].forEach(async n => { await sleep(10); console.log(n); }); // returns immediately
for (const n of [1, 2, 3]) { await sleep(10); console.log(n); } // actually sequential
// 2. sequential vs parallel
const a = await fetchA(); const b = await fetchB(); // 2 round trips, serialized
const [a2, b2] = await Promise.all([fetchA(), fetchB()]); // 1 round trip of latency
// 3. unhandled rejection from a promise created but awaited later
const p = risky(); // rejects at t=0
await sleep(1000); // by now Node may have reported an unhandled rejection
try { await p; } catch {} // too late in some Node versions
// fix: attach the handler immediately, or create the promise where you await it
// 4. throwing inside a non-awaited async function is invisible
async function bg() { throw new Error('lost'); }
bg(); // unhandledRejection
bg().catch(reportError); // correct
// 5. try/finally around await, and the return-in-finally trap
async function f() { try { return 'a'; } finally { return 'b'; } } // resolves 'b'
Node 22 default for an unhandled rejection is --unhandled-rejections=throw, i.e. the process
crashes. That is deliberate — a swallowed rejection is a silent data-corruption bug.
6.8 Cancellation and concurrency control
// AbortController is the standard cancellation channel; fetch, streams and timers honour it.
async function fetchWithTimeout(url, ms = 100) {
const ac = new AbortController();
const timer = setTimeout(() => ac.abort(new Error('timeout')), ms);
try {
const res = await fetch(url, { signal: ac.signal });
return await res.json();
} finally { clearTimeout(timer); }
}
// AbortSignal.timeout(ms) is the one-liner; AbortSignal.any([...]) combines signals.
A bounded-concurrency map, which is the “rate limit N parallel requests” question:
async function pMap(items, mapper, concurrency = 4) {
const out = new Array(items.length);
let i = 0, active = 0;
await new Promise((resolve, reject) => {
const next = () => {
if (i >= items.length && active === 0) return resolve();
while (active < concurrency && i < items.length) {
const idx = i++; active++;
Promise.resolve(mapper(items[idx], idx))
.then(v => { out[idx] = v; active--; next(); }, reject);
}
};
next();
});
return out;
}
results 0,1,4,9,16,25,36,49,64,81
peak concurrency 3 | elapsed ~ 200 ms (serial would be 500)
Follow-ups to be ready for: make it fail-soft (collect errors instead of rejecting), add retries with exponential backoff and jitter, preserve order (this does — it writes by index), and make it a stream so memory stays O(concurrency) instead of O(n).
AsyncLocalStorage (from node:async_hooks) is the Node answer to thread-local storage: it propagates
a context object across await boundaries, which is how request-scoped tracing and logging work
without threading a ctx parameter through every function.
Interview follow-ups
Q: Is JavaScript single-threaded?
A: The JavaScript execution is single-threaded per realm, but the runtime is not: libuv keeps a
threadpool (default 4, UV_THREADPOOL_SIZE) for filesystem, DNS and crypto work, and you can add real
threads with worker_threads. Network I/O uses the OS event notification mechanism, not the pool.
Q: Why is setTimeout(fn, 0) not zero?
A: HTML clamps nested timers to 4 ms after 5 levels; Node’s timer resolution means the minimum is 1 ms; and the callback still has to wait for the current synchronous execution plus the microtask drain plus the loop to reach the timers phase.
Q: What is the difference between process.nextTick and setImmediate?
A: Despite the names, nextTick fires before the loop continues (higher priority than promises)
and setImmediate fires in the next loop turn’s check phase. Prefer setImmediate for “yield to the
loop”; reserve nextTick for internal invariants because it can starve I/O.
Q: How do you make a blocking CPU task not freeze the server?
A: Move it off the loop: worker_threads for CPU-bound JS, a native addon or child process for
heavy work, or chunk it and await new Promise(r => setImmediate(r)) between chunks to let I/O breathe.
Q: Can a microtask starve the event loop?
A: Yes. A microtask that enqueues another microtask unconditionally never lets the loop advance —
no timers, no I/O. The same is true of recursive process.nextTick.
Q: What does await do to a non-promise?
A: It wraps it via Promise.resolve and still yields — so await 1 costs one microtask tick. That
is why “8 after await” appears in the microtask group in Puzzle 1.
7. Memory and garbage collection
7.1 V8’s generational collector
Two spaces, two very different collectors — the generational hypothesis is that most objects die young.
| Space | Collector | Mechanics | Cost |
|---|---|---|---|
| Young generation (“new space”, a few MB, two semi-spaces) | Scavenger (Cheney’s algorithm) | allocate in from-space; on GC copy live objects to to-space, swap. Objects surviving two scavenges are promoted to old space. | proportional to live data, so very cheap when most objects die |
| Old generation | Mark-Sweep-Compact | tri-colour mark (white/grey/black) from the roots, sweep free lists, compact when fragmented | proportional to heap size |
Modern V8 (“Orinoco”) makes the major GC mostly non-blocking: incremental marking interleaved with
JS, concurrent marking and sweeping on helper threads, parallel scavenging, and lazy sweeping. What
remains stop-the-world is short. Node flags worth knowing: --max-old-space-size=N (MB),
--expose-gc (then global.gc()), --trace-gc.
Roots are the stack, the global object, and handles held by native code. Reachability, not reference counting, decides liveness — which is why cycles are collected in JS but need a cycle detector in CPython (see Python core).
7.2 Weak references
| API | Holds | Use |
|---|---|---|
WeakMap<object, V> | key weakly | attach metadata/private state to objects you do not own; memo caches keyed by object |
WeakSet<object> | member weakly | “have I seen this object” marks (e.g. cycle detection in a deep clone) |
WeakRef<T> | target weakly, deref() may return undefined | caches you are willing to lose |
FinalizationRegistry | — | best-effort cleanup notification; never guaranteed to run |
WeakMap is not enumerable by design — being able to list the keys would expose GC timing.
7.3 The four leak archetypes
- Accidental globals / module-level accumulation.
const cache = new Map()at module scope that never evicts is the single most common Node leak. - Closures capturing more than they need. A closure keeps its whole scope alive; one long-lived callback can pin a large object graph. Extract just the field you need before closing over it.
- Timers and listeners never removed.
setIntervalkeeps its closure alive forever;emitter.onwithoutoffgrows the listener array (Node warns at 11 listeners). - Detached views/nodes still referenced. In the browser, removing a DOM node while JS still holds a reference keeps the whole subtree alive.
Finding one:
node --expose-gc app.js # exposes global.gc() so you can force a collection
// coarse: watch the trend, forcing GC between samples so you see retention, not garbage
setInterval(() => {
global.gc();
const { heapUsed, external, arrayBuffers } = process.memoryUsage();
console.log((heapUsed / 1e6).toFixed(1), 'MB heap |', (external / 1e6).toFixed(1), 'MB external');
}, 5000);
Then take two heap snapshots (node --inspect, Chrome DevTools Memory tab, or
require('v8').writeHeapSnapshot()) at points where memory should be equal, and use the
Comparison view: the retaining path of the top delta tells you which closure or map is holding on.
heapUsed growing while rss is flat means fragmentation, not a leak.
Interview follow-ups
Q: What exactly makes an object eligible for collection?
A: Unreachability from any root. Setting a variable to null only helps if it was the last strong
reference.
Q: Does delete obj.prop free memory?
A: It removes the reference (so the value may become collectable) but it also transitions the
object into dictionary mode, which is slower forever. Prefer obj.prop = undefined in hot objects.
Q: What is external memory in process.memoryUsage()?
A: C++ objects bound to JS objects — Buffers, ArrayBuffer backing stores, native addon memory.
It is not part of the V8 heap limit, so a buffer leak shows up here, not in heapUsed.
Q: Why can FinalizationRegistry not be used for resource cleanup?
A: Callbacks are best-effort: they may never run (process exit, engine choice), may run late, and
must not resurrect the object. Use try/finally, Symbol.dispose/using, or explicit close().
8. The V8 performance model
8.1 The compilation pipeline
source ──> scanner/parser ──> AST ──> Ignition (bytecode interpreter)
│ hot?
├──> Sparkplug (baseline, non-optimizing, near-instant compile)
├──> Maglev (mid-tier optimizing, since V8 11.x)
└──> TurboFan (top-tier optimizing, speculative)
│
deoptimize ──> back to Ignition when a speculation fails
Key ideas to be able to state:
- Lazy parsing. Function bodies are pre-parsed only, and fully parsed on first call. Wrapping a
function in parentheses (
(function(){})) is the old “eager parse” hint; the modern equivalent is not worrying about it. - Speculative optimization. TurboFan compiles code specialised on the types it has observed. If a function that always saw integers suddenly gets a string, the optimized code is thrown away (deoptimization) and the function re-warms. A single polymorphic call site can cost you the whole optimization.
- Optimization is per-function and feedback-driven; that is why microbenchmarks need warm-up runs and why the first measurement in a loop is worthless.
8.2 Hidden classes and inline caches
Every object has a hidden class (V8 calls it a Map) describing its shape: which properties exist, in
what order, at what offset. Adding a property creates a transition to a new hidden class. Objects
built the same way share a hidden class, and a property access site caches “if the shape is M, the
value is at offset 3” — an inline cache.
| IC state | Shapes seen | Behaviour |
|---|---|---|
| uninitialized | 0 | first run, records feedback |
| monomorphic | 1 | direct offset load — as fast as a C struct field |
| polymorphic | 2–4 | small linear scan of cached shapes |
| megamorphic | 5+ | falls back to a global hash lookup, and TurboFan largely gives up on the site |
Measured on this container (Node 22.22.2), same read loop compiled at four separate call sites, 20,000 objects, 2,000 iterations:
monomorphic (1 shape) 77.6 ms
polymorphic (2 shapes) 116.0 ms
polymorphic (4 shapes) 116.1 ms
megamorphic (8 shapes) 332.5 ms
Monomorphic to megamorphic is a 4.3x slowdown on pure property reads (a repeat run gave 4.6x). The practical rules:
- Initialize every field in the constructor, in the same order, even to
null. - Do not add or
deleteproperties after construction. {x, y}and{y, x}are different hidden classes. Object literal key order matters.- Prefer a
Mapwhen keys are genuinely dynamic — a plain object used as a dictionary goes into dictionary (slow, hash-backed) mode anyway, andMapis designed for it.
A warning about measuring this. My first attempt at this benchmark showed no difference, because
a shared counter++ in the loop body dominated the cost and the two cases shared one call site. Isolate
the call sites (a fresh function per case), keep the loop body trivial, warm up, and sanity-check that
the numbers move when you make the effect bigger. A benchmark that shows no difference is more often a
broken benchmark than a disproved theory.
8.3 Elements kinds: packed vs holey
Arrays have their own lattice of representations, and transitions are one-way (you can never go back to a more specific kind):
PACKED_SMI ──> PACKED_DOUBLE ──> PACKED_ELEMENTS
│ │ │
v v v
HOLEY_SMI ──> HOLEY_DOUBLE ──> HOLEY_ELEMENTS ──> DICTIONARY_ELEMENTS
SMI = small integer (31-bit tagged, no boxing). DOUBLE = unboxed floats. ELEMENTS = arbitrary
tagged values. Holey means the array has gaps, so every read must also consult the prototype chain
to be spec-correct — which is why it is slower.
PACKED_SMI 35.4 ms
HOLEY_SMI (one delete) 49.7 ms
HOLEY_SMI (50% holes) 68.8 ms
One delete arr[10] on a 20,000-element array made the sum loop measurably slower — 15% to 40%
depending on the run — and permanently, because the transition is one-way. (This is the noisiest
measurement in this guide; the direction is completely consistent, the magnitude is not.) Things that
create holes: new Array(n) without filling, delete, assigning past the end (a[a.length+1]=x), and
arr.length = biggerNumber.
const good = new Array(n).fill(0); // PACKED_SMI
const alsoGood = Array.from({ length: n }, () => 0);
const bad = new Array(n); // HOLEY_SMI from birth
DICTIONARY_ELEMENTS happens when an array becomes very sparse (a = []; a[100000] = 1); it is a hash
map wearing an array costume, and iteration collapses.
8.4 Strings
V8 has several string representations and concatenation does not copy:
| Representation | What it is |
|---|---|
SeqString | flat contiguous characters (one-byte or two-byte) |
ConsString | a rope node: left + right, built by + in O(1) |
SlicedString | a view: parent + offset + length, built by substring/slice in O(1) |
ThinString/internalized | deduplicated, for identifiers and literals |
A ConsString is flattened lazily when something needs the characters contiguously (a regex, a char
access, passing to C++). So:
+= in loop 37.1 ms len=400000
array push + join 34.8 ms len=400000
s += x in a loop is fine in V8 (it builds a rope), essentially tying push/join. This is the
opposite of Python, where += on str is quadratic unless CPython’s in-place optimization applies —
so the “always use join” advice is Python advice, not universal advice. What is expensive in JS is
forcing flattening repeatedly, and str.split('').reverse().join('') which allocates an array of
one-character strings (and is also wrong for non-BMP characters — use [...str]).
8.5 A checklist for hot JavaScript
- Keep object shapes stable and call sites monomorphic.
- Keep arrays packed and single-typed; preallocate with
fill. - Avoid
arguments,with,eval, anddeletein hot paths. - Do not mix types in a numeric array (
[1, 2, 'x']collapses toPACKED_ELEMENTS). - Prefer
for/for...ofoverforEachin the hottest loops (callback allocation and megamorphic dispatch), but measure — usually it does not matter. Map/Setfor dynamic keys, plain objects for fixed shapes.- Typed arrays (
Float64Array,Int32Array) when you have numeric bulk data — no boxing, cache-friendly. - Measure with
--prof+node --prof-process,--cpu-prof, orperf; read%OptimizeFunctionOnNextCall-style diagnostics only with--allow-natives-syntax. node --trace-deoptand--trace-opt-verbosetell you which functions keep getting thrown away.
Interview follow-ups
Q: What is a hidden class transition and why should I care?
A: Adding a property moves the object to a new shape. Two “identical” objects built in different orders have different shapes, which turns a monomorphic call site polymorphic and costs you the optimized code path.
Q: Why is arr.includes(x) slower than set.has(x)?
A: O(n) linear scan versus O(1) average hash lookup. The crossover is small — usually a few dozen elements — so for tiny arrays the linear scan wins on cache locality.
Q: What is deoptimization and what triggers it?
A: Discarding TurboFan code and resuming in the interpreter, triggered when a speculation fails:
an unexpected type, a hole in an array assumed packed, an object shape change, or arguments escaping.
Q: Is for...of slower than a for loop?
A: It allocates an iterator and a result object per step in principle; V8 escape-analyses most of that away for arrays. Treat them as equal until a profile says otherwise.
Q: How do you make numeric code fast in JS?
A: Typed arrays, avoid NaN/undefined sentinels in the data, keep arrays packed-double, use Math.fround
if you want float32 semantics, and consider WebAssembly for genuinely heavy kernels.
9. Modules
9.1 CJS vs ESM
| CommonJS | ES modules | |
|---|---|---|
| Syntax | require / module.exports | import / export |
| Resolution | synchronous, runtime, node_modules walk | asynchronous, spec algorithm, honours exports map |
| Bindings | value copy at require time | live bindings (a view on the exporter’s variable) |
| Evaluation | on first require, cached in require.cache | linked first (all imports hoisted), then evaluated depth-first |
this at top level | module.exports | undefined |
Top-level await | no | yes |
| Tree-shakeable | not statically | yes (static structure) |
Loading a .mjs from CJS | await import() only | — |
9.2 Live bindings, demonstrated
// counter.mjs
export let count = 0;
export const inc = () => ++count;
// main.mjs
import { count, inc } from './counter.mjs';
inc(); inc();
console.log(count); // 2 — the imported name tracks the exporter's variable
// count = 5; // TypeError: Assignment to constant variable (imports are read-only)
The CJS equivalent (const { count } = require('./counter')) would print 0, because destructuring
copied the number.
9.3 Circular imports
The behaviour differs, and it is a favourite question. Same two-module cycle, both systems:
[cjs] a: start
[cjs] b: start
[cjs] b: saw a.aVal = A-initial (partial export!) <- b got a HALF-BUILT module object
[cjs] a: saw b.bVal = B-final
CJS returns the partially-populated module.exports to break the cycle. Code that reads a value at
require time gets whatever existed at that moment — a real source of undefined is not a function.
[esm] b: start <- depth-first: b is evaluated before a
[esm] b: reading aVal threw ReferenceError - TDZ via live binding
[esm] a: start
[esm] a: saw bVal = B-final
ESM hoists the bindings but leaves them in the temporal dead zone, so a premature read throws a loud
ReferenceError instead of silently yielding undefined. Function declarations are hoisted and
initialized, which is why cycles between modules that only call each other’s functions work fine in
both systems.
9.4 Other module facts worth knowing
"type": "module"inpackage.jsonmakes.jsfiles ESM;.cjs/.mjsalways win over it.- The
exportsfield is the modern entry map and it blocks deep imports not listed in it. - The dual package hazard: shipping both CJS and ESM builds can load two copies of your module,
each with its own module-level state (two
Symbolregistries, two singletons,instanceoffailures). import()returns a promise for the namespace object and works in both systems; it is how you do lazy loading and how CJS reaches ESM.import.meta.urlreplaces__filename;import.meta.dirname/filenameexist in Node 20.11+;import.meta.resolve()resolves specifiers.- Import attributes:
import cfg from './c.json' with { type: 'json' }.
10. Node specifics
10.1 Streams and backpressure
Streams exist so you can process data larger than memory, and backpressure is the mechanism that
stops a fast producer from overwhelming a slow consumer. write() returning false means “the internal
buffer is above highWaterMark, wait for 'drain'”. pipe/pipeline handle this for you.
import { pipeline } from 'node:stream/promises';
import { createReadStream, createWriteStream } from 'node:fs';
import { createGzip } from 'node:zlib';
import { Transform } from 'node:stream';
const upper = new Transform({
transform(chunk, _enc, cb) { cb(null, chunk.toString().toUpperCase()); },
});
await pipeline(createReadStream('in.txt'), upper, createGzip(), createWriteStream('out.gz'));
Use pipeline (not .pipe()) because it propagates errors and destroys every stream in the chain;
a bare .pipe() leaks file descriptors on error. Async iteration works too:
for await (const chunk of readable).
10.2 Parallelism options
| Tool | Shares memory | Startup | Use for |
|---|---|---|---|
worker_threads | yes, via SharedArrayBuffer/MessageChannel (structured clone otherwise) | ~ms | CPU-bound JS inside one process |
child_process | no | ~10ms+ | running other programs, isolation |
cluster | no (shares the listening socket) | process-level | scaling an HTTP server across cores |
| libuv threadpool | internal | — | fs, dns.lookup, crypto.pbkdf2, zlib — tune with UV_THREADPOOL_SIZE |
Network I/O does not use the threadpool; it uses epoll/kqueue/IOCP. That is why thousands of idle
sockets cost almost nothing but four concurrent fs.readFiles can queue.
10.3 Buffers and binary
Buffer is a Uint8Array subclass with encoding helpers. Buffer.allocUnsafe(n) skips zero-filling
(faster, may expose old memory — always overwrite it fully). buf.subarray() shares memory;
Buffer.from(buf) copies. For structured binary, DataView gives you explicit endianness.
11. Modern syntax you should be fluent in
// Optional chaining and nullish coalescing
const city = user?.address?.city ?? 'unknown';
const first = list?.[0];
const out = fn?.(arg); // only calls if fn is not null/undefined
opts.retries ??= 3; // logical assignment: ??=, ||=, &&=
// Destructuring: rename, default, nested, rest, in parameters
const { a: alpha = 1, b: { c } = {}, ...rest } = obj;
const [x, , z = 0, ...tail] = arr;
function f({ id, tags = [] } = {}) {}
// Array methods that do NOT mutate (ES2023) — great for state updates
arr.toSorted((a, b) => a - b); arr.toReversed(); arr.with(2, 'new'); arr.toSpliced(1, 2);
arr.at(-1); // last element
arr.findLast(p); arr.findLastIndex(p);
arr.flat(Infinity); arr.flatMap(f);
// Grouping (ES2024)
Object.groupBy([1, 2, 3, 4, 5], n => (n % 2 ? 'odd' : 'even'));
// -> {"odd":[1,3,5],"even":[2,4]}
Map.groupBy(users, u => u.teamId); // Map keyed by anything
// Set algebra (ES2025) — verified on Node 22
const a = new Set([1, 2, 3]), b = new Set([3, 4]);
[...a.union(b)]; // [1,2,3,4]
[...a.intersection(b)]; // [3]
[...a.difference(b)]; // [1,2]
[...a.symmetricDifference(b)]; // [1,2,4]
a.isSubsetOf(b); a.isSupersetOf(b); a.isDisjointFrom(new Set([9])); // false false true
// Explicit resource management (ES2026)
class Conn { [Symbol.dispose]() { console.log('closed'); } }
{ using c = new Conn(); /* ... */ } // disposed at end of block, even on throw
// await using for [Symbol.asyncDispose]
// Structured deep clone, handles cycles, Map/Set/Date/RegExp/ArrayBuffer
const copy = structuredClone(graph); // NOT functions, DOM nodes, or prototypes
// Misc
Object.hasOwn(obj, 'k'); // replaces hasOwnProperty.call
String.prototype.replaceAll;
Array.fromAsync(asyncIterable);
AbortSignal.timeout(1000); AbortSignal.any([s1, s2]);
label: for (const i of xs) { for (const j of ys) if (bad) continue label; }
12. Interview questions
12.1 Implement these cold
Implement map, filter, reduce.
Array.prototype.myMap = function (cb, thisArg) {
const out = new Array(this.length);
for (let i = 0; i < this.length; i++) if (i in this) out[i] = cb.call(thisArg, this[i], i, this);
return out;
};
Array.prototype.myFilter = function (cb, thisArg) {
const out = [];
for (let i = 0; i < this.length; i++) if (i in this && cb.call(thisArg, this[i], i, this)) out.push(this[i]);
return out;
};
Array.prototype.myReduce = function (cb, ...init) {
let i = 0, acc;
if (init.length) acc = init[0];
else { while (i < this.length && !(i in this)) i++;
if (i >= this.length) throw new TypeError('Reduce of empty array with no initial value');
acc = this[i++]; }
for (; i < this.length; i++) if (i in this) acc = cb(acc, this[i], i, this);
return acc;
};
The i in this check (skipping holes) and the empty-array-without-initial-value TypeError are the
details that distinguish a real answer from a sketch.
Deep clone with cycles.
function deepClone(value, seen = new Map()) {
if (value === null || typeof value !== 'object') return value;
if (seen.has(value)) return seen.get(value); // handles cycles AND shared refs
if (value instanceof Date) return new Date(value);
if (value instanceof RegExp) return new RegExp(value.source, value.flags);
if (value instanceof Map) {
const m = new Map(); seen.set(value, m);
for (const [k, v] of value) m.set(deepClone(k, seen), deepClone(v, seen));
return m;
}
if (value instanceof Set) {
const s = new Set(); seen.set(value, s);
for (const v of value) s.add(deepClone(v, seen));
return s;
}
if (ArrayBuffer.isView(value)) return new value.constructor(value);
const out = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value));
seen.set(value, out);
for (const k of Reflect.ownKeys(value)) {
const d = Object.getOwnPropertyDescriptor(value, k);
if ('value' in d) d.value = deepClone(d.value, seen);
Object.defineProperty(out, k, d);
}
return out;
}
Say out loud: structuredClone does most of this natively now; hand-roll it only when you need to
handle functions, prototypes, or custom classes.
Deep equal.
function deepEqual(a, b, seen = new Set()) {
if (Object.is(a, b)) return true;
if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) return false;
if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) return false;
const pair = a; if (seen.has(pair)) return true; seen.add(pair);
if (a instanceof Date) return a.getTime() === b.getTime();
if (a instanceof RegExp) return a.source === b.source && a.flags === b.flags;
if (a instanceof Map) return a.size === b.size && [...a].every(([k, v]) => b.has(k) && deepEqual(v, b.get(k), seen));
if (a instanceof Set) return a.size === b.size && [...a].every(v => b.has(v));
const ka = Reflect.ownKeys(a), kb = Reflect.ownKeys(b);
return ka.length === kb.length && ka.every(k => Reflect.has(b, k) && deepEqual(a[k], b[k], seen));
}
Flatten with depth, iteratively.
function flatten(arr, depth = 1) {
const out = [], stack = arr.map(v => [v, depth]);
while (stack.length) {
const [v, d] = stack.shift();
if (Array.isArray(v) && d > 0) stack.unshift(...v.map(x => [x, d - 1]));
else out.push(v);
}
return out;
}
// Note: shift/unshift make this O(n^2); the O(n) version pushes and reverses, or recurses.
Event emitter.
class Emitter {
#m = new Map();
on(ev, fn) { (this.#m.get(ev) ?? this.#m.set(ev, new Set()).get(ev)).add(fn); return () => this.off(ev, fn); }
once(ev, fn) { const w = (...a) => { this.off(ev, w); fn(...a); }; return this.on(ev, w); }
off(ev, fn) { this.#m.get(ev)?.delete(fn); }
emit(ev, ...args) {
// copy first: a handler may add/remove handlers during emit
for (const fn of [...(this.#m.get(ev) ?? [])]) { try { fn(...args); } catch (e) { queueMicrotask(() => { throw e; }); } }
return (this.#m.get(ev)?.size ?? 0) > 0;
}
}
Retry with exponential backoff and jitter.
async function retry(fn, { attempts = 5, base = 100, cap = 5000, signal } = {}) {
let lastErr;
for (let i = 0; i < attempts; i++) {
try { return await fn(i); } catch (e) {
lastErr = e;
if (signal?.aborted || i === attempts - 1) break;
const exp = Math.min(cap, base * 2 ** i);
const wait = Math.random() * exp; // full jitter
await new Promise(r => setTimeout(r, wait));
}
}
throw lastErr;
}
Promisify a callback API.
const promisify = fn => (...args) =>
new Promise((res, rej) => fn(...args, (err, ...vals) => err ? rej(err) : res(vals.length > 1 ? vals : vals[0])));
once, chunk, groupBy, range.
const once = fn => { let done = false, val; return (...a) => (done ? val : (done = true, val = fn(...a))); };
const chunk = (a, n) => Array.from({ length: Math.ceil(a.length / n) }, (_, i) => a.slice(i * n, i * n + n));
const groupBy = (a, f) => a.reduce((m, v) => ((m[f(v)] ??= []).push(v), m), {});
const range = (n, start = 0, step = 1) => Array.from({ length: n }, (_, i) => start + i * step);
12.2 Rapid-fire Q&A
Q: null vs undefined?
A: undefined is “not assigned” (the engine produces it); null is “intentionally empty” (you
produce it). typeof null === 'object' is a famous bug. null == undefined is true, null === undefined
is false. JSON has null but not undefined.
Q: What is hoisting exactly?
A: Declarations are processed when the scope is entered. var is initialized to undefined;
function declarations are fully initialized; let/const/class are created but uninitialized (TDZ),
so touching them throws.
Q: Why does typeof NaN === 'number'?
A: NaN is an IEEE-754 double whose exponent is all ones and mantissa non-zero. It is a number that is not a numeric value.
Q: == vs === vs Object.is?
A: === is SameValueZero-minus-zero-handling: strict, no coercion, but NaN !== NaN and +0 === -0.
Object.is fixes both. == coerces per the abstract equality algorithm.
Q: What is a closure, in one sentence, without the word “closure”?
A: A function together with the variable environment it was created in, which stays alive as long as the function does.
Q: Explain event delegation.
A: Attach one listener to a common ancestor and use event.target/closest() to find which
descendant was hit. Fewer listeners, works for nodes added later, relies on bubbling.
Q: Capturing vs bubbling?
A: Dispatch goes capture (window -> target), then target, then bubble (target -> window).
addEventListener(ev, fn, true) or {capture: true} listens on the way down.
stopPropagation halts traversal; stopImmediatePropagation also skips other listeners on the same node.
Q: What is a Proxy good for?
A: Intercepting fundamental operations (get, set, has, deleteProperty, ownKeys, apply,
construct). Real uses: reactivity systems (Vue 3), negative array indices, validation, lazy remote
objects, immutability enforcement. Cost: every trapped access is a function call, and it defeats inline
caches.
Q: Reflect — why does it exist?
A: It gives function forms of the internal operations that proxy traps mirror, with better return
values than the old equivalents (Reflect.defineProperty returns a boolean instead of throwing) and
correct receiver forwarding for inherited accessors.
Q: Explain currying vs partial application.
A: Currying turns an n-ary function into n unary functions; partial application fixes some arguments and returns a function of the rest. Currying is a special case of the general idea.
Q: What is the difference between Array.from and spread?
A: Spread requires an iterable. Array.from accepts array-likes too ({length: 3}) and takes a
map function, which lets you avoid an intermediate array: Array.from({length: n}, (_, i) => i).
Q: Why is [10, 9, 1].sort() [1, 10, 9]?
A: The default comparator converts to strings and compares UTF-16 code units. Always pass
(a, b) => a - b for numbers.
Q: Is sort stable?
A: Yes, guaranteed since ES2019 and implemented as TimSort in V8. TypedArray.prototype.sort is a
separate, numerically-ordered path.
Q: What does JSON.parse(JSON.stringify(x)) lose?
A: undefined, functions, symbols, Map/Set contents, Date type (becomes a string), Infinity/NaN
(become null), prototypes, cycles (throws), and BigInt (throws).
Q: Object.keys vs for...in vs Object.getOwnPropertyNames vs Reflect.ownKeys?
A: Own enumerable string keys / inherited and own enumerable string keys / own string keys including non-enumerable / everything own, strings and symbols.
Q: What is the temporal dead zone for?
A: It turns “used before initialized” from a silent undefined into a loud error, and it is what
makes const meaningfully constant rather than “assigned later”.
Q: Debounce vs throttle in one line each?
A: Debounce: wait for quiet. Throttle: at most once per interval.
Q: How do you deep-freeze an object?
A: Recurse over Object.getOwnPropertyNames, freeze each object-valued property, guard against
cycles with a WeakSet, then freeze the root.
Q: What is the difference between Map and a plain object?
A: Map accepts any key type, preserves insertion order for all keys, has size, is iterable, has
no prototype keys to collide with, and is optimized for frequent add/delete. Objects are faster for
small fixed shapes and are what JSON gives you.
Q: What happens if you await inside a for loop over 1000 URLs?
A: 1000 sequential round trips. Batch with Promise.all, or bound it with a concurrency pool —
unbounded Promise.all over 1000 fetches will hit socket limits and blow memory.
Q: What is the output of console.log(typeof function(){}, typeof class{}, typeof Symbol(), typeof 10n)?
A: function function symbol bigint. class is a function; typeof has no separate class tag.
Q: let in a for loop — how many bindings?
A: One per iteration, which is why closures created inside capture distinct values. var creates
one binding for the whole loop.
Q: How would you detect a memory leak in a Node service?
A: Trend process.memoryUsage().heapUsed over time with GC forced between samples, then diff two
heap snapshots taken at equivalent points and inspect the retaining path of the largest delta.
Q: What is globalThis for?
A: One name for the global object across browsers (window), workers (self), and Node (global).
Q: Why avoid eval and with?
A: They make the scope dynamic, so the engine cannot resolve variables statically — it disables
optimization for the whole enclosing function, and eval on untrusted input is code injection.
Q: What is tree shaking and what breaks it?
A: Static dead-code elimination over ESM. Breakers: CJS require, dynamic property access on a
namespace, and side effects at module top level (mark packages "sideEffects": false when true).
Q: Explain Symbol in one paragraph.
A: A unique, non-string primitive usable as a property key that cannot collide and is skipped by
Object.keys/JSON.stringify. Well-known symbols (Symbol.iterator, asyncIterator, toPrimitive,
hasInstance, toStringTag, dispose) are the protocol hooks. Symbol.for uses a cross-realm registry.
Q: What does void 0 mean and why is it used?
A: It evaluates its operand and returns undefined. Historically used because undefined was
writable in ES3; now mostly seen in minified output and void asyncFn() to mark a deliberately
un-awaited promise.
Q: Explain Array.prototype.reduce’s type-level pain in TypeScript.
A: The accumulator type is inferred from the initial value, so [].reduce((a, b) => ..., {}) widens
to {} and then rejects your writes. Annotate the initial value or pass a type argument:
reduce<Record<string, number>>(...).
Q: What is the difference between shallow and deep equality in React-style code?
A: Shallow compares one level of keys by reference — cheap, and why you must produce new objects on state change. Deep equality is O(size) and hides accidental re-creation, which is why hooks use reference identity.
13. Feature availability on Node 22
Feature-detected on this container (Node 22.22.2). Useful because “available in ES2026” and “available in your runtime” are different questions, and an interviewer may run your code.
| Feature | Node 22 |
|---|---|
Iterator helpers (.map/.filter/.take/... on iterators) | yes |
Set methods (union, intersection, difference, …) | yes |
Object.groupBy / Map.groupBy | yes |
Array.fromAsync | yes |
Symbol.dispose / using | yes (symbol present; using syntax needs a recent V8 or transpiler) |
Array.prototype.toSorted/toReversed/with/toSpliced | yes |
Object.hasOwn, structuredClone, AbortSignal.timeout | yes |
Promise.try | no |
RegExp.escape | no |
Error.isError | no |
Float16Array / Math.f16round | no |
Math.sumPrecise | no |
Temporal | no (behind a flag / polyfill) |
Map.prototype.getOrInsert | no |
Uint8Array.prototype.toBase64 | no |
Sources for the runtime and spec claims are listed in the README.
Verify it yourself
js-core/01-numbers.js
// IEEE-754 doubles and integer safety
console.log('0.1 + 0.2 =', 0.1 + 0.2);
console.log('0.1 + 0.2 === 0.3 =', 0.1 + 0.2 === 0.3);
console.log('(0.1+0.2).toFixed(20) =', (0.1 + 0.2).toFixed(20));
console.log('Number.EPSILON =', Number.EPSILON);
console.log('nearlyEqual(0.1+0.2, 0.3) =', Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON);
console.log('MAX_SAFE_INTEGER =', Number.MAX_SAFE_INTEGER);
console.log('2**53 =', 2 ** 53);
console.log('2**53 + 1 =', 2 ** 53 + 1);
console.log('2**53 === 2**53 + 1 =', 2 ** 53 === 2 ** 53 + 1);
console.log('isSafeInteger(2**53) =', Number.isSafeInteger(2 ** 53));
console.log('MAX_VALUE =', Number.MAX_VALUE);
console.log('MAX_VALUE * 2 =', Number.MAX_VALUE * 2);
console.log('MIN_VALUE (denormal) =', Number.MIN_VALUE);
console.log('0.1 + 0.7 =', 0.1 + 0.7);
console.log('(0.1+0.7)*10 =', (0.1 + 0.7) * 10);
console.log('Math.floor((0.1+0.7)*10) =', Math.floor((0.1 + 0.7) * 10));
// BigInt
const big = 2n ** 53n;
console.log('BigInt 2n**53n + 1n =', big + 1n);
console.log('typeof 1n =', typeof 1n);
console.log('1n == 1 =', 1n == 1);
console.log('1n === 1 =', 1n === 1);
try { console.log(1n + 1); } catch (e) { console.log('1n + 1 throws =', e.constructor.name + ': ' + e.message); }
console.log('BigInt(Number.MAX_SAFE_INTEGER) * 2n =', BigInt(Number.MAX_SAFE_INTEGER) * 2n);
// -0
console.log('-0 === 0 =', -0 === 0);
console.log('Object.is(-0, 0) =', Object.is(-0, 0));
console.log('1 / -0 =', 1 / -0);
console.log('(-0).toString() =', (-0).toString());
console.log('String(-0) =', String(-0));
console.log('JSON.stringify(-0) =', JSON.stringify(-0));
console.log('[-0].includes(0) =', [-0].includes(0)); // SameValueZero -> true
console.log('[-0].indexOf(0) =', [-0].indexOf(0)); // strict equality -> 0
console.log('Math.round(-0.4) =', Math.round(-0.4));
console.log('Math.sign(-0) =', Math.sign(-0));
// NaN
console.log('NaN === NaN =', NaN === NaN);
console.log('Object.is(NaN, NaN) =', Object.is(NaN, NaN));
console.log('[NaN].includes(NaN) =', [NaN].includes(NaN));
console.log('[NaN].indexOf(NaN) =', [NaN].indexOf(NaN));
console.log('new Set([NaN, NaN]).size =', new Set([NaN, NaN]).size);
console.log('typeof NaN =', typeof NaN);
console.log('isNaN("foo") =', isNaN('foo'));
console.log('Number.isNaN("foo") =', Number.isNaN('foo'));
console.log('Math.max() =', Math.max());
console.log('Math.min() =', Math.min());
// Precision helpers
console.log('Math.sumPrecise([0.1,0.2])=', Math.sumPrecise ? Math.sumPrecise([0.1, 0.2]) : 'unavailable');
console.log('0.1+0.2 rounded to 2dp =', Math.round((0.1 + 0.2) * 100) / 100);
js-core/02-equality.js
// Abstract equality surprises, laid out as a table
const cases = [
['[] == ![]', () => [] == ![]],
['[] == false', () => [] == false],
['[] == ""', () => [] == ''],
['[] == 0', () => [] == 0],
['[[]] == 0', () => [[]] == 0],
['[0] == false', () => [0] == false],
['[1] == true', () => [1] == true],
['[2] == true', () => [2] == true],
['null == undefined', () => null == undefined],
['null === undefined', () => null === undefined],
['null == 0', () => null == 0],
['null >= 0', () => null >= 0],
['null > 0', () => null > 0],
['undefined == 0', () => undefined == 0],
['NaN == NaN', () => NaN == NaN],
['"" == 0', () => '' == 0],
['"0" == 0', () => '0' == 0],
['"" == "0"', () => '' == '0'],
['" \\t\\n" == 0', () => ' \t\n' == 0],
['false == "false"', () => false == 'false'],
['false == "0"', () => false == '0'],
['true == "1"', () => true == '1'],
['null == false', () => null == false],
['undefined == false', () => undefined == false],
['{} == "[object Object]"', () => ({}) == '[object Object]'],
['new String("a") == "a"', () => new String('a') == 'a'],
['new String("a") === "a"', () => new String('a') === 'a'],
['Symbol() == Symbol()', () => Symbol() == Symbol()],
['1n == 1', () => 1n == 1],
['document?"":"" ', () => true],
];
const pad = (s, n) => String(s).padEnd(n);
console.log(pad('expression', 26) + '| result');
console.log('-'.repeat(26) + '+' + '-'.repeat(8));
for (const [label, fn] of cases.slice(0, 29)) {
console.log(pad(label, 26) + '| ' + fn());
}
// Non-transitivity of ==
console.log('\nNon-transitive:');
console.log(' "" == 0 ->', '' == 0);
console.log(' 0 == "0" ->', 0 == '0');
console.log(' "" == "0" ->', '' == '0');
// ToPrimitive walkthrough
console.log('\nToPrimitive trace for `obj + ""` with default hint:');
const traced = {
[Symbol.toPrimitive](hint) {
console.log(' Symbol.toPrimitive called with hint:', hint);
return hint === 'number' ? 42 : 'forty-two';
},
};
console.log(' +traced ->', +traced);
console.log(' `${traced}` ->', `${traced}`);
console.log(' traced + "" ->', traced + '');
console.log(' traced * 2 ->', traced * 2);
console.log(' String(traced)->', String(traced));
console.log(' traced == 42 ->', traced == 42);
console.log('\nWithout Symbol.toPrimitive, valueOf then toString (default/number hint):');
const vt = {
valueOf() { console.log(' valueOf'); return 7; },
toString() { console.log(' toString'); return 'seven'; },
};
console.log(' vt + 1 ->', vt + 1);
console.log(' `${vt}` ->', `${vt}`);
console.log(' vt * 2 ->', vt * 2);
console.log(' [vt] + ""->', [vt] + '');
// Date is the special case: default hint behaves like string
const d = new Date(0);
console.log('\nDate default hint prefers string:');
console.log(' d + 1 (string concat) ->', typeof (d + 1));
console.log(' d - 1 (numeric) ->', d - 1);
// Array.prototype.join is why [] == ""
console.log('\n[].toString() ->', JSON.stringify([].toString()));
console.log('[1,2].toString() ->', JSON.stringify([1, 2].toString()));
console.log('[null, undefined].toString() ->', JSON.stringify([null, undefined].toString()));
console.log('[[1,[2]],3].toString() ->', JSON.stringify([[1, [2]], 3].toString()));
// Object.is vs === vs ==
const rows = [
['0, -0'],
['NaN, NaN'],
['1, "1"'],
['null, undefined'],
];
const vals = [[0, -0], [NaN, NaN], [1, '1'], [null, undefined]];
console.log('\n' + pad('a, b', 18) + '| == | === | Object.is');
console.log('-'.repeat(18) + '+-------+-------+----------');
vals.forEach(([a, b], i) => {
console.log(
pad(rows[i][0], 18) + '| ' + pad(a == b, 6) + '| ' + pad(a === b, 6) + '| ' + Object.is(a, b)
);
});
// SameValueZero (Map/Set/includes) vs SameValue (Object.is)
const m = new Map();
m.set(NaN, 'nan').set(-0, 'minus zero');
console.log('\nMap keyed by NaN ->', m.get(NaN));
console.log('Map.get(0) after set(-0) ->', m.get(0), '(SameValueZero collapses -0 and 0)');
console.log('Map key stored as ->', Object.is([...m.keys()][1], 0) ? '+0 (normalized)' : '-0');
js-core/03-scope.js
'use strict';
// --- hoisting ---
console.log('typeof hoistedFn :', typeof hoistedFn); // 'function'
console.log('varX before decl :', typeof varX, varX); // undefined
try { letY; } catch (e) { console.log('letY before decl :', e.constructor.name + ': ' + e.message); }
function hoistedFn() {}
var varX = 1;
let letY = 2;
// TDZ is per-binding, not per-file
{
try {
// eslint-disable-next-line no-use-before-define
console.log(shadow);
} catch (e) {
console.log('TDZ inside block :', e.constructor.name);
}
let shadow = 'inner';
console.log('after decl :', shadow);
}
// typeof is NOT safe for TDZ bindings (it is safe for undeclared ones)
console.log('typeof neverDeclared :', typeof neverDeclared);
try { typeof tdzVar; } catch (e) { console.log('typeof tdzVar :', e.constructor.name); }
let tdzVar = 1;
// const is binding-immutable, not value-immutable
const cfg = { a: 1 };
cfg.a = 2;
console.log('const object mutated :', cfg);
try { eval('cfg = {}'); } catch (e) { console.log('reassign const :', e.constructor.name); }
console.log('frozen :', Object.isFrozen(Object.freeze(cfg)));
try { cfg.a = 99; } catch (e) { console.log('write to frozen :', e.constructor.name, '(strict); silent no-op in sloppy'); }
console.log('after frozen write :', cfg);
// --- the classic loop bug ---
console.log('\nvar loop (bug):');
const varFns = [];
for (var i = 0; i < 3; i++) varFns.push(() => i);
console.log(' ', varFns.map((f) => f()));
console.log('let loop (fix 1 - per-iteration binding):');
const letFns = [];
for (let j = 0; j < 3; j++) letFns.push(() => j);
console.log(' ', letFns.map((f) => f()));
console.log('IIFE (fix 2):');
const iifeFns = [];
for (var k = 0; k < 3; k++) iifeFns.push(((captured) => () => captured)(k));
console.log(' ', iifeFns.map((f) => f()));
console.log('bind (fix 3):');
const bindFns = [];
for (var m = 0; m < 3; m++) bindFns.push(((x) => x).bind(null, m));
console.log(' ', bindFns.map((f) => f()));
// let in a for loop copies the binding forward each iteration
const seen = [];
for (let n = 0; n < 3; n++) { setTimeout(() => seen.push(n), 0); }
setTimeout(() => console.log('let + setTimeout :', seen), 1);
// closure over a shared cell
function counter() {
let count = 0;
return { inc: () => ++count, get: () => count };
}
const c1 = counter(), c2 = counter();
c1.inc(); c1.inc(); c2.inc();
console.log('\nindependent closures:', c1.get(), c2.get());
// function declarations in blocks (Annex B / strict differences)
{
function blockFn() { return 'block'; }
console.log('blockFn in block :', blockFn());
}
console.log('blockFn after block :', typeof blockFn); // 'function' in sloppy web-compat, 'undefined' in strict module
// arguments vs rest, and why leaking arguments hurts
function sum() { return Array.prototype.reduce.call(arguments, (a, b) => a + b, 0); }
function sumRest(...xs) { return xs.reduce((a, b) => a + b, 0); }
console.log('sum(1,2,3) :', sum(1, 2, 3), '| sumRest(1,2,3):', sumRest(1, 2, 3));
// function .length ignores rest and defaults-and-after
function f1(a, b) {}
function f2(a, b = 1, c) {}
function f3(a, ...rest) {}
function f4({ a, b }, [c]) {}
console.log('lengths :', f1.length, f2.length, f3.length, f4.length);
js-core/04-this.js
'use strict';
// 1. Default binding
function showThis() { return this; }
console.log('strict default this :', showThis()); // undefined
const sloppy = new Function('return this;'); // compiled as sloppy
console.log('sloppy default this :', sloppy() === globalThis ? 'globalThis' : sloppy());
// 2. Implicit binding — the call site decides
const obj = { name: 'obj', who() { return this?.name; } };
console.log('obj.who() :', obj.who());
const detached = obj.who;
console.log('detached() :', detached()); // undefined (strict)
// implicit binding is lost through any intermediate
const wrapper = { name: 'wrapper', inner: obj };
console.log('wrapper.inner.who() :', wrapper.inner.who()); // "obj" - only the LAST . matters
console.log('(0, obj.who)() :', (0, obj.who)()); // undefined - comma op strips the reference
// 3. Explicit binding
console.log('call/apply/bind :', obj.who.call({ name: 'call' }),
obj.who.apply({ name: 'apply' }), obj.who.bind({ name: 'bind' })());
// hard binding wins over later rebinding
const hard = obj.who.bind({ name: 'first' });
console.log('rebind a bound fn :', hard.call({ name: 'second' })); // "first"
// 4. new binding
function Person(name) { this.name = name; }
Person.prototype.who = function () { return this.name; };
console.log('new binding :', new Person('newed').who());
// new beats bind (bound this is ignored, bound args are kept)
const BoundPerson = Person.bind({ name: 'ignored' });
console.log('new on bound ctor :', new BoundPerson('still-works').name);
// 5. Arrow functions: lexical this, no own arguments/new.target, not constructible
const lex = {
name: 'lex',
method() { return (() => this.name)(); },
// module-scope `this` is module.exports ({}) in CJS, undefined in ESM
arrow: () => (typeof this === 'undefined' ? 'undefined (ESM module scope)' : 'module.exports = ' + JSON.stringify(this)),
};
console.log('arrow in method :', lex.method());
console.log('arrow as method :', lex.arrow());
try { new (() => {})(); } catch (e) { console.log('new Arrow :', e.constructor.name); }
// 6. Class fields are per-instance and capture `this` when initialised with an arrow
class Widget {
label = 'widget';
onClickMethod() { return this?.label; }
onClickField = () => this.label; // bound at construction, one closure per instance
static kind = 'Widget';
static { this.registry = new Map(); } // static initialisation block (ES2022)
}
const w = new Widget();
const { onClickMethod, onClickField } = w;
console.log('detached method :', (() => { try { return onClickMethod(); } catch (e) { return e.constructor.name; } })());
console.log('detached field arrow :', onClickField());
console.log('static block ran :', Widget.registry instanceof Map, '| static field:', Widget.kind);
// 7. this in class bodies is the class itself for statics
class Meta { static self() { return this === Meta; } }
console.log('static this === class :', Meta.self());
// 8. new.target
function Guard() {
if (new.target === undefined) return 'called without new';
return 'constructed as ' + new.target.name;
}
console.log('Guard() :', Guard());
console.log('new Guard() :', new Guard() instanceof Guard ? 'object' : Guard.call({}));
class Base { constructor() { this.builtBy = new.target.name; } }
class Derived extends Base {}
console.log('new.target in super :', new Derived().builtBy);
// 9. Losing this in callbacks and the three fixes
class Timer {
ticks = 0;
incBroken() { this.ticks++; }
incArrow = () => { this.ticks++; };
}
const t = new Timer();
const results = [];
try { [1].forEach(t.incBroken); } catch (e) { results.push('forEach(t.incBroken) -> ' + e.constructor.name); }
[1].forEach(t.incBroken.bind(t)); results.push('bind -> ticks=' + t.ticks);
[1].forEach(() => t.incBroken()); results.push('arrow wrap -> ticks=' + t.ticks);
[1].forEach(t.incArrow); results.push('field arrow -> ticks=' + t.ticks);
[1].forEach(t.incBroken, t); results.push('thisArg -> ticks=' + t.ticks);
console.log('\n' + results.join('\n'));
// 10. this inside a getter and inside a Proxy trap
const g = { _v: 5, get v() { return this._v; } };
console.log('\ngetter this :', g.v, Object.getOwnPropertyDescriptor(g, 'v').get.call({ _v: 9 }));
js-core/05-prototypes.js
'use strict';
// --- property descriptors ---
const o = {};
Object.defineProperty(o, 'hidden', { value: 1 }); // all flags default false
Object.defineProperty(o, 'shown', { value: 2, enumerable: true, writable: true, configurable: true });
console.log('descriptors:', Object.getOwnPropertyDescriptors(o));
console.log('Object.keys :', Object.keys(o));
console.log('for..in :', (() => { const r = []; for (const k in o) r.push(k); return r; })());
console.log('getOwnPropertyNames:', Object.getOwnPropertyNames(o));
console.log('JSON :', JSON.stringify(o));
try { o.hidden = 9; } catch (e) { console.log('write non-writable:', e.constructor.name); }
// accessor descriptor
const temp = {
_c: 0,
get f() { return this._c * 9 / 5 + 32; },
set f(v) { this._c = (v - 32) * 5 / 9; },
};
temp.f = 212;
console.log('\ngetter/setter: _c =', temp._c, ' f =', temp.f);
console.log('descriptor of f:', Object.getOwnPropertyDescriptor(temp, 'f'));
// defineProperty vs assignment: assignment consults setters up the chain
const proto = { set x(v) { this._x = v * 2; } };
const child = Object.create(proto);
child.x = 5;
console.log('\nassignment hits proto setter -> _x =', child._x, '| own x?', Object.hasOwn(child, 'x'));
Object.defineProperty(child, 'x', { value: 5, enumerable: true, writable: true, configurable: true });
console.log('defineProperty bypasses setter -> own x =', child.x, '| own?', Object.hasOwn(child, 'x'));
// --- prototype chain resolution ---
const grand = { greet() { return 'grand'; }, tag: 'G' };
const mid = Object.create(grand, { greet: { value() { return 'mid'; }, enumerable: false } });
const leaf = Object.create(mid);
console.log('\nleaf.greet() :', leaf.greet());
console.log('leaf.tag :', leaf.tag, '(found 2 links up)');
console.log('chain :',
[leaf, Object.getPrototypeOf(leaf), Object.getPrototypeOf(mid), Object.getPrototypeOf(grand),
Object.getPrototypeOf(Object.prototype)]
.map((p) => (p === null ? 'null' : p === grand ? 'grand' : p === mid ? 'mid' : p === Object.prototype ? 'Object.prototype' : 'leaf'))
.join(' -> '));
// shadowing: writing to leaf creates an own property, it does not change grand
leaf.tag = 'L';
console.log('after shadow : leaf.tag =', leaf.tag, ', grand.tag =', grand.tag);
// null-prototype objects (safe dictionaries)
const dict = Object.create(null);
dict.toString = 'not a function, and that is fine';
console.log('\nnull-proto dict:', dict.toString, '| has hasOwnProperty?', 'hasOwnProperty' in dict);
console.log('Object.hasOwn still works:', Object.hasOwn(dict, 'toString'));
// __proto__ vs prototype vs getPrototypeOf
function Ctor() {}
const inst = new Ctor();
console.log('\nCtor.prototype === Object.getPrototypeOf(inst) :', Ctor.prototype === Object.getPrototypeOf(inst));
console.log('inst.prototype :', inst.prototype);
console.log('Object.getPrototypeOf(Ctor) === Function.prototype:', Object.getPrototypeOf(Ctor) === Function.prototype);
console.log('Ctor.prototype.constructor === Ctor :', Ctor.prototype.constructor === Ctor);
console.log('"__proto__" own on {}? :', Object.hasOwn({}, '__proto__'),
'(it is an accessor on Object.prototype)');
// instanceof and Symbol.hasInstance
class Even { static [Symbol.hasInstance](n) { return typeof n === 'number' && n % 2 === 0; } }
console.log('\n4 instanceof Even :', 4 instanceof Even, '| 5 instanceof Even :', 5 instanceof Even);
function Legacy() {}
const legacyInst = new Legacy();
Legacy.prototype = {}; // rebinding prototype breaks old instances
console.log('legacyInst instanceof Legacy after prototype swap :', legacyInst instanceof Legacy);
console.log('[] instanceof Array :', [] instanceof Array, '| Array.isArray([]) :', Array.isArray([]));
// isPrototypeOf / in / hasOwn
console.log('\ngrand.isPrototypeOf(leaf) :', grand.isPrototypeOf(leaf));
console.log('"greet" in leaf :', 'greet' in leaf, '| Object.hasOwn(leaf,"greet") :', Object.hasOwn(leaf, 'greet'));
js-core/06-classes.js
'use strict';
// ES2022+ class with every modern feature
class Animal {
static #count = 0;
static registry = new Map();
static { Animal.registry.set('init', true); } // static initialisation block
#secret; // private field
name;
constructor(name) {
this.name = name;
this.#secret = `${name}-${++Animal.#count}`;
}
static get count() { return Animal.#count; }
static isAnimal(o) { return #secret in o; } // ergonomic brand check (ES2022)
get id() { return this.#secret; }
#privateMethod() { return 'private:' + this.name; }
callPrivate() { return this.#privateMethod(); }
speak() { return `${this.name} makes a noise`; }
toString() { return `Animal(${this.name})`; }
}
class Dog extends Animal {
constructor(name) { super(name); this.kind = 'dog'; }
speak() { return `${super.speak()} — specifically a bark`; }
}
const d = new Dog('Rex');
console.log('speak :', d.speak());
console.log('id :', d.id, '| count:', Animal.count);
console.log('brand check:', Animal.isAnimal(d), Animal.isAnimal({}));
console.log('callPrivate:', d.callPrivate());
try { JSON.parse('{}'); console.log('private access from outside:', eval('d.#secret')); }
catch (e) { console.log('outside access:', e.constructor.name); }
console.log('instanceof :', d instanceof Dog, d instanceof Animal);
console.log('proto chain:', [Dog.prototype, Animal.prototype].map((p) => p.constructor.name).join(' -> '));
console.log('static inherited:', Dog.count, '| Object.getPrototypeOf(Dog) === Animal:', Object.getPrototypeOf(Dog) === Animal);
// classes are not hoisted-callable and are always strict
try { new (class { constructor() { undeclared = 1; } })(); }
catch (e) { console.log('class body is strict:', e.constructor.name); }
try { Animal('x'); } catch (e) { console.log('call class w/o new :', e.constructor.name); }
console.log('class methods enumerable? :',
Object.getOwnPropertyDescriptor(Animal.prototype, 'speak').enumerable);
// --- the ES5 equivalent of `class Dog extends Animal` ---
function AnimalES5(name) {
if (!(this instanceof AnimalES5)) throw new TypeError("Class constructor cannot be invoked without 'new'");
this.name = name;
}
AnimalES5.prototype.speak = function () { return this.name + ' makes a noise'; };
Object.defineProperty(AnimalES5.prototype, 'speak', { enumerable: false });
function DogES5(name) {
AnimalES5.call(this, name); // == super(name)
this.kind = 'dog';
}
DogES5.prototype = Object.create(AnimalES5.prototype, {
constructor: { value: DogES5, writable: true, configurable: true, enumerable: false },
});
Object.setPrototypeOf(DogES5, AnimalES5); // == static inheritance
DogES5.prototype.speak = function () {
return AnimalES5.prototype.speak.call(this) + ' — specifically a bark';
};
const d5 = new DogES5('Rex5');
console.log('\nES5 equivalent :', d5.speak(), '| instanceof AnimalES5:', d5 instanceof AnimalES5);
// --- why `super` breaks with an assigned function ---
const base = { hello() { return 'base hello'; } };
const good = { __proto__: base, hello() { return super.hello() + ' + shorthand'; } };
console.log('\nmethod shorthand + super :', good.hello());
const bad = { __proto__: base };
try {
// `super` is only legal syntactically inside a method definition; a function expression
// assigned to a property has no [[HomeObject]], so this is a SyntaxError at parse time.
eval('bad.hello = function () { return super.hello(); };');
} catch (e) {
console.log('assigned function + super:', e.constructor.name + ': ' + e.message.split('\n')[0]);
}
// Reparenting a shorthand method keeps its ORIGINAL [[HomeObject]]:
const other = { hello() { return 'other hello'; } };
const moved = { __proto__: other, hello: good.hello };
console.log('reparented shorthand :', moved.hello(), '(still resolves super through `base`)');
// --- mixins ---
const Serializable = (Base) => class extends Base {
serialize() { return JSON.stringify(this, (k, v) => (k.startsWith('_') ? undefined : v)); }
};
const Comparable = (Base) => class extends Base {
compareTo(other) { return this.name < other.name ? -1 : this.name > other.name ? 1 : 0; }
};
class Plain { constructor(name) { this.name = name; this._internal = 'skip'; } }
class Rich extends Serializable(Comparable(Plain)) {}
const r1 = new Rich('a'), r2 = new Rich('b');
console.log('\nmixin serialize :', r1.serialize());
console.log('mixin compareTo :', r1.compareTo(r2));
console.log('mixin chain :', (function walk(p, acc = []) {
return p === null ? acc.join(' -> ') : walk(Object.getPrototypeOf(p), [...acc, p.constructor?.name || '(anonymous mixin)']);
})(Object.getPrototypeOf(r1)));
// --- Symbol.species and extending built-ins ---
class MyArray extends Array { static get [Symbol.species]() { return Array; } }
const ma = MyArray.from([1, 2, 3]);
console.log('\nmapped instanceof MyArray :', ma.map((x) => x) instanceof MyArray, '(species -> Array)');
class MyArray2 extends Array {}
console.log('without species :', MyArray2.from([1]).map((x) => x) instanceof MyArray2);
js-core/07-functions.js
'use strict';
// ---------- curry ----------
function curry(fn, arity = fn.length) {
return function curried(...args) {
if (args.length >= arity) return fn.apply(this, args);
return function (...more) { return curried.apply(this, [...args, ...more]); };
};
}
const volume = (l, w, h) => l * w * h;
const cv = curry(volume);
console.log('curry:', cv(2)(3)(4), cv(2, 3)(4), cv(2)(3, 4), cv(2, 3, 4));
// placeholder currying (lodash-style _)
const _ = Symbol('placeholder');
function curryP(fn, arity = fn.length) {
return function next(prev) {
return (...args) => {
const merged = prev.slice();
let ai = 0;
for (let i = 0; i < merged.length && ai < args.length; i++) {
if (merged[i] === _) merged[i] = args[ai++];
}
while (ai < args.length) merged.push(args[ai++]);
const complete = merged.length >= arity && !merged.slice(0, arity).includes(_);
return complete ? fn(...merged.slice(0, arity)) : next(merged);
};
}([]);
}
const div = (a, b) => a / b;
const cdiv = curryP(div);
console.log('curry w/ placeholder:', cdiv(_, 2)(10), cdiv(10)(2));
// partial application
const partial = (fn, ...bound) => (...rest) => fn(...bound, ...rest);
const partialRight = (fn, ...bound) => (...rest) => fn(...rest, ...bound);
console.log('partial:', partial(volume, 2, 3)(4), '| partialRight:', partialRight(div, 2)(10));
// ---------- composition ----------
const compose = (...fns) => (x) => fns.reduceRight((acc, f) => f(acc), x);
const pipe = (...fns) => (x) => fns.reduce((acc, f) => f(acc), x);
const inc = (n) => n + 1, dbl = (n) => n * 2, sq = (n) => n * n;
console.log('\ncompose(inc,dbl,sq)(3) =', compose(inc, dbl, sq)(3), '(sq -> dbl -> inc)');
console.log('pipe(inc,dbl,sq)(3) =', pipe(inc, dbl, sq)(3), '(inc -> dbl -> sq)');
// variadic-first pipe that supports multiple args on the first call
const pipeN = (first, ...rest) => (...args) => rest.reduce((acc, f) => f(acc), first(...args));
console.log('pipeN(volume, inc)(2,3,4) =', pipeN(volume, inc)(2, 3, 4));
// async pipe
const pipeAsync = (...fns) => (x) => fns.reduce((p, f) => p.then(f), Promise.resolve(x));
pipeAsync(async (n) => n + 1, (n) => n * 10)(1).then((v) => console.log('pipeAsync(1) =', v));
// ---------- memoize ----------
function memoize(fn, keyFn = (...a) => (a.length === 1 ? a[0] : JSON.stringify(a))) {
const cache = new Map();
const memo = function (...args) {
const key = keyFn(...args);
if (cache.has(key)) return cache.get(key);
const val = fn.apply(this, args);
cache.set(key, val);
return val;
};
memo.cache = cache;
return memo;
}
let calls = 0;
const slowFib = (n) => { calls++; return n < 2 ? n : fib(n - 1) + fib(n - 2); };
const fib = memoize(slowFib);
console.log('\nfib(35) =', fib(35), '| raw calls =', calls, '| cache size =', fib.cache.size);
// memoize with WeakMap for object keys — entries die with the key
function memoizeObj(fn) {
const cache = new WeakMap();
return (obj) => {
if (cache.has(obj)) return cache.get(obj);
const v = fn(obj);
cache.set(obj, v);
return v;
};
}
let heavyCalls = 0;
const summarize = memoizeObj((o) => { heavyCalls++; return Object.keys(o).length; });
const key1 = { a: 1, b: 2 };
console.log('memoizeObj:', summarize(key1), summarize(key1), '| computed', heavyCalls, 'time(s)');
// multi-arg memoize with a WeakMap trie (no stringification, GC-friendly)
function memoizeDeep(fn) {
const root = { primitives: new Map(), objects: new WeakMap(), has: false, value: undefined };
return (...args) => {
let node = root;
for (const a of args) {
const bucket = (a !== null && (typeof a === 'object' || typeof a === 'function')) ? node.objects : node.primitives;
if (!bucket.has(a)) bucket.set(a, { primitives: new Map(), objects: new WeakMap(), has: false });
node = bucket.get(a);
}
if (!node.has) { node.value = fn(...args); node.has = true; }
return node.value;
};
}
let deepCalls = 0;
const add3 = memoizeDeep((a, b, c) => { deepCalls++; return a + b + c; });
console.log('memoizeDeep:', add3(1, 2, 3), add3(1, 2, 3), '| computed', deepCalls, 'time(s)');
// ---------- debounce / throttle ----------
function debounce(fn, wait, { leading = false, trailing = true } = {}) {
let timer = null, lastArgs = null, lastThis = null, result;
const invoke = () => { result = fn.apply(lastThis, lastArgs); lastArgs = lastThis = null; };
const debounced = function (...args) {
lastArgs = args; lastThis = this;
const callNow = leading && timer === null;
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
timer = null;
if (trailing && lastArgs) invoke();
}, wait);
if (callNow) invoke();
return result;
};
debounced.cancel = () => { if (timer) clearTimeout(timer); timer = null; lastArgs = lastThis = null; };
debounced.flush = () => { if (timer) { clearTimeout(timer); timer = null; if (lastArgs) invoke(); } return result; };
return debounced;
}
function throttle(fn, wait, { leading = true, trailing = true } = {}) {
let last = 0, timer = null, lastArgs = null, lastThis = null;
const throttled = function (...args) {
const now = Date.now();
if (!last && !leading) last = now;
const remaining = wait - (now - last);
lastArgs = args; lastThis = this;
if (remaining <= 0 || remaining > wait) {
if (timer) { clearTimeout(timer); timer = null; }
last = now;
fn.apply(lastThis, lastArgs);
lastArgs = lastThis = null;
} else if (!timer && trailing) {
timer = setTimeout(() => {
last = leading ? Date.now() : 0;
timer = null;
if (lastArgs) { fn.apply(lastThis, lastArgs); lastArgs = lastThis = null; }
}, remaining);
}
};
throttled.cancel = () => { clearTimeout(timer); timer = null; last = 0; lastArgs = null; };
return throttled;
}
// deterministic demo: fire an event every 30ms for 300ms
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function demo() {
const t0 = Date.now();
const stamp = (tag) => (v) => log.push(`${tag}@~${Math.round((Date.now() - t0) / 30) * 30}ms(${v})`);
const log = [];
const d = debounce(stamp('debounce'), 100);
const dl = debounce(stamp('debounce-leading'), 100, { leading: true, trailing: false });
const th = throttle(stamp('throttle'), 100);
for (let i = 0; i < 10; i++) { d(i); dl(i); th(i); await sleep(30); }
await sleep(200);
console.log('\ntimeline (10 events, 30ms apart, window 100ms):');
for (const line of log) console.log(' ' + line);
// tail call: V8 does NOT implement proper tail calls
const deep = (n) => (n === 0 ? 'done' : deep(n - 1));
try { console.log('\nrecursion depth 1e6 :', deep(1_000_000)); }
catch (e) { console.log('\nrecursion depth 1e6 :', e.constructor.name, '- V8 has no proper tail calls'); }
let depth = 0;
const probe = () => { depth++; probe(); };
try { probe(); } catch { console.log('max stack depth ~ :', depth); }
}
demo();
// arity / .length / name
console.log('\n.length of curried :', cv.length, '(curried wrappers use rest args -> 0)');
console.log('.name preserved :', memoize(function namedFn() {}).name || '(empty - assign explicitly if you need it)');
console.log('bound fn name :', volume.bind(null).name, '| length:', volume.bind(null, 1).length);
js-core/08-iterators.js
'use strict';
// ---------- the iteration protocol, by hand ----------
const range = {
from: 1, to: 4,
[Symbol.iterator]() {
let cur = this.from; const last = this.to;
return {
next: () => (cur <= last ? { value: cur++, done: false } : { value: undefined, done: true }),
return(v) { console.log(' (iterator.return called — cleanup)'); return { value: v, done: true }; },
[Symbol.iterator]() { return this; }, // makes the iterator itself iterable
};
},
};
console.log('spread :', [...range]);
console.log('for..of :');
for (const n of range) { console.log(' ', n); if (n === 2) break; } // break triggers return()
console.log('destructure:', (([a, b]) => [a, b])(range));
// Array.from consumes any iterable, and also array-likes
console.log('Array.from(iterable):', Array.from(range));
console.log('Array.from(arraylike):', Array.from({ length: 3, 0: 'a', 1: 'b', 2: 'c' }));
console.log('Array.from with mapFn:', Array.from({ length: 5 }, (_, i) => i * i));
// strings iterate by code point, not code unit
const s = 'a\u{1F600}b';
console.log('\nstring .length :', s.length);
console.log('[...string] :', [...s], '(length', [...s].length, ')');
console.log('s[1] (broken half) :', JSON.stringify(s[1]));
console.log('Intl.Segmenter graphemes:', [...new Intl.Segmenter('en', { granularity: 'grapheme' }).segment('a👨👩👧b')].map((x) => x.segment));
// ---------- generators ----------
function* counter(start = 0) { let i = start; while (true) yield i++; }
const g = counter(10);
console.log('\ngenerator next :', g.next(), g.next(), g.next());
function* inner() { yield 'i1'; yield 'i2'; return 'inner-return'; }
function* outer() {
yield 'o1';
const got = yield* inner(); // delegation; the return value flows to `got`
yield 'got:' + got;
}
console.log('yield* delegation:', [...outer()]);
// two-way communication
function* dialogue() {
const name = yield 'What is your name?';
const age = yield `Hello ${name}, how old are you?`;
try {
yield `${name} is ${age}`;
} catch (e) {
yield 'caught inside generator: ' + e.message;
} finally {
console.log(' (generator finally ran)');
}
}
const dg = dialogue();
console.log('\n1:', dg.next().value);
console.log('2:', dg.next('Hendrix').value);
console.log('3:', dg.next(41).value);
console.log('4:', dg.throw(new Error('boom')).value);
console.log('5:', dg.return('early'));
// generators as state machines / lazy infinite sequences
function* take(it, n) { let i = 0; for (const v of it) { if (i++ >= n) return; yield v; } }
function* mapG(it, f) { for (const v of it) yield f(v); }
function* filterG(it, p) { for (const v of it) if (p(v)) yield v; }
function* naturals() { let n = 1; while (true) yield n++; }
console.log('\nlazy pipeline :', [...take(filterG(mapG(naturals(), (x) => x * x), (x) => x % 2 === 1), 5)]);
// how lazy? count how many source values are pulled
let pulled = 0;
function* countedNaturals() { let n = 1; while (true) { pulled++; yield n++; } }
const firstThree = [...take(filterG(countedNaturals(), (x) => x % 100 === 0), 3)];
console.log('pulled from source:', pulled, '-> got', firstThree);
// ---------- ES2025/2026 iterator helpers ----------
console.log('\nIterator global present:', typeof Iterator !== 'undefined');
if (typeof Iterator !== 'undefined') {
let touched = 0;
function* src() { let n = 1; while (true) { touched++; yield n++; } }
const out = src()
.map((x) => x * 3)
.filter((x) => x % 2 === 0)
.drop(1)
.take(4)
.toArray();
console.log('helpers result :', out, '| source values touched:', touched);
console.log('flatMap :', [1, 2, 3].values().flatMap((n) => [n, -n]).toArray());
console.log('reduce :', naturals().take(100).reduce((a, b) => a + b, 0));
console.log('some/every/find :',
naturals().take(10).some((x) => x > 8),
naturals().take(10).every((x) => x > 0),
naturals().find((x) => x % 7 === 0));
console.log('Iterator.from :', Iterator.from({ next: (() => { let i = 0; return () => ({ value: i, done: i++ >= 3 }); })() }).toArray());
console.log('helpers are lazy & one-shot:', (() => { const it = naturals().take(3); it.toArray(); return it.toArray(); })());
}
// ---------- async iterators ----------
async function* asyncRange(n, delay = 5) {
for (let i = 0; i < n; i++) {
await new Promise((r) => setTimeout(r, delay));
yield i;
}
}
(async () => {
const got = [];
for await (const v of asyncRange(4)) got.push(v);
console.log('\nfor await :', got);
// for await also accepts a sync iterable of promises (awaits each)
const proms = [Promise.resolve('a'), Promise.resolve('b')];
const seq = [];
for await (const v of proms) seq.push(v);
console.log('for await over promises:', seq);
// Array.fromAsync (ES2024)
console.log('Array.fromAsync :', await Array.fromAsync(asyncRange(3)));
// Symbol.asyncIterator by hand: a paginated API
const api = {
pages: [[1, 2], [3, 4], [5]],
async *[Symbol.asyncIterator]() {
for (const page of this.pages) { await new Promise((r) => setImmediate(r)); yield* page; }
},
};
const flat = [];
for await (const item of api) flat.push(item);
console.log('paginated stream:', flat);
// async generator with early break -> return() runs finally
async function* withCleanup() {
try { yield 1; yield 2; yield 3; } finally { console.log(' (async cleanup ran)'); }
}
for await (const v of withCleanup()) { if (v === 2) break; }
})();
js-core/09-eventloop-1.js
console.log('1 sync start');
setTimeout(() => console.log('2 setTimeout 0'), 0);
setImmediate(() => console.log('3 setImmediate'));
Promise.resolve().then(() => console.log('4 promise.then'));
queueMicrotask(() => console.log('5 queueMicrotask'));
process.nextTick(() => console.log('6 process.nextTick'));
(async () => {
console.log('7 async fn body (sync until first await)');
await null;
console.log('8 after await null');
})();
console.log('9 sync end');
js-core/09-eventloop-2.js
// Puzzle 2: microtask starvation and nesting order
console.log('script start');
setTimeout(() => {
console.log('timeout 1');
Promise.resolve().then(() => console.log(' microtask inside timeout 1'));
process.nextTick(() => console.log(' nextTick inside timeout 1'));
}, 0);
setTimeout(() => console.log('timeout 2'), 0);
process.nextTick(() => {
console.log('nextTick A');
process.nextTick(() => console.log(' nextTick A.1 (nested — still drained before promises)'));
});
Promise.resolve()
.then(() => { console.log('promise 1'); return Promise.resolve('x'); })
.then((v) => console.log('promise 2 (took 2 extra ticks because a promise was returned)', v));
Promise.resolve().then(() => console.log('promise 3'));
console.log('script end');
js-core/09-eventloop-3.js
// Puzzle 3: async/await desugaring, and `await` on a thenable vs a native promise
async function a1() {
console.log('a1 start');
await a2();
console.log('a1 end');
}
async function a2() { console.log('a2'); }
console.log('script start');
setTimeout(() => console.log('setTimeout'), 0);
a1();
new Promise((resolve) => { console.log('promise executor (synchronous!)'); resolve(); })
.then(() => console.log('promise then 1'))
.then(() => console.log('promise then 2'))
.then(() => console.log('promise then 3'));
console.log('script end');
// Extra: awaiting a native promise costs 1 microtask tick; awaiting a thenable costs more.
setTimeout(() => {
console.log('\n--- tick accounting ---');
const order = [];
(async () => { await Promise.resolve(); order.push('await native promise'); })();
(async () => { await { then(r) { r(); } }; order.push('await thenable'); })();
(async () => { await 1; order.push('await plain value'); })();
Promise.resolve()
.then(() => order.push('tick 1'))
.then(() => order.push('tick 2'))
.then(() => order.push('tick 3'))
.then(() => order.push('tick 4'))
.then(() => console.log(order.join('\n')));
}, 10);
js-core/09-eventloop-4.js
// Puzzle 4: setTimeout(0) vs setImmediate — indeterminate at top level, deterministic inside I/O
const fs = require('node:fs');
console.log('--- top level (order is NOT guaranteed; depends on loop start-up cost) ---');
setTimeout(() => console.log(' timeout'), 0);
setImmediate(() => console.log(' immediate'));
setTimeout(() => {
console.log('\n--- inside an I/O callback (poll phase): immediate ALWAYS wins ---');
fs.readFile(__filename, () => {
setTimeout(() => console.log(' timeout (next timers phase, a full loop later)'), 0);
setImmediate(() => console.log(' immediate (check phase, same iteration)'));
process.nextTick(() => console.log(' nextTick (before either — drains on phase exit)'));
Promise.resolve().then(() => console.log(' promise (after nextTick, still before both)'));
});
}, 20);
// starvation demo: nextTick can block the loop forever
setTimeout(() => {
console.log('\n--- nextTick starvation ---');
let n = 0;
const t = setTimeout(() => console.log(' timer fired after', n, 'nextTicks'), 0);
(function spin() { if (n++ < 5) process.nextTick(spin); })();
// with n < 5 the timer still fires; make it unbounded and the process hangs.
}, 100);
js-core/09-eventloop-5.js
// Close phase, deterministically: destroy a live socket from inside an I/O callback.
const net = require('node:net');
const server = net.createServer((sock) => {
sock.on('close', () => { console.log('5 close phase : socket "close" event'); server.close(); });
sock.on('data', () => {
console.log('1 poll phase : socket "data" (I/O callback)');
process.nextTick(() => console.log('2 nextTick : drains as the phase exits'));
Promise.resolve().then(() => console.log('3 microtask : right after nextTick'));
setImmediate(() => console.log('4 check phase : setImmediate'));
setTimeout(() => console.log('6 timers phase : setTimeout 0, next loop turn'), 0);
sock.destroy();
});
});
server.listen(0, () => {
const c = net.connect(server.address().port, () => c.end('ping'));
c.on('error', () => {});
});
js-core/10-promise.js
'use strict';
const PENDING = 'pending', FULFILLED = 'fulfilled', REJECTED = 'rejected';
class MyPromise {
#state = PENDING;
#value = undefined;
#handlers = []; // { onFulfilled, onRejected, resolve, reject }
#handled = false;
constructor(executor) {
if (typeof executor !== 'function') throw new TypeError('Promise resolver is not a function');
const resolve = (v) => this.#resolveWith(v);
const reject = (r) => this.#settle(REJECTED, r);
try { executor(resolve, reject); } catch (err) { reject(err); }
}
// 2.3 The Promise Resolution Procedure
#resolveWith(x) {
if (this.#state !== PENDING) return;
if (x === this) return this.#settle(REJECTED, new TypeError('Chaining cycle detected'));
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
let then;
try { then = x.then; } catch (err) { return this.#settle(REJECTED, err); }
if (typeof then === 'function') {
let called = false; // a thenable may call back more than once
try {
then.call(x,
(y) => { if (!called) { called = true; this.#resolveWith(y); } },
(r) => { if (!called) { called = true; this.#settle(REJECTED, r); } });
} catch (err) { if (!called) { called = true; this.#settle(REJECTED, err); } }
return;
}
}
this.#settle(FULFILLED, x);
}
#settle(state, value) {
if (this.#state !== PENDING) return;
this.#state = state;
this.#value = value;
if (state === REJECTED && this.#handlers.length === 0) {
queueMicrotask(() => { if (!this.#handled) MyPromise.onUnhandled?.(value); });
}
for (const h of this.#handlers) this.#schedule(h);
this.#handlers = [];
}
#schedule(handler) {
// 2.2.4: handlers run asynchronously, on the microtask queue
queueMicrotask(() => {
const cb = this.#state === FULFILLED ? handler.onFulfilled : handler.onRejected;
if (typeof cb !== 'function') {
// pass through
return this.#state === FULFILLED ? handler.resolve(this.#value) : handler.reject(this.#value);
}
try { handler.resolve(cb(this.#value)); } catch (err) { handler.reject(err); }
});
}
then(onFulfilled, onRejected) {
this.#handled = true;
let resolve, reject;
const next = new MyPromise((res, rej) => { resolve = res; reject = rej; });
const handler = { onFulfilled, onRejected, resolve, reject };
if (this.#state === PENDING) this.#handlers.push(handler);
else this.#schedule(handler);
return next;
}
catch(onRejected) { return this.then(undefined, onRejected); }
finally(onFinally) {
return this.then(
(v) => MyPromise.resolve(onFinally()).then(() => v),
(r) => MyPromise.resolve(onFinally()).then(() => { throw r; }),
);
}
static resolve(v) { return v instanceof MyPromise ? v : new MyPromise((res) => res(v)); }
static reject(r) { return new MyPromise((_, rej) => rej(r)); }
static all(iterable) {
return new MyPromise((resolve, reject) => {
const items = [...iterable];
const out = new Array(items.length);
let remaining = items.length;
if (remaining === 0) return resolve(out);
items.forEach((item, i) => MyPromise.resolve(item).then((v) => {
out[i] = v;
if (--remaining === 0) resolve(out);
}, reject));
});
}
static allSettled(iterable) {
return MyPromise.all([...iterable].map((p) => MyPromise.resolve(p).then(
(value) => ({ status: 'fulfilled', value }),
(reason) => ({ status: 'rejected', reason }),
)));
}
static race(iterable) {
return new MyPromise((resolve, reject) => {
for (const item of iterable) MyPromise.resolve(item).then(resolve, reject);
});
}
static any(iterable) {
return new MyPromise((resolve, reject) => {
const items = [...iterable];
const errors = new Array(items.length);
let remaining = items.length;
if (remaining === 0) return reject(new AggregateError([], 'All promises were rejected'));
items.forEach((item, i) => MyPromise.resolve(item).then(resolve, (e) => {
errors[i] = e;
if (--remaining === 0) reject(new AggregateError(errors, 'All promises were rejected'));
}));
});
}
static try(fn, ...args) {
return new MyPromise((resolve) => resolve(fn(...args)));
}
static withResolvers() {
let resolve, reject;
const promise = new MyPromise((res, rej) => { resolve = res; reject = rej; });
return { promise, resolve, reject };
}
get [Symbol.toStringTag]() { return 'MyPromise'; }
}
// ------------------------------------------------------------------ tests
let pass = 0, fail = 0;
const results = [];
function it(name, fn) {
return Promise.resolve().then(fn).then(
() => { pass++; results.push(` ok ${name}`); },
(e) => { fail++; results.push(` FAIL ${name}: ${e && e.message}`); },
);
}
const assert = (cond, msg) => { if (!cond) throw new Error(msg || 'assertion failed'); };
const eq = (a, b, msg) => assert(JSON.stringify(a) === JSON.stringify(b), `${msg || ''} expected ${JSON.stringify(b)} got ${JSON.stringify(a)}`);
const toNative = (mp) => new Promise((res, rej) => mp.then(res, rej));
(async () => {
await it('resolves with a value', async () => eq(await toNative(MyPromise.resolve(42)), 42));
await it('rejects and is catchable', async () => {
try { await toNative(MyPromise.reject(new Error('nope'))); assert(false, 'should throw'); }
catch (e) { eq(e.message, 'nope'); }
});
await it('executor runs synchronously', async () => {
let ran = false;
new MyPromise(() => { ran = true; });
assert(ran);
});
await it('then callbacks are asynchronous', async () => {
const order = [];
MyPromise.resolve(1).then(() => order.push('then'));
order.push('sync');
await toNative(MyPromise.resolve());
await toNative(MyPromise.resolve());
eq(order, ['sync', 'then']);
});
await it('chains and transforms', async () => {
const v = await toNative(MyPromise.resolve(1).then((x) => x + 1).then((x) => x * 10));
eq(v, 20);
});
await it('adopts a returned thenable', async () => {
const v = await toNative(MyPromise.resolve(1).then(() => ({ then: (res) => res('adopted') })));
eq(v, 'adopted');
});
await it('adopts a returned MyPromise', async () => {
const v = await toNative(MyPromise.resolve(1).then(() => MyPromise.resolve('inner')));
eq(v, 'inner');
});
await it('adopts a native promise', async () => {
const v = await toNative(MyPromise.resolve(1).then(() => Promise.resolve('native')));
eq(v, 'native');
});
await it('a throw becomes a rejection', async () => {
try { await toNative(MyPromise.resolve().then(() => { throw new Error('thrown'); })); assert(false); }
catch (e) { eq(e.message, 'thrown'); }
});
await it('rejection passes through missing handlers', async () => {
try { await toNative(MyPromise.reject(new Error('deep')).then((x) => x).then((x) => x)); assert(false); }
catch (e) { eq(e.message, 'deep'); }
});
await it('catch recovers', async () => {
const v = await toNative(MyPromise.reject(new Error('x')).catch(() => 'recovered'));
eq(v, 'recovered');
});
await it('settles only once', async () => {
const p = new MyPromise((res, rej) => { res('first'); res('second'); rej(new Error('third')); });
eq(await toNative(p), 'first');
});
await it('detects chaining cycles', async () => {
const p = MyPromise.resolve();
const q = p.then(() => q);
try { await toNative(q); assert(false); } catch (e) { assert(e instanceof TypeError); }
});
await it('finally passes the value through', async () => {
let ran = false;
const v = await toNative(MyPromise.resolve('v').finally(() => { ran = true; return 'ignored'; }));
assert(ran); eq(v, 'v');
});
await it('finally rethrows the reason', async () => {
try { await toNative(MyPromise.reject(new Error('e')).finally(() => {})); assert(false); }
catch (e) { eq(e.message, 'e'); }
});
await it('all resolves in input order', async () => {
const slow = new MyPromise((r) => setTimeout(() => r('slow'), 20));
eq(await toNative(MyPromise.all([slow, MyPromise.resolve('fast'), 'plain'])), ['slow', 'fast', 'plain']);
});
await it('all rejects fast', async () => {
try { await toNative(MyPromise.all([MyPromise.resolve(1), MyPromise.reject(new Error('bad'))])); assert(false); }
catch (e) { eq(e.message, 'bad'); }
});
await it('all([]) resolves immediately', async () => eq(await toNative(MyPromise.all([])), []));
await it('allSettled reports both outcomes', async () => {
const r = await toNative(MyPromise.allSettled([MyPromise.resolve(1), MyPromise.reject(new Error('z'))]));
eq(r.map((x) => x.status), ['fulfilled', 'rejected']);
eq(r[1].reason.message, 'z');
});
await it('race takes the first settlement', async () => {
const slow = new MyPromise((r) => setTimeout(() => r('slow'), 30));
const fast = new MyPromise((r) => setTimeout(() => r('fast'), 1));
eq(await toNative(MyPromise.race([slow, fast])), 'fast');
});
await it('any ignores rejections', async () => {
const r = await toNative(MyPromise.any([MyPromise.reject(new Error('a')), MyPromise.resolve('b')]));
eq(r, 'b');
});
await it('any rejects with AggregateError', async () => {
try { await toNative(MyPromise.any([MyPromise.reject(new Error('a')), MyPromise.reject(new Error('b'))])); assert(false); }
catch (e) { assert(e instanceof AggregateError, 'AggregateError'); eq(e.errors.length, 2); }
});
await it('try catches synchronous throws', async () => {
try { await toNative(MyPromise.try(() => { throw new Error('sync-boom'); })); assert(false); }
catch (e) { eq(e.message, 'sync-boom'); }
});
await it('withResolvers exposes resolve/reject', async () => {
const { promise, resolve } = MyPromise.withResolvers();
setTimeout(() => resolve('later'), 1);
eq(await toNative(promise), 'later');
});
await it('interoperates with native await (thenable)', async () => {
eq(await MyPromise.resolve('interop'), 'interop');
});
await it('a thenable that calls back twice is ignored the second time', async () => {
const weird = { then(res) { res('one'); res('two'); } };
eq(await toNative(MyPromise.resolve().then(() => weird)), 'one');
});
await it('a thenable whose then throws after resolving is ignored', async () => {
const weird = { then(res) { res('ok'); throw new Error('late'); } };
eq(await toNative(MyPromise.resolve().then(() => weird)), 'ok');
});
await it('multiple then() on the same promise all fire', async () => {
const p = MyPromise.resolve(5);
eq(await toNative(MyPromise.all([p.then((x) => x + 1), p.then((x) => x + 2)])), [6, 7]);
});
console.log(results.join('\n'));
console.log(`\n${pass} passed, ${fail} failed`);
if (fail) process.exitCode = 1;
})();
js-core/11-async-patterns.js
'use strict';
const { setTimeout: sleep } = require('node:timers/promises');
const { AsyncLocalStorage } = require('node:async_hooks');
// Promise.try is ES2025 but is not in Node 22's V8 12.4 (it lands with V8 13.x / Node 24).
if (!Promise.try) {
Promise.try = function (fn, ...args) { return new Promise((resolve) => resolve(fn(...args))); };
console.log('(polyfilled Promise.try — absent in Node 22 / V8 12.4)');
}
const t0 = Date.now();
const ms = () => `${String(Date.now() - t0).padStart(4)}ms`;
const work = async (id, delay) => { await sleep(delay); return id; };
(async () => {
// ---------- combinators ----------
console.log('--- combinators ---');
const settled = await Promise.allSettled([
Promise.resolve('a'),
Promise.reject(new Error('b failed')),
sleep(10, 'c'),
]);
console.log('allSettled :', settled.map((s) => (s.status === 'fulfilled' ? `ok(${s.value})` : `err(${s.reason.message})`)));
console.log('any :', await Promise.any([Promise.reject(new Error('x')), sleep(5, 'winner')]));
console.log('race :', await Promise.race([sleep(5, 'fast'), sleep(50, 'slow')]));
try { await Promise.all([Promise.reject(new Error('first')), Promise.reject(new Error('second'))]); }
catch (e) { console.log('all rejects:', e.message, '(the other rejection is swallowed but NOT unhandled)'); }
try { await Promise.any([Promise.reject(new Error('e1')), Promise.reject(new Error('e2'))]); }
catch (e) { console.log('any all-rejected:', e.constructor.name, e.errors.map((x) => x.message)); }
console.log('Promise.try:', await Promise.try(() => 'sync value'),
'| catches sync throw:', await Promise.try(() => { throw new Error('sync'); }).catch((e) => e.message));
// ---------- sequential vs parallel ----------
console.log('\n--- sequential vs parallel ---');
let s = Date.now();
const seq = [await work('a', 40), await work('b', 40), await work('c', 40)];
console.log(`sequential awaits : ${seq} in ~${Date.now() - s}ms`);
s = Date.now();
const par = await Promise.all([work('a', 40), work('b', 40), work('c', 40)]);
console.log(`Promise.all : ${par} in ~${Date.now() - s}ms`);
s = Date.now();
const pa = work('a', 40), pb = work('b', 40); // start both, then await
const both = [await pa, await pb];
console.log(`start-then-await : ${both} in ~${Date.now() - s}ms`);
// ---------- await inside forEach does not work ----------
console.log('\n--- await in forEach ---');
const out = [];
[1, 2, 3].forEach(async (n) => { await sleep(5); out.push(n); });
console.log('immediately after forEach :', out, '(forEach ignored the returned promises)');
await sleep(20);
console.log('20ms later :', out);
const out2 = [];
for (const n of [1, 2, 3]) { await sleep(5); out2.push(n); }
console.log('for..of + await :', out2);
const out3 = await Promise.all([1, 2, 3].map(async (n) => { await sleep(5); return n; }));
console.log('map + Promise.all :', out3);
// reduce is the sequential-with-accumulator idiom
const chained = await [1, 2, 3].reduce(async (accP, n) => { const acc = await accP; await sleep(2); return acc + n; }, Promise.resolve(0));
console.log('sequential reduce :', chained);
// ---------- error handling shapes ----------
console.log('\n--- error handling ---');
const safe = async (p) => { try { return [null, await p]; } catch (e) { return [e, null]; } };
console.log('tuple style :', await safe(Promise.reject(new Error('tuple'))).then(([e]) => e.message));
// finally does not swallow
try { await Promise.reject(new Error('kept')).finally(() => 'ignored'); }
catch (e) { console.log('finally rethrows :', e.message); }
// errors thrown after the function returns escape the try
async function leaky() {
try { return sleep(1).then(() => { throw new Error('escaped'); }); } // NOT awaited
catch { return 'never reached'; }
}
console.log('return without await :', await leaky().catch((e) => 'escaped the try/catch: ' + e.message));
async function tight() {
try { return await sleep(1).then(() => { throw new Error('caught'); }); }
catch (e) { return 'caught by try/catch: ' + e.message; }
}
console.log('return await :', await tight());
// ---------- AbortController ----------
console.log('\n--- AbortController ---');
async function fetchish(signal) {
if (signal.aborted) throw signal.reason;
return new Promise((resolve, reject) => {
const id = setTimeout(() => resolve('payload'), 100);
signal.addEventListener('abort', () => { clearTimeout(id); reject(signal.reason); }, { once: true });
});
}
const ac = new AbortController();
setTimeout(() => ac.abort(new Error('user cancelled')), 20);
try { await fetchish(ac.signal); } catch (e) { console.log('aborted with :', e.message); }
// AbortSignal helpers
try { await sleep(100, undefined, { signal: AbortSignal.timeout(20) }); }
catch (e) { console.log('AbortSignal.timeout:', e.name); }
const combined = AbortSignal.any([AbortSignal.timeout(15), new AbortController().signal]);
try { await fetchish(combined); } catch (e) { console.log('AbortSignal.any :', e.name); }
// ---------- pMap: bounded concurrency ----------
console.log('\n--- pMap (concurrency limit) ---');
async function pMap(iterable, mapper, { concurrency = Infinity, stopOnError = true } = {}) {
const items = [...iterable];
const results = new Array(items.length);
const errors = [];
let nextIndex = 0;
const limit = Math.max(1, Math.min(concurrency, items.length));
async function worker() {
while (nextIndex < items.length) {
const i = nextIndex++;
try { results[i] = await mapper(items[i], i); }
catch (e) { if (stopOnError) throw e; errors.push(e); }
}
}
await Promise.all(Array.from({ length: limit }, worker));
if (errors.length) throw new AggregateError(errors, 'pMap failures');
return results;
}
let inFlight = 0, peak = 0;
const timeline = [];
const job = async (n) => {
peak = Math.max(peak, ++inFlight);
timeline.push(`${ms()} start ${n} (inFlight=${inFlight})`);
await sleep(30);
inFlight--;
return n * 2;
};
const mapped = await pMap([1, 2, 3, 4, 5, 6, 7], job, { concurrency: 3 });
console.log('results :', mapped);
console.log('peak concurrency :', peak);
console.log(timeline.slice(0, 5).join('\n'));
// a semaphore, which is what pMap is underneath
function semaphore(max) {
let active = 0; const queue = [];
const release = () => { active--; queue.shift()?.(); };
return async function acquire(fn) {
if (active >= max) await new Promise((r) => queue.push(r));
active++;
try { return await fn(); } finally { release(); }
};
}
const gate = semaphore(2);
let semPeak = 0, semActive = 0;
await Promise.all([1, 2, 3, 4, 5].map((n) => gate(async () => {
semPeak = Math.max(semPeak, ++semActive); await sleep(10); semActive--; return n;
})));
console.log('semaphore peak :', semPeak);
// ---------- AsyncLocalStorage ----------
console.log('\n--- AsyncLocalStorage ---');
const als = new AsyncLocalStorage();
async function handler(id) {
await sleep(Math.random() * 10);
return `deep call sees requestId=${als.getStore()?.requestId}`;
}
const lines = await Promise.all([1, 2, 3].map((i) =>
als.run({ requestId: `req-${i}` }, () => handler(i))));
console.log(lines.join('\n'));
})();
js-core/12-unhandled.js
'use strict';
// Unhandled rejections: what Node does, and the "handled too late" warning.
process.on('unhandledRejection', (reason, promise) => {
console.log('[unhandledRejection]', reason.message);
});
process.on('rejectionHandled', () => {
console.log('[rejectionHandled] a handler was attached after the fact');
});
// 1. Never handled -> unhandledRejection fires at the end of the microtask checkpoint.
Promise.reject(new Error('never handled'));
// 2. Handled synchronously -> no event.
Promise.reject(new Error('handled sync')).catch(() => console.log('caught sync-attached handler'));
// 3. Handled LATE (after a macrotask) -> unhandledRejection fires first, then rejectionHandled.
const late = Promise.reject(new Error('handled late'));
setTimeout(() => late.catch((e) => console.log('caught late:', e.message)), 10);
// 4. Promise.all swallows the losing rejections WITHOUT marking them unhandled,
// because .all attaches a handler to every input.
Promise.all([Promise.reject(new Error('all-first')), Promise.reject(new Error('all-second'))])
.catch((e) => console.log('Promise.all rejected with:', e.message));
// 5. But an array you build and only PARTLY await does leak.
const leaked = [Promise.reject(new Error('leaked-0')), Promise.resolve(1)];
leaked[1].then(() => console.log('only awaited the resolved one'));
// 6. Throwing inside a setTimeout callback is an uncaughtException, not a rejection.
process.on('uncaughtException', (e) => console.log('[uncaughtException]', e.message));
setTimeout(() => { throw new Error('sync throw in a timer'); }, 20);
setTimeout(() => {
console.log('\nDefault policy: `--unhandled-rejections=throw` (Node 15+) turns an');
console.log('unhandled rejection into an uncaughtException and exits with code 1.');
console.log('Node 22 flag values: throw | strict | warn | warn-with-error-code | none');
}, 40);
js-core/13-memory.js
'use strict';
const v8 = require('node:v8');
const mb = (b) => (b / 1024 / 1024).toFixed(2) + ' MB';
const snap = (label) => {
const m = process.memoryUsage();
console.log(`${label.padEnd(24)} rss=${mb(m.rss)} heapTotal=${mb(m.heapTotal)} heapUsed=${mb(m.heapUsed)} external=${mb(m.external)} arrayBuffers=${mb(m.arrayBuffers)}`);
};
snap('baseline');
// --- allocate a lot of short-lived garbage: scavenger territory ---
let sink = 0;
for (let i = 0; i < 2_000_000; i++) { const o = { i, s: 'x' }; sink += o.i; }
snap('after 2M short-lived');
// --- retain a lot: promotes to old space ---
const retained = [];
for (let i = 0; i < 500_000; i++) retained.push({ i, payload: 'y'.repeat(8) });
snap('after 500k retained');
// heap space breakdown
console.log('\nheap spaces:');
for (const s of v8.getHeapSpaceStatistics()) {
if (s.space_used_size > 512 * 1024) {
console.log(' ' + s.space_name.padEnd(24), 'used=' + mb(s.space_used_size), 'size=' + mb(s.space_size));
}
}
const hs = v8.getHeapStatistics();
console.log('\nheap_size_limit :', mb(hs.heap_size_limit));
console.log('used_heap_size :', mb(hs.used_heap_size));
console.log('malloced_memory :', mb(hs.malloced_memory));
console.log('number_of_native_contexts:', hs.number_of_native_contexts);
console.log('number_of_detached_contexts:', hs.number_of_detached_contexts);
retained.length = 0;
if (global.gc) { global.gc(); snap('\nafter explicit gc'); } else { console.log('\n(run with --expose-gc for a forced collection)'); }
// --- WeakRef / FinalizationRegistry ---
const registry = new FinalizationRegistry((held) => console.log(' finalized:', held));
let target = { big: new Array(1000).fill(0) };
const ref = new WeakRef(target);
registry.register(target, 'cache-entry-1');
console.log('\nWeakRef deref while alive :', ref.deref() !== undefined);
target = null;
setTimeout(() => {
if (global.gc) global.gc();
setTimeout(() => {
console.log('WeakRef deref after drop :', ref.deref() === undefined ? 'undefined (collected)' : 'still alive (GC has not run)');
}, 50);
}, 10);
// --- WeakMap: private data that dies with the key ---
const privates = new WeakMap();
class Session {
constructor(token) { privates.set(this, { token }); }
get token() { return privates.get(this).token; }
}
let sess = new Session('abc');
console.log('WeakMap-backed private :', sess.token, '| WeakMap is not enumerable:', privates.size === undefined);
sess = null; // the entry becomes unreachable too — no manual delete needed
// --- WeakSet for "have I seen this object" without retaining it ---
const seen = new WeakSet();
const node = {};
seen.add(node);
console.log('WeakSet has :', seen.has(node));
// --- leak source 4: an unbounded cache (and the WeakMap/LRU fixes) ---
const leakyCache = new Map();
function leakyLookup(key) {
if (!leakyCache.has(key)) leakyCache.set(key, { key, data: 'x'.repeat(100) });
return leakyCache.get(key);
}
for (let i = 0; i < 100_000; i++) leakyLookup('k' + i);
snap('\nunbounded Map cache');
leakyCache.clear();
// --- leak source 3: a timer that keeps a closure alive ---
function startLeakyTimer() {
const bigBuffer = Buffer.alloc(4 * 1024 * 1024);
const id = setInterval(() => { void bigBuffer[0]; }, 1000);
return () => clearInterval(id); // returning a disposer is the fix
}
const stop = startLeakyTimer();
snap('timer holding 4MB');
stop();
// --- leak source 2: listeners that are never removed ---
const { EventEmitter } = require('node:events');
const bus = new EventEmitter();
bus.setMaxListeners(0);
const handlers = [];
for (let i = 0; i < 50_000; i++) { const h = () => i; handlers.push(h); bus.on('tick', h); }
console.log('listener count :', bus.listenerCount('tick'), '(each closure pins its scope)');
bus.removeAllListeners('tick');
// --- leak source 1: an accidental global ---
// `new Function` bodies are always sloppy, so an undeclared assignment creates a global.
const sloppyGlobal = new Function('accidental = new Array(1e5).fill(0); return typeof accidental;');
console.log('sloppy undeclared assign :', sloppyGlobal(), '-> globalThis.accidental exists:', 'accidental' in globalThis);
try { (function () { 'use strict'; undeclared2 = 1; })(); }
catch (e) { console.log('strict mode instead :', e.constructor.name, '(this is why you use strict/ESM)'); }
js-core/14-v8-bench.js
'use strict';
// Measured V8 behaviour. Run with plain `node 14-v8-bench.js`.
const N = 5_000_000;
function bench(label, fn, iters = 9) {
const times = [];
fn(); fn(); // warm up: let TurboFan tier the loop up first
for (let i = 0; i < iters; i++) {
const t = process.hrtime.bigint();
const r = fn();
const dt = Number(process.hrtime.bigint() - t) / 1e6;
times.push(dt);
if (r === Symbol.for('never')) console.log('unreachable');
}
times.sort((a, b) => a - b);
const median = times[Math.floor(times.length / 2)];
console.log(` ${label.padEnd(42)} ${median.toFixed(1).padStart(8)} ms (min ${times[0].toFixed(1)})`);
return median;
}
// ---------------------------------------------------------------- 1. IC state
console.log('1) Inline caches: monomorphic vs polymorphic vs megamorphic property access');
// Same shape (same hidden class) -> monomorphic
const mono = Array.from({ length: 1000 }, (_, i) => ({ x: i, y: i }));
// 4 shapes -> polymorphic (V8 handles up to 4 maps in a polymorphic IC)
const poly = Array.from({ length: 1000 }, (_, i) => {
switch (i % 4) {
case 0: return { x: i, y: i };
case 1: return { y: i, x: i }; // different insertion order = different map
case 2: return { x: i, y: i, z: i };
default: return { x: i, a: i, y: i };
}
});
// 20 shapes -> megamorphic (IC gives up, falls back to the global megamorphic stub cache)
const mega = Array.from({ length: 1000 }, (_, i) => {
const o = { x: i };
for (let k = 0; k < (i % 20); k++) o['k' + k] = k;
o.y = i;
return o;
});
function readX(arr) {
let s = 0;
for (let i = 0; i < N; i++) s += arr[i % 1000].x; // one call site, one IC
return s;
}
// Force each call site to be its own function so the ICs do not merge.
const readMono = new Function('arr', 'N', 'let s=0; for(let i=0;i<N;i++) s+=arr[i%1000].x; return s;');
const readPoly = new Function('arr', 'N', 'let s=0; for(let i=0;i<N;i++) s+=arr[i%1000].x; return s;');
const readMega = new Function('arr', 'N', 'let s=0; for(let i=0;i<N;i++) s+=arr[i%1000].x; return s;');
bench('monomorphic (1 hidden class)', () => readMono(mono, N));
bench('polymorphic (4 hidden classes)', () => readPoly(poly, N));
bench('megamorphic (20 hidden classes)', () => readMega(mega, N));
void readX;
// ---------------------------------------------------------------- 2. elements kinds
console.log('\n2) Elements kinds: PACKED vs HOLEY vs DOUBLE vs generic');
const M = 100_000, REPS = 200;
const packedSmi = Array.from({ length: M }, (_, i) => i);
const packedDouble = Array.from({ length: M }, (_, i) => i + 0.5);
const packedElements = Array.from({ length: M }, (_, i) => ({ v: i }));
const holeySmi = Array.from({ length: M }, (_, i) => i);
delete holeySmi[5]; // one delete converts the whole array to HOLEY
const holeyByHole = new Array(M); // `new Array(n)` starts HOLEY_SMI
for (let i = 0; i < M; i++) holeyByHole[i] = i;
const sumSmi = new Function('a', 'R', 'let s=0; for(let r=0;r<R;r++) for(let i=0;i<a.length;i++) s+=a[i]; return s;');
const sumDbl = new Function('a', 'R', 'let s=0; for(let r=0;r<R;r++) for(let i=0;i<a.length;i++) s+=a[i]; return s;');
const sumHoley = new Function('a', 'R', 'let s=0; for(let r=0;r<R;r++) for(let i=0;i<a.length;i++) s+=a[i]||0; return s;');
const sumHoley2 = new Function('a', 'R', 'let s=0; for(let r=0;r<R;r++) for(let i=0;i<a.length;i++) s+=a[i]; return s;');
const sumObj = new Function('a', 'R', 'let s=0; for(let r=0;r<R;r++) for(let i=0;i<a.length;i++) s+=a[i].v; return s;');
bench('PACKED_SMI_ELEMENTS sum', () => sumSmi(packedSmi, REPS));
bench('PACKED_DOUBLE_ELEMENTS sum', () => sumDbl(packedDouble, REPS));
bench('HOLEY_SMI (one delete) sum', () => sumHoley(holeySmi, REPS));
bench('HOLEY_SMI (new Array(n), fully filled)', () => sumHoley2(holeyByHole, REPS));
bench('PACKED_ELEMENTS (objects) sum', () => sumObj(packedElements, REPS));
// ---------------------------------------------------------------- 3. shape stability
console.log('\n3) Shape stability: fixed shape vs shape mutated after construction');
function makeStable(i) { return { a: i, b: i, c: i, d: 0 }; }
function makeUnstable(i) { const o = { a: i, b: i, c: i }; if (i % 2) o.d = 0; return o; }
const stable = [], unstable = [];
for (let i = 0; i < 200_000; i++) { stable.push(makeStable(i)); unstable.push(makeUnstable(i)); }
const sumD1 = new Function('a', 'let s=0; for(let r=0;r<40;r++) for(const o of a) s+=o.d|0; return s;');
const sumD2 = new Function('a', 'let s=0; for(let r=0;r<40;r++) for(const o of a) s+=o.d|0; return s;');
bench('always-present property .d', () => sumD1(stable));
bench('sometimes-missing property .d', () => sumD2(unstable));
// ---------------------------------------------------------------- 4. strings
console.log('\n4) Strings: ConsString (+=) vs array-join vs split/reverse/join');
const WORDS = 200_000;
bench('s += chunk in a loop (ConsString rope)', () => {
let s = '';
for (let i = 0; i < WORDS; i++) s += 'abc';
return s.length;
});
bench('parts.push + join', () => {
const parts = [];
for (let i = 0; i < WORDS; i++) parts.push('abc');
return parts.join('').length;
});
const big = 'abcdefghij'.repeat(50_000); // 500k chars
bench('split("").reverse().join("") 500k', () => big.split('').reverse().join('').length, 3);
bench('manual reverse loop 500k', () => {
let out = '';
for (let i = big.length - 1; i >= 0; i--) out += big[i];
return out.length;
}, 3);
bench('Array.from + reverse + join 500k', () => Array.from(big).reverse().join('').length, 3);
bench('slice 500k (SlicedString, O(1))', () => { let n = 0; for (let i = 0; i < 200_000; i++) n += big.slice(i, i + 100).length; return n; }, 3);
// ---------------------------------------------------------------- 5. arguments
console.log('\n5) `arguments` leak vs rest parameters');
function useArguments() { let s = 0; for (let i = 0; i < arguments.length; i++) s += arguments[i]; return s; }
function useRest(...a) { let s = 0; for (let i = 0; i < a.length; i++) s += a[i]; return s; }
function leakArguments() { return arguments; } // escapes -> must be materialised
const consume = (o) => { let s = 0; for (let i = 0; i < o.length; i++) s += o[i]; return s; };
const L = 3_000_000;
bench('arguments, consumed in place', () => { let s = 0; for (let i = 0; i < L; i++) s += useArguments(1, 2, 3); return s; });
bench('rest parameters', () => { let s = 0; for (let i = 0; i < L; i++) s += useRest(1, 2, 3); return s; });
bench('arguments escaping the frame', () => { let s = 0; for (let i = 0; i < L; i++) s += consume(leakArguments(1, 2, 3)); return s; });
bench('Array.prototype.slice.call(arguments)', () => { let s = 0; for (let i = 0; i < L; i++) s += consume(Array.prototype.slice.call(leakArguments(1, 2, 3))); return s; });
// ---------------------------------------------------------------- 6. try/catch + deopt triggers
console.log('\n6) Misc: delete vs undefined, for..in vs Object.keys');
const objs = Array.from({ length: 200_000 }, (_, i) => ({ a: i, b: i, c: i }));
bench('set property to undefined', () => { for (const o of objs) o.b = undefined; return objs.length; });
bench('delete property (dictionary mode)', () => { for (const o of objs) { o.b = 1; delete o.b; } return objs.length; });
const shape = { a: 1, b: 2, c: 3, d: 4, e: 5 };
bench('for..in', () => { let n = 0; for (let r = 0; r < 2_000_000; r++) for (const k in shape) n += k.length; return n; });
bench('Object.keys + for', () => { let n = 0; for (let r = 0; r < 2_000_000; r++) { const ks = Object.keys(shape); for (let i = 0; i < ks.length; i++) n += ks[i].length; } return n; });
js-core/15-natives.js
// Run with: node --allow-natives-syntax 15-natives.js
function kind(a) {
// %DebugPrint writes to stderr; HasFastPackedElements/HasHoleyElements are cheap predicates.
return [
%HasSmiElements(a) ? 'SMI' : '',
%HasDoubleElements(a) ? 'DOUBLE' : '',
%HasObjectElements(a) ? 'OBJECT' : '',
%HasDictionaryElements(a) ? 'DICTIONARY' : '',
%HasHoleyElements(a) ? '(HOLEY)' : '(PACKED)',
].filter(Boolean).join(' ');
}
const p = (label, a) => console.log((' ' + label).padEnd(40), kind(a));
const nl = () => console.log();
console.log('elements-kind transitions (one-way lattice):');
const a = [1, 2, 3]; p('[1,2,3]', a);
a.push(4.5); p('after push(4.5)', a);
a.push('str'); p('after push("str")', a);
nl();
const b = [1, 2, 3]; p('[1,2,3]', b);
b[10] = 1; p('after b[10] = 1 (creates a hole)', b);
nl();
const c = [1, 2, 3]; p('[1,2,3]', c);
delete c[1]; p('after delete c[1]', c);
nl();
const d = new Array(3); p('new Array(3)', d);
d[0] = 1; d[1] = 2; d[2] = 3; p('after filling all 3 slots', d);
nl();
const e = [1, 2, 3]; p('[1,2,3]', e);
e[100000] = 1; p('after e[100000] = 1 (sparse)', e);
console.log('\nfast vs dictionary properties:');
const o = { a: 1, b: 2, c: 3 };
console.log(' literal object fastProperties =', %HasFastProperties(o));
delete o.b;
console.log(' after delete o.b fastProperties =', %HasFastProperties(o));
const big = {};
for (let i = 0; i < 50; i++) big['p' + i] = i;
console.log(' 50 dynamically added keys fastProperties =', %HasFastProperties(big));
console.log('\noptimisation tiers:');
function hot(x) { return x * 2 + 1; }
const decode = (f) => {
const st = %GetOptimizationStatus(f);
const bits = { isFunction: 0, neverOptimize: 1, alwaysOptimize: 2, maybeDeopted: 3, optimized: 4,
maglevved: 5, turbofanned: 6, interpreted: 7, markedForOpt: 8, markedForConcurrentOpt: 9,
optimizingConcurrently: 10, isExecuting: 11, topmostIsTurbofanned: 12, liteMode: 13,
markedForDeopt: 14, baseline: 15, topmostIsInterpreted: 16, topmostIsBaseline: 17, isLazy: 18 };
return Object.entries(bits).filter(([, b]) => st & (1 << b)).map(([k]) => k).join(', ') || '(none)';
};
console.log(' cold :', decode(hot));
for (let i = 0; i < 20; i++) hot(i);
console.log(' after 20 calls :', decode(hot));
for (let i = 0; i < 2000; i++) hot(i);
console.log(' after 2k calls :', decode(hot));
for (let i = 0; i < 200000; i++) hot(i);
console.log(' after 200k calls:', decode(hot));
console.log('\nstring representations:');
let s = 'a';
for (let i = 0; i < 10; i++) s += s; // builds a ConsString rope
console.log(' s.length =', s.length);
console.log(' s.slice(1, 100).length =', s.slice(1, 100).length, '(SlicedString: no copy)');
js-core/16-node.js
'use strict';
const { Readable, Writable, Transform, pipeline } = require('node:stream');
const { pipeline: pipelineAsync } = require('node:stream/promises');
const { Worker, isMainThread, parentPort, workerData } = require('node:worker_threads');
// ---------------- streams & backpressure ----------------
async function streamDemo() {
console.log('--- backpressure ---');
let produced = 0, consumed = 0, pauses = 0;
const source = new Readable({
highWaterMark: 4, // 4 objects in the read buffer
objectMode: true,
read() {
if (produced >= 20) return this.push(null);
this.push({ n: produced++ });
},
});
const slowSink = new Writable({
highWaterMark: 2,
objectMode: true,
write(chunk, _enc, cb) {
consumed++;
setTimeout(cb, 2); // deliberately slower than the producer
},
});
const doubler = new Transform({
objectMode: true,
transform(chunk, _enc, cb) { cb(null, { n: chunk.n * 2 }); },
});
// manual write() with backpressure handling, for contrast
const manual = new Writable({ highWaterMark: 2, objectMode: true, write(c, e, cb) { setTimeout(cb, 1); } });
for (let i = 0; i < 8; i++) {
if (!manual.write({ i })) {
pauses++;
await new Promise((r) => manual.once('drain', r)); // THIS is backpressure
}
}
manual.end();
console.log('manual write(): hit a full buffer', pauses, 'times and awaited "drain"');
await pipelineAsync(source, doubler, slowSink);
console.log(`pipeline: produced ${produced}, consumed ${consumed} — the readable never ran ahead`);
console.log('(pipeline/pipelineAsync also destroys every stream on error; .pipe() does not)');
// async iteration over a stream is the modern consumer
const nums = Readable.from(async function* () { for (let i = 0; i < 5; i++) yield i; }());
const collected = [];
for await (const n of nums) collected.push(n);
console.log('for await over a stream:', collected);
}
// ---------------- Buffer ----------------
function bufferDemo() {
console.log('\n--- Buffer ---');
const b = Buffer.from('héllo', 'utf8');
console.log('bytes :', b, '| .length (bytes) =', b.length, '| string length =', 'héllo'.length);
console.log('base64 / hex :', b.toString('base64'), '/', b.toString('hex'));
console.log('alloc vs unsafe :', Buffer.alloc(4), Buffer.allocUnsafe(4).length, '(allocUnsafe may contain old memory)');
const view = b.subarray(0, 2);
view[0] = 0x48;
console.log('subarray shares memory:', b.toString('utf8').slice(0, 3), '(mutating the view mutated the source)');
console.log('Buffer is a Uint8Array:', b instanceof Uint8Array, '| pooled:', b.buffer.byteLength > b.length);
const ab = new ArrayBuffer(8);
const dv = new DataView(ab);
dv.setUint32(0, 0xdeadbeef);
console.log('DataView big-endian:', dv.getUint32(0).toString(16), '| little-endian:', dv.getUint32(0, true).toString(16));
console.log('concat :', Buffer.concat([Buffer.from('ab'), Buffer.from('cd')]).toString());
}
// ---------------- structuredClone ----------------
function cloneDemo() {
console.log('\n--- structuredClone ---');
const src = { d: new Date(0), m: new Map([['k', [1, 2]]]), s: new Set([1]), r: /ab+c/gi, t: new Uint8Array([1, 2, 3]), big: 1n };
src.self = src; // cycle
const clone = structuredClone(src);
console.log('cycle preserved :', clone.self === clone);
console.log('Map/Set/Date/RegExp/TypedArray/BigInt:',
clone.m.get('k'), clone.s.has(1), clone.d.toISOString(), String(clone.r), clone.t, clone.big);
console.log('deep, not shared :', clone.m !== src.m && clone.m.get('k') !== src.m.get('k'));
try { structuredClone({ fn() {} }); } catch (e) { console.log('functions :', e.name, '- not cloneable'); }
try { structuredClone(Symbol('s')); } catch (e) { console.log('symbols :', e.name); }
class Tagged { constructor() { this.x = 1; } }
console.log('prototype is LOST:', structuredClone(new Tagged()).constructor.name);
// transfer moves an ArrayBuffer instead of copying it
const buf = new ArrayBuffer(8);
structuredClone(buf, { transfer: [buf] });
console.log('after transfer : source byteLength =', buf.byteLength, '(detached)');
// JSON round-trip loses all of this
console.log('JSON equivalent :', JSON.stringify({ d: new Date(0), m: new Map([['k', 1]]), u: undefined, n: NaN }));
}
// ---------------- CPU-bound work blocks the loop ----------------
function blockDemo() {
return new Promise((resolve) => {
console.log('\n--- blocking the event loop ---');
const start = Date.now();
setTimeout(() => {
console.log(`a 10ms timer actually fired after ${Date.now() - start}ms — the sync loop held the thread`);
resolve();
}, 10);
let x = 0;
for (let i = 0; i < 3e8; i++) x += i; // synchronous CPU burn
console.log(`sync loop done in ${Date.now() - start}ms (sum=${x})`);
});
}
// ---------------- worker_threads ----------------
function workerDemo() {
return new Promise((resolve) => {
console.log('\n--- worker_threads ---');
const start = Date.now();
let ticks = 0;
const tick = setInterval(() => ticks++, 1);
const src = `
const { parentPort, workerData } = require('node:worker_threads');
let x = 0;
for (let i = 0; i < workerData.n; i++) x += i;
parentPort.postMessage({ x, threadId: require('node:worker_threads').threadId });
`;
const w = new Worker(src, { eval: true, workerData: { n: 3e8 } });
w.on('message', (m) => {
clearInterval(tick);
console.log(`worker finished in ${Date.now() - start}ms; the main loop still ticked ${ticks} times`);
console.log('worker result :', m.x, '| threadId:', m.threadId);
resolve();
});
});
}
// ---------------- SharedArrayBuffer + Atomics ----------------
function sabDemo() {
return new Promise((resolve) => {
console.log('\n--- SharedArrayBuffer / Atomics ---');
const sab = new SharedArrayBuffer(8);
const view = new Int32Array(sab);
const src = `
const { parentPort, workerData } = require('node:worker_threads');
const v = new Int32Array(workerData.sab);
for (let i = 0; i < 100000; i++) Atomics.add(v, 0, 1);
parentPort.postMessage('done');
`;
const w = new Worker(src, { eval: true, workerData: { sab } }); // no copy: memory is shared
for (let i = 0; i < 100000; i++) Atomics.add(view, 0, 1);
w.on('message', () => {
console.log('two threads, 100k Atomics.add each ->', view[0], '(exactly 200000; no lost updates)');
resolve();
});
});
}
(async () => {
await streamDemo();
bufferDemo();
cloneDemo();
await blockDemo();
await workerDemo();
await sabDemo();
console.log('\nprocess: pid', process.pid, '| cpus', require('node:os').cpus().length);
})();
js-core/17-modern-syntax.js
'use strict';
// ---------- optional chaining & nullish ----------
const user = { profile: { name: 'Hendrix' }, tags: ['a'], greet() { return 'hi'; } };
console.log('?. property :', user.settings?.theme);
console.log('?. call :', user.greet?.(), user.missing?.());
console.log('?. index :', user.tags?.[0], user.other?.[0]);
console.log('short-circuits:', (() => { let n = 0; const f = () => n++; user.nope?.[f()]; return n; })(), '(the index expression never ran)');
console.log('?? vs || :', 0 ?? 'fallback', '|', 0 || 'fallback', '|', '' ?? 'f', '|', '' || 'f');
let a = null, b = 0, c;
a ??= 'assigned'; b ||= 'assigned'; c &&= 'never';
console.log('??= ||= &&= :', a, b, c);
// ?? cannot be mixed with && / || without parens
try { eval('null ?? 1 || 2'); } catch (e) { console.log('mixing ?? and ||:', e.constructor.name); }
// ---------- destructuring ----------
const cfg = { host: 'x', port: undefined, nested: { deep: [1, 2, 3] } };
const { host: h = 'localhost', port = 8080, missing: m = 'default', nested: { deep: [, second, ...restDeep] } } = cfg;
console.log('\nrename+default:', h, port, m, second, restDeep);
const { host, ...others } = cfg;
console.log('rest in object:', Object.keys(others));
function draw({ x = 0, y = 0, ...opts } = {}) { return `${x},${y} ${JSON.stringify(opts)}`; }
console.log('param default :', draw(), '|', draw({ x: 1, color: 'red' }));
const [p = 1, q = 2] = [undefined, null];
console.log('defaults only fire on undefined:', p, q);
// swap and nested array destructuring
let s1 = 1, s2 = 2; [s1, s2] = [s2, s1];
console.log('swap :', s1, s2);
const { length } = 'hello';
console.log('destructure a string:', length);
// destructuring in for..of over entries
for (const [k, v] of Object.entries({ a: 1 })) console.log('entries :', k, v);
// ---------- labelled statements ----------
console.log('\nlabelled break/continue:');
const found = [];
outer:
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
if (i * j > 4) break outer;
if (j === 2) continue outer;
found.push(`${i}${j}`);
}
}
console.log(' ', found.join(' '));
// a labelled block is a legal goto-forward
blk: { console.log(' in block'); if (true) break blk; console.log(' never'); }
// ---------- at() ----------
console.log('\nat():', [1, 2, 3].at(-1), 'abc'.at(-1), new Int8Array([1, 2]).at(-1));
// ---------- Object.groupBy / Map.groupBy ----------
const people = [
{ name: 'ann', dept: 'eng', age: 31 }, { name: 'bob', dept: 'eng', age: 45 },
{ name: 'cat', dept: 'ops', age: 27 },
];
console.log('\nObject.groupBy:', JSON.stringify(Object.groupBy(people, (p) => p.dept), null, 0));
const byDecade = Map.groupBy(people, (p) => Math.floor(p.age / 10) * 10);
console.log('Map.groupBy :', [...byDecade].map(([k, v]) => `${k}s: ${v.map((x) => x.name)}`).join(' | '));
console.log('groupBy result has null prototype:', Object.getPrototypeOf(Object.groupBy([], () => 'k')) === null);
// ---------- non-mutating array methods (ES2023) ----------
const arr = [3, 1, 2];
console.log('\ntoSorted :', arr.toSorted((x, y) => x - y), '| original:', arr);
console.log('toReversed :', arr.toReversed(), '| original:', arr);
console.log('toSpliced :', arr.toSpliced(1, 1, 'X', 'Y'), '| original:', arr);
console.log('with :', arr.with(0, 99), '| original:', arr);
console.log('with(-1) :', arr.with(-1, 99));
console.log('sort is stable & default is LEXICOGRAPHIC:', [10, 9, 1].sort(), 'vs', [10, 9, 1].toSorted((a, b) => a - b));
// ---------- findLast / flat / flatMap ----------
console.log('\nfindLast :', [1, 2, 3, 4].findLast((x) => x % 2 === 1), '| findLastIndex:', [1, 2, 3, 4].findLastIndex((x) => x % 2 === 1));
console.log('flat(Infinity):', [1, [2, [3, [4, [5]]]]].flat(Infinity));
console.log('flat removes holes:', [1, , 3].flat());
console.log('flatMap :', [1, 2, 3].flatMap((x) => (x % 2 ? [x, x] : [])));
console.log('copyWithin/fill:', [1, 2, 3, 4, 5].copyWithin(0, 3), new Array(3).fill(0));
// ---------- Set methods (ES2025) ----------
const A = new Set([1, 2, 3]), B = new Set([3, 4]);
console.log('\nunion :', [...A.union(B)]);
console.log('intersection :', [...A.intersection(B)]);
console.log('difference :', [...A.difference(B)]);
console.log('symmetricDifference:', [...A.symmetricDifference(B)]);
console.log('isSubsetOf/isSupersetOf/isDisjointFrom:',
new Set([1]).isSubsetOf(A), A.isSupersetOf(new Set([1])), A.isDisjointFrom(new Set([9])));
// ---------- string helpers ----------
console.log('\nreplaceAll :', 'a-b-c'.replaceAll('-', '+'));
console.log('matchAll :', [...'a1b2'.matchAll(/(\w)(\d)/g)].map((m) => m[1] + m[2]));
console.log('padStart/trimEnd:', '5'.padStart(3, '0'), JSON.stringify(' x '.trimEnd()));
console.log('raw template :', String.raw`a\nb`);
console.log('tagged template:', ((strings, ...v) => strings.raw.join('|') + ' :: ' + v)`x${1}y${2}z`);
console.log('localeCompare :', ['b', 'a', 'ä'].sort((x, y) => x.localeCompare(y, 'de')));
// ---------- numeric & misc ----------
console.log('\nnumeric separators:', 1_000_000, '| exponent:', 2 ** 10, '| binary:', 0b1010, '| octal:', 0o17);
console.log('Object.fromEntries:', Object.fromEntries([['a', 1], ['b', 2]]));
console.log('spread into call :', Math.max(...[1, 5, 3]));
console.log('computed keys :', { [`k${1}`]: 'v' });
console.log('Symbol.toStringTag:', Object.prototype.toString.call(new (class { get [Symbol.toStringTag]() { return 'Custom'; } })()));
console.log('error cause :', new Error('outer', { cause: new Error('inner') }).cause.message);
console.log('Array.prototype.includes vs indexOf on NaN:', [NaN].includes(NaN), [NaN].indexOf(NaN));
console.log('structuredClone :', structuredClone(new Map([['a', 1]])).get('a'));
js-core/18-using.ts
// Explicit resource management (ES2026 `using` / `await using`).
// Node 22's V8 12.4 does not parse `using` natively; run through tsx (esbuild downlevels it).
// Native support arrives with V8 13.x (Node 24+).
class FileHandle {
constructor(public readonly name: string) { console.log(` open ${name}`); }
read(): string { return `<${this.name} contents>`; }
[Symbol.dispose](): void { console.log(` close ${this.name}`); }
}
class Connection {
constructor(public readonly id: number) { console.log(` connect ${id}`); }
async query(): Promise<string> { return `rows for ${this.id}`; }
async [Symbol.asyncDispose](): Promise<void> {
await new Promise((r) => setTimeout(r, 5));
console.log(` disconnect ${this.id} (awaited)`);
}
}
function syncScope(): void {
console.log('sync scope:');
using f1 = new FileHandle('a.txt');
using f2 = new FileHandle('b.txt');
console.log(' read ', f1.read(), f2.read());
// disposal happens in REVERSE order at scope exit, even on throw
}
async function asyncScope(): Promise<void> {
console.log('\nasync scope:');
await using c = new Connection(1);
console.log(' query ', await c.query());
}
function throwingScope(): void {
console.log('\nthrowing scope:');
try {
using f = new FileHandle('c.txt');
throw new Error('boom');
} catch (e) {
console.log(' caught', (e as Error).message, '— dispose still ran first');
}
}
// DisposableStack is part of the same proposal but is absent from Node 22 — here is the shape.
class MiniDisposableStack {
#stack: Array<() => void> = [];
use<T extends { [Symbol.dispose](): void }>(r: T): T { this.#stack.push(() => r[Symbol.dispose]()); return r; }
defer(fn: () => void): void { this.#stack.push(fn); }
dispose(): void { while (this.#stack.length) this.#stack.pop()!(); }
[Symbol.dispose](): void { this.dispose(); }
}
function stackScope(): void {
console.log('\nDisposableStack (manual composition; polyfilled — absent in Node 22):');
console.log(' native DisposableStack available:', typeof (globalThis as Record<string, unknown>).DisposableStack !== 'undefined');
using stack = new MiniDisposableStack();
stack.use(new FileHandle('d.txt'));
stack.defer(() => console.log(' deferred cleanup'));
}
syncScope();
throwingScope();
stackScope();
void asyncScope();
js-core/19-implementations.js
'use strict';
// ============================================================ tiny test harness
let pass = 0, fail = 0;
const lines = [];
function t(name, fn) {
try { fn(); pass++; lines.push(` ok ${name}`); }
catch (e) { fail++; lines.push(` FAIL ${name}: ${e.message}`); }
}
const deepEqCheck = (a, b) => JSON.stringify(a) === JSON.stringify(b);
const eq = (a, b, m = '') => { if (!deepEqCheck(a, b)) throw new Error(`${m} expected ${JSON.stringify(b)}, got ${JSON.stringify(a)}`); };
const ok = (v, m = 'expected truthy') => { if (!v) throw new Error(m); };
// ============================================================ 1. Function.prototype.bind
Function.prototype.myBind = function (thisArg, ...bound) {
if (typeof this !== 'function') throw new TypeError('Bind must be called on a function');
const target = this;
function BoundFunction(...args) {
// `new BoundFunction()` ignores thisArg and constructs the target instead
return new.target
? Reflect.construct(target, [...bound, ...args], new.target === BoundFunction ? target : new.target)
: target.apply(thisArg, [...bound, ...args]);
}
// preserve the prototype chain so `instanceof` still works
if (target.prototype) BoundFunction.prototype = Object.create(target.prototype);
Object.defineProperty(BoundFunction, 'length', { value: Math.max(0, target.length - bound.length), configurable: true });
Object.defineProperty(BoundFunction, 'name', { value: 'bound ' + target.name, configurable: true });
return BoundFunction;
};
t('myBind binds this', () => eq(function () { return this.x; }.myBind({ x: 1 })(), 1));
t('myBind partially applies', () => eq(((a, b, c) => a + b + c).myBind(null, 1, 2)(3), 6));
t('myBind works with new', () => {
function P(a, b) { this.a = a; this.b = b; }
P.prototype.sum = function () { return this.a + this.b; };
const B = P.myBind({ ignored: true }, 10);
const inst = new B(5);
eq(inst.sum(), 15); ok(inst instanceof P, 'instanceof target');
});
t('myBind length/name', () => { const f = ((a, b, c) => 0).myBind(null, 1); eq(f.length, 2); eq(f.name, 'bound '); });
// ============================================================ 2. call / apply
Function.prototype.myCall = function (thisArg, ...args) {
const ctx = thisArg === null || thisArg === undefined ? globalThis : Object(thisArg);
const key = Symbol('fn');
Object.defineProperty(ctx, key, { value: this, configurable: true });
try { return ctx[key](...args); } finally { delete ctx[key]; }
};
Function.prototype.myApply = function (thisArg, args = []) { return this.myCall(thisArg, ...args); };
t('myCall / myApply', () => {
function greet(g, p) { return `${g}, ${this.name}${p}`; }
eq(greet.myCall({ name: 'A' }, 'hi', '!'), 'hi, A!');
eq(greet.myApply({ name: 'B' }, ['yo', '?']), 'yo, B?');
});
// ============================================================ 3. map / filter / reduce / forEach
Array.prototype.myMap = function (cb, thisArg) {
if (this == null) throw new TypeError('called on null or undefined');
if (typeof cb !== 'function') throw new TypeError(cb + ' is not a function');
const o = Object(this), len = o.length >>> 0;
const out = new Array(len);
for (let i = 0; i < len; i++) if (i in o) out[i] = cb.call(thisArg, o[i], i, o); // skip holes
return out;
};
Array.prototype.myFilter = function (cb, thisArg) {
const o = Object(this), len = o.length >>> 0, out = [];
for (let i = 0; i < len; i++) if (i in o && cb.call(thisArg, o[i], i, o)) out.push(o[i]);
return out;
};
Array.prototype.myReduce = function (cb, ...init) {
const o = Object(this), len = o.length >>> 0;
let i = 0, acc;
if (init.length) acc = init[0];
else {
while (i < len && !(i in o)) i++;
if (i >= len) throw new TypeError('Reduce of empty array with no initial value');
acc = o[i++];
}
for (; i < len; i++) if (i in o) acc = cb(acc, o[i], i, o);
return acc;
};
t('myMap', () => eq([1, 2, 3].myMap((x) => x * 2), [2, 4, 6]));
t('myMap preserves holes', () => { const r = [1, , 3].myMap((x) => x * 2); eq(1 in r, false); eq(r.length, 3); });
t('myMap passes index and array', () => eq([10, 20].myMap((v, i, a) => v + i + a.length), [12, 23]));
t('myFilter', () => eq([1, 2, 3, 4].myFilter((x) => x % 2 === 0), [2, 4]));
t('myReduce with init', () => eq([1, 2, 3].myReduce((a, b) => a + b, 10), 16));
t('myReduce without init', () => eq([1, 2, 3].myReduce((a, b) => a + b), 6));
t('myReduce empty throws', () => { try { [].myReduce((a, b) => a + b); throw new Error('no throw'); } catch (e) { ok(e instanceof TypeError); } });
// ============================================================ 4. deep clone with cycles
function deepClone(value, seen = new WeakMap()) {
if (value === null || typeof value !== 'object') return value; // primitives + functions returned as-is
if (seen.has(value)) return seen.get(value); // cycle / shared reference
if (value instanceof Date) return new Date(value.getTime());
if (value instanceof RegExp) { const r = new RegExp(value.source, value.flags); r.lastIndex = value.lastIndex; return r; }
if (ArrayBuffer.isView(value)) return new value.constructor(value);
if (value instanceof ArrayBuffer) return value.slice(0);
if (value instanceof Map) {
const out = new Map(); seen.set(value, out);
for (const [k, v] of value) out.set(deepClone(k, seen), deepClone(v, seen));
return out;
}
if (value instanceof Set) {
const out = new Set(); seen.set(value, out);
for (const v of value) out.add(deepClone(v, seen));
return out;
}
if (value instanceof Error) {
const out = new value.constructor(value.message, value.cause !== undefined ? { cause: deepClone(value.cause, seen) } : undefined);
seen.set(value, out); out.stack = value.stack;
return out;
}
const out = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value));
seen.set(value, out);
for (const key of Reflect.ownKeys(value)) {
const d = Object.getOwnPropertyDescriptor(value, key);
if ('value' in d) Object.defineProperty(out, key, { ...d, value: deepClone(d.value, seen) });
else Object.defineProperty(out, key, d); // keep getters/setters
}
return out;
}
t('deepClone nested', () => { const s = { a: { b: [1, { c: 2 }] } }; const c = deepClone(s); eq(c, s); ok(c.a !== s.a && c.a.b[1] !== s.a.b[1]); });
t('deepClone cycles', () => { const s = { n: 1 }; s.self = s; const c = deepClone(s); ok(c.self === c); });
t('deepClone shared refs stay shared', () => { const shared = { x: 1 }; const c = deepClone({ a: shared, b: shared }); ok(c.a === c.b); });
t('deepClone Map/Set/Date/RegExp', () => {
const c = deepClone({ m: new Map([['k', { v: 1 }]]), s: new Set([1]), d: new Date(5), r: /x/g });
eq(c.m.get('k').v, 1); ok(c.s.has(1)); eq(c.d.getTime(), 5); eq(String(c.r), '/x/g');
});
t('deepClone keeps prototype', () => { class C { constructor() { this.x = 1; } } ok(deepClone(new C()) instanceof C); });
t('deepClone keeps symbols and non-enumerables', () => {
const sym = Symbol('s'); const src = { [sym]: 1 };
Object.defineProperty(src, 'hidden', { value: 2, enumerable: false });
const c = deepClone(src); eq(c[sym], 1); eq(c.hidden, 2);
});
// ============================================================ 5. deep equal
function deepEqual(a, b, seen = new WeakMap()) {
if (Object.is(a, b)) return true;
if (typeof a !== typeof b) return false;
if (a === null || b === null || typeof a !== 'object') return false;
if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) return false;
if (seen.get(a) === b) return true; // assume equal while recursing (cycles)
seen.set(a, b);
if (a instanceof Date) return a.getTime() === b.getTime();
if (a instanceof RegExp) return a.source === b.source && a.flags === b.flags;
if (a instanceof Map) {
if (a.size !== b.size) return false;
for (const [k, v] of a) {
if (!b.has(k)) { // key may be a deep-equal object
const match = [...b.keys()].find((bk) => deepEqual(k, bk, seen));
if (match === undefined) return false;
if (!deepEqual(v, b.get(match), seen)) return false;
} else if (!deepEqual(v, b.get(k), seen)) return false;
}
return true;
}
if (a instanceof Set) {
if (a.size !== b.size) return false;
for (const v of a) if (!b.has(v) && ![...b].some((bv) => deepEqual(v, bv, seen))) return false;
return true;
}
if (ArrayBuffer.isView(a)) return a.length === b.length && [...a].every((v, i) => Object.is(v, b[i]));
const ka = Reflect.ownKeys(a), kb = Reflect.ownKeys(b);
if (ka.length !== kb.length) return false;
return ka.every((k) => Object.prototype.hasOwnProperty.call(b, k) && deepEqual(a[k], b[k], seen));
}
t('deepEqual basics', () => { ok(deepEqual({ a: [1, { b: 2 }] }, { a: [1, { b: 2 }] })); ok(!deepEqual({ a: 1 }, { a: 2 })); });
t('deepEqual NaN and -0', () => { ok(deepEqual(NaN, NaN)); ok(!deepEqual(0, -0)); });
t('deepEqual key count', () => ok(!deepEqual({ a: 1 }, { a: 1, b: undefined })));
t('deepEqual different prototypes', () => { class C {} ok(!deepEqual(new C(), {})); });
t('deepEqual cycles', () => { const a = { }; a.s = a; const b = {}; b.s = b; ok(deepEqual(a, b)); });
t('deepEqual Map/Set', () => { ok(deepEqual(new Map([['k', [1]]]), new Map([['k', [1]]]))); ok(deepEqual(new Set([{ a: 1 }]), new Set([{ a: 1 }]))); });
// ============================================================ 6. flatten with depth
function flatten(arr, depth = 1) {
const out = [];
const step = (a, d) => {
for (const v of a) (Array.isArray(v) && d > 0) ? step(v, d - 1) : out.push(v);
};
step(arr, depth);
return out;
}
function flattenIterative(arr) { // no recursion: stack-based, safe for deep nesting
const stack = [...arr], out = [];
while (stack.length) {
const v = stack.pop();
if (Array.isArray(v)) stack.push(...v); else out.push(v);
}
return out.reverse();
}
t('flatten depth 1', () => eq(flatten([1, [2, [3, [4]]]]), [1, 2, [3, [4]]]));
t('flatten depth 2', () => eq(flatten([1, [2, [3, [4]]]], 2), [1, 2, 3, [4]]));
t('flatten Infinity', () => eq(flatten([1, [2, [3, [4]]]], Infinity), [1, 2, 3, 4]));
t('flattenIterative', () => eq(flattenIterative([1, [2, [3, [4]]]]), [1, 2, 3, 4]));
t('flatten deep without stack overflow', () => {
let deep = [1]; for (let i = 0; i < 50_000; i++) deep = [deep];
eq(flattenIterative(deep), [1]);
});
// ============================================================ 7. once
function once(fn) {
let called = false, result;
const wrapped = function (...args) {
if (called) return result;
called = true;
result = fn.apply(this, args);
fn = null; // let the closure be collected
return result;
};
wrapped.reset = () => { called = false; result = undefined; };
return wrapped;
}
t('once', () => { let n = 0; const f = once(() => ++n); eq([f(), f(), f()], [1, 1, 1]); eq(n, 1); });
// ============================================================ 8. EventEmitter
class Emitter {
#events = new Map();
on(name, fn) { (this.#events.get(name) ?? this.#events.set(name, new Set()).get(name)).add(fn); return () => this.off(name, fn); }
once(name, fn) {
const wrap = (...a) => { this.off(name, wrap); fn(...a); };
wrap.listener = fn;
return this.on(name, wrap);
}
off(name, fn) {
const set = this.#events.get(name);
if (!set) return this;
for (const l of set) if (l === fn || l.listener === fn) set.delete(l);
if (set.size === 0) this.#events.delete(name);
return this;
}
emit(name, ...args) {
const set = this.#events.get(name);
if (!set || set.size === 0) {
if (name === 'error') throw args[0] instanceof Error ? args[0] : new Error('Unhandled error event');
return false;
}
for (const l of [...set]) l(...args); // copy: a listener may unsubscribe during emit
return true;
}
listenerCount(name) { return this.#events.get(name)?.size ?? 0; }
}
t('Emitter on/emit/off', () => {
const e = new Emitter(); const seen = [];
const off = e.on('x', (v) => seen.push('a' + v));
e.on('x', (v) => seen.push('b' + v));
e.emit('x', 1); off(); e.emit('x', 2);
eq(seen, ['a1', 'b1', 'b2']);
});
t('Emitter once', () => { const e = new Emitter(); let n = 0; e.once('y', () => n++); e.emit('y'); e.emit('y'); eq(n, 1); });
t('Emitter safe removal during emit', () => {
const e = new Emitter(); const seen = [];
const h1 = () => { seen.push(1); e.off('z', h2); };
const h2 = () => seen.push(2);
e.on('z', h1); e.on('z', h2); e.emit('z');
eq(seen, [1, 2]); // snapshot semantics: h2 still runs this round
});
t('Emitter throws on unhandled error', () => { try { new Emitter().emit('error', new Error('e')); throw new Error('no throw'); } catch (e) { eq(e.message, 'e'); } });
// ============================================================ 9. LRU cache with Map
class LRU {
#max; #map = new Map();
constructor(max) { this.#max = max; }
get size() { return this.#map.size; }
has(k) { return this.#map.has(k); }
get(k) {
if (!this.#map.has(k)) return undefined;
const v = this.#map.get(k);
this.#map.delete(k); this.#map.set(k, v); // Map preserves insertion order -> re-insert = "recently used"
return v;
}
set(k, v) {
if (this.#map.has(k)) this.#map.delete(k);
else if (this.#map.size >= this.#max) this.#map.delete(this.#map.keys().next().value); // evict oldest
this.#map.set(k, v);
return this;
}
delete(k) { return this.#map.delete(k); }
keys() { return [...this.#map.keys()]; }
}
t('LRU evicts least recently used', () => {
const c = new LRU(3);
c.set('a', 1).set('b', 2).set('c', 3);
c.get('a'); // a becomes most-recent
c.set('d', 4); // evicts b
eq(c.keys(), ['c', 'a', 'd']);
eq(c.get('b'), undefined);
});
t('LRU update refreshes recency', () => { const c = new LRU(2); c.set('a', 1).set('b', 2).set('a', 9).set('c', 3); eq(c.keys(), ['a', 'c']); });
// ============================================================ 10. retry with exponential backoff + jitter
async function retry(fn, { attempts = 5, baseMs = 10, maxMs = 1000, factor = 2, jitter = true, signal, shouldRetry = () => true } = {}) {
let lastErr;
for (let i = 0; i < attempts; i++) {
if (signal?.aborted) throw signal.reason;
try { return await fn(i); }
catch (e) {
lastErr = e;
if (i === attempts - 1 || !shouldRetry(e, i)) break;
const exp = Math.min(maxMs, baseMs * factor ** i);
const delay = jitter ? Math.random() * exp : exp; // full jitter
await new Promise((res, rej) => {
const id = setTimeout(res, delay);
signal?.addEventListener('abort', () => { clearTimeout(id); rej(signal.reason); }, { once: true });
});
}
}
throw lastErr;
}
// ============================================================ 11. promisify / callbackify
function promisify(fn) {
return function (...args) {
return new Promise((resolve, reject) => {
fn.call(this, ...args, (err, ...values) => (err ? reject(err) : resolve(values.length > 1 ? values : values[0])));
});
};
}
function callbackify(asyncFn) {
return function (...args) {
const cb = args.pop();
asyncFn.apply(this, args).then((v) => cb(null, v), (e) => cb(e || new Error('rejected with falsy value')));
};
}
// ============================================================ 12. chunk / zip / range / groupBy / uniqBy / partition
const chunk = (arr, size) => {
if (size < 1) throw new RangeError('size must be >= 1');
return Array.from({ length: Math.ceil(arr.length / size) }, (_, i) => arr.slice(i * size, i * size + size));
};
const zip = (...arrays) => Array.from({ length: Math.min(...arrays.map((a) => a.length)) }, (_, i) => arrays.map((a) => a[i]));
const range = (start, end, step = 1) => Array.from({ length: Math.max(0, Math.ceil((end - start) / step)) }, (_, i) => start + i * step);
const groupBy = (arr, keyFn) => arr.reduce((acc, item, i) => { const k = keyFn(item, i); (acc[k] ??= []).push(item); return acc; }, Object.create(null));
const uniqBy = (arr, keyFn = (x) => x) => { const seen = new Set(); return arr.filter((x) => { const k = keyFn(x); return seen.has(k) ? false : (seen.add(k), true); }); };
const partition = (arr, pred) => arr.reduce(([yes, no], x, i) => (pred(x, i) ? [[...yes, x], no] : [yes, [...no, x]]), [[], []]);
const countBy = (arr, keyFn) => arr.reduce((acc, x) => { const k = keyFn(x); acc[k] = (acc[k] ?? 0) + 1; return acc; }, Object.create(null));
t('chunk', () => eq(chunk([1, 2, 3, 4, 5], 2), [[1, 2], [3, 4], [5]]));
t('zip', () => eq(zip([1, 2, 3], 'ab'.split('')), [[1, 'a'], [2, 'b']]));
t('range', () => { eq(range(0, 5), [0, 1, 2, 3, 4]); eq(range(0, 10, 3), [0, 3, 6, 9]); eq(range(5, 0), []); });
t('groupBy', () => eq(groupBy(['one', 'two', 'three'], (s) => s.length), { 3: ['one', 'two'], 5: ['three'] }));
t('uniqBy', () => eq(uniqBy([{ id: 1 }, { id: 1 }, { id: 2 }], (o) => o.id).length, 2));
t('partition', () => eq(partition([1, 2, 3, 4], (x) => x % 2), [[1, 3], [2, 4]]));
t('countBy', () => eq(countBy('aabbbc'.split(''), (c) => c), { a: 2, b: 3, c: 1 }));
// ============================================================ 13. get / set by path, deepFreeze, pick/omit
const get = (obj, path, fallback) => {
const keys = Array.isArray(path) ? path : path.replace(/\[(\d+)\]/g, '.$1').split('.').filter(Boolean);
let cur = obj;
for (const k of keys) { if (cur == null) return fallback; cur = cur[k]; }
return cur === undefined ? fallback : cur;
};
const setPath = (obj, path, value) => {
const keys = Array.isArray(path) ? path : path.replace(/\[(\d+)\]/g, '.$1').split('.').filter(Boolean);
let cur = obj;
keys.forEach((k, i) => {
if (i === keys.length - 1) cur[k] = value;
else { if (cur[k] == null || typeof cur[k] !== 'object') cur[k] = /^\d+$/.test(keys[i + 1]) ? [] : {}; cur = cur[k]; }
});
return obj;
};
function deepFreeze(o, seen = new WeakSet()) {
if (o === null || typeof o !== 'object' || seen.has(o)) return o;
seen.add(o);
Object.freeze(o);
for (const k of Reflect.ownKeys(o)) {
const d = Object.getOwnPropertyDescriptor(o, k);
if ('value' in d) deepFreeze(d.value, seen); // do not trigger getters
}
return o;
}
const pick = (o, keys) => Object.fromEntries(keys.filter((k) => k in o).map((k) => [k, o[k]]));
const omit = (o, keys) => Object.fromEntries(Object.entries(o).filter(([k]) => !keys.includes(k)));
t('get by path', () => { eq(get({ a: { b: [{ c: 1 }] } }, 'a.b[0].c'), 1); eq(get({}, 'x.y', 'def'), 'def'); });
t('set by path', () => eq(setPath({}, 'a.b[1].c', 7), { a: { b: [null, { c: 7 }] } }));
t('deepFreeze', () => { const o = deepFreeze({ a: { b: 1 } }); try { o.a.b = 2; } catch {} eq(o.a.b, 1); ok(Object.isFrozen(o.a)); });
t('deepFreeze cycles', () => { const o = { }; o.self = o; deepFreeze(o); ok(Object.isFrozen(o)); });
t('pick/omit', () => { eq(pick({ a: 1, b: 2, c: 3 }, ['a', 'c']), { a: 1, c: 3 }); eq(omit({ a: 1, b: 2 }, ['a']), { b: 2 }); });
// ============================================================ 14. implement `new`
function myNew(Ctor, ...args) {
if (typeof Ctor !== 'function') throw new TypeError('not a constructor');
const proto = typeof Ctor.prototype === 'object' && Ctor.prototype !== null ? Ctor.prototype : Object.prototype;
const obj = Object.create(proto);
const ret = Ctor.apply(obj, args);
return (ret !== null && (typeof ret === 'object' || typeof ret === 'function')) ? ret : obj;
}
t('myNew', () => {
function P(x) { this.x = x; }
P.prototype.get = function () { return this.x; };
const i = myNew(P, 5);
eq(i.get(), 5); ok(i instanceof P);
});
t('myNew honours an explicit object return', () => { function P() { this.a = 1; return { b: 2 }; } eq(myNew(P), { b: 2 }); });
// ============================================================ 15. instanceof
function myInstanceOf(obj, Ctor) {
if (typeof Ctor !== 'function') throw new TypeError('Right-hand side is not callable');
if (typeof Ctor[Symbol.hasInstance] === 'function' && Ctor[Symbol.hasInstance] !== Function.prototype[Symbol.hasInstance]) {
return !!Ctor[Symbol.hasInstance](obj);
}
if (obj === null || (typeof obj !== 'object' && typeof obj !== 'function')) return false;
const target = Ctor.prototype;
let proto = Object.getPrototypeOf(obj);
while (proto !== null) { if (proto === target) return true; proto = Object.getPrototypeOf(proto); }
return false;
}
t('myInstanceOf', () => { class A {} class B extends A {} ok(myInstanceOf(new B(), A)); ok(!myInstanceOf({}, A)); ok(!myInstanceOf(1, A)); });
// ============================================================ 16. throttle/debounce/curry live in 07-functions.js
// ============================================================ 17. sleep, timeout wrapper, defer
const sleep = (ms, signal) => new Promise((res, rej) => {
const id = setTimeout(res, ms);
signal?.addEventListener('abort', () => { clearTimeout(id); rej(signal.reason); }, { once: true });
});
const withTimeout = (promise, ms, msg = 'Timed out') => {
const ac = new AbortController();
const timer = new Promise((_, rej) => setTimeout(() => rej(new Error(msg)), ms));
return Promise.race([promise.finally(() => ac.abort()), timer]);
};
// ============================================================ async tests
(async () => {
// retry
let tries = 0;
const flaky = async () => { if (++tries < 3) throw new Error('flaky'); return 'ok after ' + tries; };
const r = await retry(flaky, { attempts: 5, baseMs: 1 });
t('retry succeeds after transient failures', () => eq(r, 'ok after 3'));
tries = 0;
try {
await retry(async () => { tries++; throw new Error('always'); }, { attempts: 3, baseMs: 1 });
t('retry gives up', () => { throw new Error('should have thrown'); });
} catch (e) { t('retry gives up after N attempts', () => { eq(e.message, 'always'); eq(tries, 3); }); }
tries = 0;
try {
await retry(async () => { tries++; const e = new Error('fatal'); e.status = 400; throw e; },
{ attempts: 5, baseMs: 1, shouldRetry: (e) => e.status >= 500 });
} catch { }
t('retry respects shouldRetry', () => eq(tries, 1));
// promisify / callbackify
const readish = (n, cb) => setTimeout(() => (n < 0 ? cb(new Error('neg')) : cb(null, n * 2)), 1);
const readAsync = promisify(readish);
const promisifiedValue = await readAsync(5);
t('promisify resolves', () => eq(promisifiedValue, 10));
let promisifyErr; try { await readAsync(-1); } catch (e) { promisifyErr = e.message; }
t('promisify rejects', () => eq(promisifyErr, 'neg'));
const back = callbackify(async (x) => x + 1);
await new Promise((res) => back(1, (err, v) => { t('callbackify', () => { eq(err, null); eq(v, 2); }); res(); }));
// withTimeout
let timedOut = false;
try { await withTimeout(sleep(50), 10); } catch (e) { timedOut = e.message === 'Timed out'; }
t('withTimeout rejects slow work', () => ok(timedOut));
const fastValue = await withTimeout(Promise.resolve('fast'), 50);
t('withTimeout passes fast work', () => eq(fastValue, 'fast'));
console.log(lines.join('\n'));
console.log(`\n${pass} passed, ${fail} failed`);
if (fail) process.exitCode = 1;
})();
js-core/20-puzzles.js
'use strict';
const show = (label, fn) => { try { console.log(label.padEnd(52), '=>', JSON.stringify(fn(), (k, v) => (typeof v === 'undefined' ? '<undefined>' : v))); } catch (e) { console.log(label.padEnd(52), '=>', e.constructor.name + ': ' + e.message.split('\n')[0]); } };
show('typeof null', () => typeof null);
show('typeof function(){}', () => typeof function () {});
show('typeof NaN', () => typeof NaN);
show('typeof undeclared', () => typeof undeclaredThing);
show('[] + []', () => [] + []);
show('[] + {}', () => [] + {});
show('({}) + []', () => ({}) + []);
show('1 + "2" - 1', () => 1 + '2' - 1);
show('"5" - - "2"', () => '5' - - '2');
show('0.1 + 0.2 == 0.3', () => 0.1 + 0.2 == 0.3);
show('[1,2,3] + [4,5]', () => [1, 2, 3] + [4, 5]);
show('typeof (()=>{})()', () => typeof (() => {})());
show('[10,1,3].sort()', () => [10, 1, 3].sort());
show('["1","7","11"].map(parseInt)', () => ['1', '7', '11'].map(parseInt));
show('["1","7","11"].map(Number)', () => ['1', '7', '11'].map(Number));
show('[1,2,3].map(x=>x*2).filter(x=>x>2)', () => [1, 2, 3].map((x) => x * 2).filter((x) => x > 2));
show('Array(3)', () => Array(3));
show('Array(3).map(()=>1)', () => Array(3).map(() => 1));
show('[...Array(3)].map(()=>1)', () => [...Array(3)].map(() => 1));
show('Array.from({length:3}, (_,i)=>i)', () => Array.from({ length: 3 }, (_, i) => i));
show('[,,].length', () => [, , ].length);
show('"b"+"a"+ +"a"+"a"', () => 'b' + 'a' + +'a' + 'a');
show('!!"false" == !!"true"', () => !!'false' == !!'true');
show('typeof typeof 1', () => typeof typeof 1);
show('(function(){return typeof arguments})()', () => (function () { return typeof arguments; })());
show('parseInt(0.0000005)', () => parseInt(0.0000005));
show('parseInt("08")', () => parseInt('08'));
show('Number("")', () => Number(''));
show('Number(" 12 ")', () => Number(' 12 '));
show('Number(null) / Number(undefined)', () => [Number(null), Number(undefined)]);
show('+[] / +{} / +"" ', () => [+[], String(+{}), +'']);
show('JSON.stringify({a:undefined,b:()=>1,c:NaN})', () => JSON.stringify({ a: undefined, b: () => 1, c: NaN }));
show('JSON.stringify([undefined, ()=>1])', () => JSON.stringify([undefined, () => 1]));
show('Object.keys("abc")', () => Object.keys('abc'));
show('"abc".length === [..."abc"].length', () => 'abc'.length === [...'abc'].length);
show('{} instanceof Object', () => ({}) instanceof Object);
show('Object.create(null) instanceof Object', () => Object.create(null) instanceof Object);
show('[1,[2,[3]]].flat()', () => [1, [2, [3]]].flat());
show('new Set("hello").size', () => new Set('hello').size);
show('[..."hello"].reverse().join("")', () => [...'hello'].reverse().join(''));
show('typeof class{}', () => typeof class {});
show('(()=>{ let x = y = 5; return typeof y })()', () => { let x; try { eval('x = y2 = 5'); } catch (e) { return e.constructor.name; } });
show('void 0 === undefined', () => void 0 === undefined);
show('(function f(){ return f.name })()', () => (function f() { return f.name; })());
show('const o={a:1}; delete o.a; o.a', () => { const o = { a: 1 }; delete o.a; return o.a; });
show('[1,2,3].indexOf("2")', () => [1, 2, 3].indexOf('2'));
show('["a","b"].forEach returns', () => ['a', 'b'].forEach(() => 1));
show('[3,1,2].sort((a,b)=>a-b) mutates?', () => { const a = [3, 1, 2]; a.sort((x, y) => x - y); return a; });
show('"5" * "2"', () => '5' * '2');
show('null + 1 / undefined + 1', () => [null + 1, undefined + 1]);
show('true + true + true', () => true + true + true);
show('[] == false == true', () => [] == false == true);
show('Math.max([1,2,3])', () => Math.max([1, 2, 3]));
show('new Array(3).fill().map((_,i)=>i)', () => new Array(3).fill().map((_, i) => i));
show('{a:1, a:2}.a', () => ({ a: 1, a: 2 }).a);
show('obj key order (int keys sort first)', () => Object.keys({ b: 1, 2: 1, a: 1, 1: 1 }));
show('typeof Symbol.iterator', () => typeof Symbol.iterator);
show('"" || null || 0 || "last"', () => '' || null || 0 || 'last');
show('({} ?? "x")', () => JSON.stringify({} ?? 'x'));
show('[..."😀"].length vs "😀".length', () => [[...'😀'].length, '😀'.length]);
js-core/bench.js
function bench(name, fn, iters=3e7){ fn(); const t=process.hrtime.bigint(); let s=0; for(let i=0;i<iters;i++) s+=fn(i); const d=Number(process.hrtime.bigint()-t)/1e6; console.log(name.padEnd(34), d.toFixed(1)+' ms', '(sink '+ (s%7) +')'); }
const mono = Array.from({length:1000},(_,i)=>({x:i,y:i}));
const poly = Array.from({length:1000},(_,i)=> i%4===0?{x:i,y:i}: i%4===1?{y:i,x:i}: i%4===2?{x:i,y:i,z:i}:{a:i,x:i,y:i});
let m=0,p=0;
bench('monomorphic .x read', (i)=> mono[(m++)%1000].x);
bench('polymorphic(4 shapes) .x read', (i)=> poly[(p++)%1000].x);
const packed = Array.from({length:100000},(_,i)=>i);
const holey = Array.from({length:100000},(_,i)=>i); delete holey[5000];
const dbl = Array.from({length:100000},(_,i)=>i+0.5);
function sum(a){let s=0;for(let i=0;i<a.length;i++)s+=a[i];return s;}
function t(name,a){ for(let k=0;k<50;k++)sum(a); const s=process.hrtime.bigint(); let acc=0; for(let k=0;k<300;k++)acc+=sum(a); const d=Number(process.hrtime.bigint()-s)/1e6; console.log(name.padEnd(34), d.toFixed(1)+' ms'); }
t('PACKED_SMI sum x300', packed); t('HOLEY_SMI sum x300', holey); t('PACKED_DOUBLE sum x300', dbl);
js-core/bench2.js
// Isolate IC state: same read function, different callsites, no shared counters.
function makeReader() { return new Function('objs', 'let s=0; for (let i=0;i<objs.length;i++) s+=objs[i].x; return s;'); }
const N = 20000;
const shapesMono = Array.from({length:N}, (_,i)=>({x:i, y:i}));
const shapes2 = Array.from({length:N}, (_,i)=> i%2 ? {x:i,y:i} : {y:i,x:i});
const shapes4 = Array.from({length:N}, (_,i)=> [{x:i,y:i},{y:i,x:i},{x:i,y:i,z:i},{a:i,x:i}][i%4]);
const shapes8 = Array.from({length:N}, (_,i)=> [{x:i},{x:i,y:i},{y:i,x:i},{x:i,y:i,z:i},{a:i,x:i},{b:i,x:i},{c:i,x:i},{d:i,e:i,x:i}][i%8]);
function time(label, arr) {
const read = makeReader(); // fresh callsite each time
for (let k=0;k<200;k++) read(arr); // warm up / let ICs settle
const t=process.hrtime.bigint(); let acc=0;
for (let k=0;k<2000;k++) acc += read(arr);
console.log(label.padEnd(30), (Number(process.hrtime.bigint()-t)/1e6).toFixed(1)+' ms');
}
time('monomorphic (1 shape)', shapesMono);
time('polymorphic (2 shapes)', shapes2);
time('polymorphic (4 shapes)', shapes4);
time('megamorphic (8 shapes)', shapes8);
console.log('--- holey vs packed, fresh fn per case ---');
function timeSum(label, arr){
const sum = new Function('a','let s=0;for(let i=0;i<a.length;i++)s+=a[i];return s;');
for(let k=0;k<200;k++) sum(arr);
const t=process.hrtime.bigint(); let acc=0;
for(let k=0;k<2000;k++) acc+=sum(arr);
console.log(label.padEnd(30), (Number(process.hrtime.bigint()-t)/1e6).toFixed(1)+' ms');
}
const packed = Array.from({length:20000},(_,i)=>i);
const holey = Array.from({length:20000},(_,i)=>i); delete holey[10];
const holey2 = new Array(20000); for(let i=0;i<20000;i++) if(i%2) holey2[i]=i;
timeSum('PACKED_SMI', packed);
timeSum('HOLEY_SMI (one delete)', holey);
timeSum('HOLEY_SMI (50% holes)', holey2);
js-core/feat.mjs
const has = (label, fn) => { let r; try { r = fn(); } catch (e) { r = 'throws: ' + e.constructor.name; } console.log(label.padEnd(38), r); };
has('Iterator helpers (.map on iterator)', () => typeof [].values().map === 'function');
has('Set.prototype.union', () => typeof new Set().union === 'function');
has('Object.groupBy', () => typeof Object.groupBy === 'function');
has('Map.groupBy', () => typeof Map.groupBy === 'function');
has('Promise.try', () => typeof Promise.try === 'function');
has('RegExp.escape', () => typeof RegExp.escape === 'function');
has('Error.isError', () => typeof Error.isError === 'function');
has('Array.fromAsync', () => typeof Array.fromAsync === 'function');
has('Float16Array', () => typeof Float16Array !== 'undefined');
has('Math.f16round', () => typeof Math.f16round === 'function');
has('Math.sumPrecise', () => typeof Math.sumPrecise === 'function');
has('Temporal', () => typeof globalThis.Temporal !== 'undefined');
has('Map.prototype.getOrInsert', () => typeof new Map().getOrInsert === 'function');
has('Uint8Array.prototype.toBase64', () => typeof new Uint8Array().toBase64 === 'function');
has('Symbol.dispose', () => typeof Symbol.dispose !== 'undefined');
has('Array.prototype.toSorted', () => typeof [].toSorted === 'function');
has('Array.prototype.with', () => typeof [].with === 'function');
has('structuredClone', () => typeof structuredClone === 'function');
has('Object.hasOwn', () => typeof Object.hasOwn === 'function');
has('AbortSignal.timeout', () => typeof AbortSignal.timeout === 'function');
console.log('--- set ops demo ---');
const a = new Set([1,2,3]), b = new Set([3,4]);
console.log([...a.union(b)], [...a.intersection(b)], [...a.difference(b)], [...a.symmetricDifference(b)], a.isDisjointFrom(new Set([9])));
console.log('--- groupBy ---');
console.log(JSON.stringify(Object.groupBy([1,2,3,4,5], n => n%2 ? 'odd':'even')));
js-core/myp.js
const PENDING='pending', FULFILLED='fulfilled', REJECTED='rejected';
class MyPromise {
#state = PENDING; #value; #cbs = [];
constructor(executor) {
const resolve = v => this.#settle(FULFILLED, v);
const reject = r => this.#settle(REJECTED, r);
try { executor(v => this.#resolveWith(v, resolve, reject), reject); }
catch (e) { reject(e); }
}
#resolveWith(v, resolve, reject) {
if (v === this) return reject(new TypeError('Chaining cycle detected'));
if (v && (typeof v === 'object' || typeof v === 'function')) {
let then;
try { then = v.then; } catch (e) { return reject(e); }
if (typeof then === 'function') {
let called = false;
try {
return then.call(v,
y => { if (called) return; called = true; this.#resolveWith(y, resolve, reject); },
r => { if (called) return; called = true; reject(r); });
} catch (e) { if (!called) reject(e); return; }
}
}
resolve(v);
}
#settle(state, value) {
if (this.#state !== PENDING) return;
this.#state = state; this.#value = value;
for (const cb of this.#cbs) queueMicrotask(cb);
this.#cbs = [];
}
then(onFul, onRej) {
return new MyPromise((resolve, reject) => {
const run = () => queueMicrotask(() => {
const handler = this.#state === FULFILLED ? onFul : onRej;
if (typeof handler !== 'function') {
this.#state === FULFILLED ? resolve(this.#value) : reject(this.#value);
return;
}
try { resolve(handler(this.#value)); } catch (e) { reject(e); }
});
this.#state === PENDING ? this.#cbs.push(run) : run();
});
}
catch(onRej) { return this.then(undefined, onRej); }
finally(fn) { return this.then(v => { fn(); return v; }, r => { fn(); throw r; }); }
static resolve(v) { return v instanceof MyPromise ? v : new MyPromise(res => res(v)); }
static reject(r) { return new MyPromise((_, rej) => rej(r)); }
static all(iter) {
return new MyPromise((resolve, reject) => {
const items = [...iter]; const out = new Array(items.length); let left = items.length;
if (!left) return resolve([]);
items.forEach((p, i) => MyPromise.resolve(p).then(v => { out[i] = v; if (--left === 0) resolve(out); }, reject));
});
}
static race(iter) { return new MyPromise((res, rej) => { for (const p of iter) MyPromise.resolve(p).then(res, rej); }); }
static allSettled(iter) {
return MyPromise.all([...iter].map(p => MyPromise.resolve(p).then(
value => ({ status: 'fulfilled', value }), reason => ({ status: 'rejected', reason }))));
}
}
// tests
const log = [];
const t = (name, ok) => log.push((ok ? 'PASS ' : 'FAIL ') + name);
(async () => {
t('resolve/then', await new MyPromise(r => r(1)).then(v => v + 1) === 2);
t('reject/catch', await new MyPromise((_, j) => j('e')).catch(e => e + '!') === 'e!');
t('thenable adoption', await new MyPromise(r => r({ then: (res) => res(42) })) === 42);
t('nested promise', await new MyPromise(r => r(MyPromise.resolve(7))) === 7);
t('all', JSON.stringify(await MyPromise.all([1, MyPromise.resolve(2), Promise.resolve(3)])) === '[1,2,3]');
t('race', await MyPromise.race([new MyPromise(r => setTimeout(() => r('slow'), 20)), MyPromise.resolve('fast')]) === 'fast');
t('allSettled', (await MyPromise.allSettled([MyPromise.resolve(1), MyPromise.reject('x')])).map(o => o.status).join() === 'fulfilled,rejected');
t('throw in handler propagates', await MyPromise.resolve(1).then(() => { throw 'boom'; }).catch(e => e) === 'boom');
t('settle once', await new MyPromise(r => { r('a'); r('b'); }) === 'a');
t('finally passthrough', await MyPromise.resolve(5).finally(() => 99) === 5);
const order = [];
MyPromise.resolve().then(() => order.push('mt'));
order.push('sync');
await null; await null;
t('async ordering', order.join() === 'sync,mt');
console.log(log.join('\n'));
})();
js-core/p1.js
console.log('1 sync');
setTimeout(() => console.log('2 setTimeout 0'), 0);
setImmediate(() => console.log('3 setImmediate'));
process.nextTick(() => console.log('4 nextTick'));
Promise.resolve().then(() => console.log('5 microtask'));
queueMicrotask(() => console.log('6 queueMicrotask'));
(async () => { console.log('7 async body sync'); await null; console.log('8 after await'); })();
console.log('9 sync end');
js-core/p2.js
async function a() { console.log('a1'); await b(); console.log('a2'); }
async function b() { console.log('b1'); }
console.log('start');
a();
new Promise(r => { console.log('p exec'); r(); }).then(() => console.log('p then'));
console.log('end');
js-core/p3.js
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => { console.log('mt1'); process.nextTick(() => console.log('tick in mt')); });
process.nextTick(() => { console.log('tick1'); Promise.resolve().then(() => console.log('mt in tick')); });
js-core/pool.js
async function pMap(items, mapper, concurrency = 4) {
const out = new Array(items.length);
let i = 0, active = 0, peak = 0;
await new Promise((resolve, reject) => {
const next = () => {
if (i >= items.length && active === 0) return resolve();
while (active < concurrency && i < items.length) {
const idx = i++; active++; peak = Math.max(peak, active);
Promise.resolve(mapper(items[idx], idx))
.then(v => { out[idx] = v; active--; next(); }, reject);
}
};
next();
});
return { out, peak };
}
const sleep = ms => new Promise(r => setTimeout(r, ms));
(async () => {
const t = Date.now();
const { out, peak } = await pMap([...Array(10).keys()], async n => { await sleep(50); return n * n; }, 3);
console.log('results', out.join(','));
console.log('peak concurrency', peak, '| elapsed ~', Math.round((Date.now() - t) / 50) * 50, 'ms (serial would be 500)');
})();
js-core/sb.js
function t(n,f){const s=process.hrtime.bigint();const r=f();const d=Number(process.hrtime.bigint()-s)/1e6;console.log(n.padEnd(28),d.toFixed(1)+' ms len='+r.length);}
const N=200000;
t('+= in loop', ()=>{let s='';for(let i=0;i<N;i++)s+='ab';return s;});
t('array push + join', ()=>{const a=[];for(let i=0;i<N;i++)a.push('ab');return a.join('');});
t('split/reverse/join x200', ()=>{const src='x'.repeat(5000);let r='';for(let i=0;i<200;i++)r=src.split('').reverse().join('');return r;});