Keentune
Adaptive skill practice — find your level, get sharper.
JavaScript practice
The language fundamentals every interview returns to
Master JavaScript itself — closures and scope, hoisting and the temporal dead zone, the event loop with microtasks and promises, this-binding and prototypes, coercion, and the classic traps (0.1 + 0.2, map(parseInt), the var-in-a-loop bug). Real "what does this log, and in what order?" questions with explanations that build genuine fluency for senior-engineer interviews. Part of the Developer track.
• Meets you at your level — never too easy, never too hard.
• Adapts every question to how you’re doing.
• Tracks your level over time. Skill practice, not a test.
What you’ll master
Follow the curriculum from foundations through advanced practice. Each line is a concept the adaptive question bank can teach and test.
A. Grammar, declarations and types
var is function-scoped; let and const are block-scoped
a let binding exists from block entry but throws until its declaration runs
const forbids reassignment, not mutation of the referenced object
a function declaration hoists with its body; var hoists initialized to undefined
absent by default versus deliberately empty, and why typeof null is "object"
seven primitive types, everything else a reference; primitives are boxed on property access
the fixed list typeof can return, including "function" and the null bug
a newline after return ends the statement and returns undefined
what "use strict" changes: silent write failures throw, this is no longer boxed
sloppy-mode assignment to an undeclared name creates a global; globalThis names it portably
B. Operators, coercion and equality
== runs the abstract-equality coercion steps; === compares type and value directly
null == undefined is true and null == 0 is not; the pair is equal only to each other
Object.is distinguishes +0 from -0 and reports NaN as equal to itself
==, ===, SameValue and SameValueZero, and which built-in reaches for which
+ concatenates if either operand becomes a string; every other arithmetic operator coerces to number
Symbol.toPrimitive first, then valueOf/toString in an order set by the hint
exactly false, 0, -0, 0n, "", null, undefined, NaN; "0" and [] are truthy
&& and || evaluate to one of their operands, not to a boolean
?? falls through only on null or undefined, so 0 and "" survive
&&=, ||= and ??= short-circuit the assignment itself, skipping the setter
?. abandons the rest of the chain and yields undefined rather than throwing
** groups right to left and rejects an unparenthesized unary minus on its left
a ?? b || c will not parse; the grammar demands parentheses
operands are truncated to int32, so >>> and >> disagree on negatives and | 0 floors
< and > coerce each operand to a primitive and then a number, so 3 > 2 > 1 evaluates true > 1 and is false
the comma operator evaluates every expression left to right and yields the LAST one, which is why (1, 2, 3) is 3
C. Control flow, errors and cleanup
anything can be thrown, which is why a caught value has no guaranteed shape
TypeError, RangeError, ReferenceError and SyntaxError each signal a different failure class
extends Error, setting name, and keeping the stack and message intact
the { cause } option preserves the original failure instead of flattening it (ES2022)
including on return and on rethrow, and a return inside finally discards the pending result
catch {} when the error value is genuinely unused
Error.isError identifies errors across realms where instanceof fails (ES2026)
a case without break continues into the next, and switch matches with ===
using disposes at scope exit via Symbol.dispose, LIFO, even when the block throws (stage 4)
D. Loops and iteration statements
enumerable string-keyed properties, inherited ones included
requires an iterable and never sees prototype properties
indices arrive as strings, which is why for...in is the wrong array loop
let in a for head creates a fresh binding each pass; var creates one for all of them
a label is how you leave a nested loop without a flag variable
forEach ignores return and has no early exit; some/every/for...of do
consumes an async iterable and is sequential by construction
the condition is tested after the body, so the body always executes at least once
E. Functions and parameters
hoisting, the named-function-expression scope, and inferred names
evaluated at call time, left to right, and only when the argument is undefined
...args is a real array; arguments is array-like and absent in arrow functions
spreading any iterable into an argument list, and the array and object forms
object patterns, inner defaults, and why a missing argument throws without = {}
fn.length counts parameters before the first default or rest parameter
no own this, arguments, super or new.target, and not constructible
an immediately invoked expression as the pre-module isolation trick
engines do not implement tail calls, so deep recursion is a RangeError
the reference is copied, so reassigning a parameter never affects the caller
F. Binding, `this` and closures
this is resolved when the function is called, not where it was written
passing obj.m as a callback strips the receiver
an explicit receiver, a spread argument list, and a permanently bound copy
an arrow closes over the enclosing this and cannot be rebound
new allocates, links the prototype and returns the object unless the body returns another object
an undecorated call gets undefined in strict mode and the global object otherwise
a closure sees the variable's later value, not a snapshot taken at creation
var leaves every callback in a loop sharing one variable
data no caller can reach, paid for with per-instance memory
fixing some arguments now and taking the rest later
G. Objects and properties
any other key is coerced to a string, so obj[1] and obj["1"] are one property
{ [expr]: v } builds the key at runtime; { x } is { x: x }
value, writable, enumerable, configurable, and how defineProperty defaults them to false
accessor properties run code on read or write, and are invisible at the call site
Object.hasOwn ignores the prototype chain; the in operator walks it
Object.freeze protects one level, so nested objects remain mutable
which of adding, deleting and writing each one still permits
both copy only own enumerable properties, and copy references not values
Object.assign triggers an inherited setter; spread creates an own data property
integer-like keys ascend first, then string keys in insertion order, then symbols
round-tripping an object through an array of pairs to filter or transform it
removes the property entirely, unlike setting it to undefined, and leaves an array hole
Object.assign applies its sources left to right, so a later source overwrites an earlier one key by key
H. Prototypes and inheritance
a missing property walks [[Prototype]] links until it reaches null
an instance's link versus the object a constructor hands to its instances
constructing an object with a chosen prototype, including no prototype at all
writing an inherited property creates an own property instead of updating the parent
if the prototype defines a setter, the write runs it and no own property appears
it tests the chain, not the constructor, and breaks across realms
inherited and writable, so it lies after you replace prototype wholesale
a safe dictionary with no inherited methods, and what stops working on it
one function object on the prototype serves every instance
I. Classes
methods land on prototype, but the body is always strict and the binding is not usable before evaluation
a derived constructor must call super() before it may touch this
instance fields are created per instance, in declaration order, before the constructor body
a class field holding an arrow is per-instance and survives detachment
#x is a hard-private slot enforced by the engine, and #x in obj is the brand check
statics live on the constructor; a static block runs once when the class is defined
get/set install accessor properties on the prototype, not on the instance
subclassing Array or Error, and how Symbol.species decides what methods return
distinguishing construction from a plain call inside the same function
super.m() resolves against the home object while this stays the current instance
a function returning class extends Base to compose behavior without multiple inheritance
J. Numbers and arithmetic
every Number is a 64-bit binary float, which is why 0.1 + 0.2 !== 0.3
beyond Number.MAX_SAFE_INTEGER integer arithmetic silently returns the wrong answer
the only value failing x === x; test it with Number.isNaN
global isNaN("foo") is true because it converts first; Number.isNaN does not
prefix parsing with an explicit radix versus whole-string conversion that rejects trailing junk
and it rounds the stored binary value, so (1.005).toFixed(2) is not "1.01"
where -0 is observable and where === hides it
-7 % 3 is -1; a true modulo needs ((n % m) + m) % m
Math.round breaks ties toward +Infinity, which differs from trunc, floor and ceil on negatives
arbitrary-precision integers throw when combined with a Number in arithmetic
K. Strings and text
no method mutates in place; every operation returns a new string
length counts code units, so an emoji or an astral character counts as two
spread and for...of walk code points; bracket indexing splits surrogate pairs
NFC versus NFD, and why two identical-looking strings can be unequal
slice accepts negative indices; substring clamps and silently swaps its arguments
a string pattern replaces only the first match unless the regex carries /g
$1, $& and $$ in the string form, and the argument list of the function form
interpolation, embedded expressions, and literal newlines
a function receiving the literal chunks and the interpolated values separately, plus .raw
< compares code units; localeCompare applies collation rules
the argument is the final total length, and a longer string is returned unchanged
L. Arrays and indexed collections
assigning a smaller length truncates; writing past the end extends with holes
iteration methods skip holes while for loops and spread see undefined
[10, 9].sort() stays [10, 9] because elements are compared as strings
it sorts in place, returns the same array, and preserves the order of ties
splice/reverse/sort versus toSpliced/toReversed/toSorted/with (ES2023)
one output per input, a filtered subset, and a fold over an accumulator
without one the first element seeds the accumulator, and an empty array throws
the first match, every match, or a position
indexOf uses strict equality and cannot find NaN; includes uses SameValueZero
depth-limited flattening, and mapping then flattening one level in a single pass
building from an iterable or array-like with an optional mapper, versus from a literal argument list
["1","2","3"].map(parseInt) passes the index as the radix
M. Keyed collections and weak references
a Map takes any value as a key, guarantees insertion order, and has no prototype keys to collide with
two structurally identical objects are two distinct keys
adding an existing member is a no-op; 0 and -0 collapse and so do two NaNs
union, intersection, difference, symmetricDifference and the subset predicates (ES2025)
Map.size is a property; counting keys of a plain object allocates an array first
the entry disappears when the key becomes unreachable, and the contents are not enumerable
deliberately non-deterministic, so never a cleanup guarantee
Map.getOrInsert and getOrInsertComputed replace the has-get-set dance (ES2026)
N. Iterators and generators
an object is iterable when Symbol.iterator returns an iterator; that is all for...of requires
next() returns { value, done }, and a spent iterator stays done
function* and yield suspend and resume with local state intact
yield* forwards to another iterable and evaluates to that generator's return value
the argument passed to next() becomes the value of the paused yield
break invokes the iterator's return(), which is where a generator's finally runs
producing values on demand instead of materializing a whole collection
map, filter, take, drop and toArray chained lazily on any iterator (ES2025)
Symbol.asyncIterator yields promises, and for await unwraps each one
O. Promises and async/await
pending, fulfilled or rejected; a promise settles once and never changes again
returning a promise from a handler flattens it into the chain
the new Promise body executes immediately; only the reactions are deferred
a catch placed before a then cannot handle that then's rejection
a promise created inside a handler but not returned is not awaited by the chain
reject on the first failure versus collect every outcome
first to settle, versus first to fulfill with an AggregateError when all reject
always, and a synchronous throw inside it becomes a rejection
start every request first and await the array to run them concurrently
attaching a handler in a later task is too late for the runtime's warning
Promise.try wraps a possibly-synchronous throw; withResolvers hoists resolve and reject out (ES2024/ES2025)
a promise cannot be cancelled; you signal the producer, typically with an AbortSignal
await adopts any object with a callable then, including one that never settles
P. The concurrency model and the event loop
a task finishes before any other task starts, so nothing interleaves mid-function
the microtask queue drains completely between two tasks
which is why a resolved then runs before a zero-delay timer
it queues a task and is clamped, so ordering with promises is fixed, not racy
one long synchronous computation delays every timer, promise and input handler
the code after an await is scheduled, so it never runs in the same synchronous pass
a microtask that queues another microtask can prevent the task from ever ending
debounce fires once after calls STOP arriving; throttle fires at most once per interval while they keep arriving
Q. Modules
renaming with as, and why a default import's local name is arbitrary
bindings exist before the body runs, and the specifier must be a literal
an imported binding reflects a later reassignment in the exporting module
assigning to an imported binding is a TypeError, even for a mutable object's variable
top-level declarations stay local, and module code is always strict
the registry caches the instance, so import side effects run exactly once per specifier
import() returns a promise, works in any scope, and is how code splitting happens
export * from versus explicit re-export, and how name collisions are resolved
a cycle can observe a binding before its initializer has run
with { type: "json" } declares the expected module type (ES2025)
R. Dates and times
a Date is a single instant stored as milliseconds since the 1970 UTC epoch
while days of the month are not
"2026-01-01" parses as UTC but "2026-01-01T00:00" parses as local time
setters modify the instance, so a shared Date is an aliasing bug waiting to happen
setting day 32 rolls forward into the next month rather than throwing
getHours reads the host time zone; getUTCHours does not, and DST breaks naive day arithmetic
a failed parse yields a Date whose time value is NaN, which compares false against everything
why Temporal exists: immutable values, explicit time zones and calendar-aware arithmetic (stage 4)
S. Regular expressions
a RegExp built from a string needs every backslash doubled
a /g regex carries lastIndex, so reusing it across calls skips matches
a boolean, a single match with its groups, or a lazy iterator over every match
with /g, match returns bare strings and the capture groups are gone
(?<name>…) and reading results from groups instead of by index
.* versus .*?, and where catastrophic backtracking comes from
/u matches whole code points and enables \p{…}; /v adds set operations
RegExp.escape neutralizes untrusted text before it becomes a pattern (ES2025)
(?i:…) and (?-i:…) apply a flag to one group only, leaving the rest of the pattern under the pattern-level flags
T. JSON and serialization
omitted from objects, but serialized as null inside arrays
an object can define its own serialized form, which is exactly how Date becomes a string
a key whitelist versus a per-key transform invoked top-down
the parse hook sees innermost values first, which is how types are restored
JSON.stringify raises a TypeError rather than truncating
Map, Set, BigInt, undefined and prototypes do not survive the trip
U. Symbols and metaprogramming
no two symbols are equal, and symbol keys are skipped by Object.keys and JSON.stringify
Symbol.for returns a shared registry symbol; Symbol() never repeats
Symbol.toStringTag changes what Object.prototype.toString.call reports about a value
get, set, has and deleteProperty intercept the operation itself, not a copied value
a trap that contradicts a non-configurable, non-writable property throws a TypeError
Reflect supplies the correct default behavior for each trap, with a boolean return
forwarding receiver through Reflect.get is what keeps an inherited getter's this correct
handing out access you can withdraw later
new Function compiles in global scope, both cost optimization, and both are injection surface
V. Typed arrays and binary data
the buffer is raw bytes; all reads and writes go through a typed array or DataView
two views over one buffer observe each other's writes immediately
Uint8Array wraps modulo 256 while Uint8ClampedArray saturates at the bounds
DataView takes an explicit byte-order argument that typed arrays do not
no push, and an out-of-range index write is silently discarded
Uint8Array.fromBase64 and toHex replace the old string round trip (ES2026)
Float16Array trades range and precision for size, so not every value round-trips (ES2025)
W. Internationalization
grouping separators, currency and unit display differ by locale
rendering one instant in a named IANA zone without mutating the date
locale-correct ordering, with options for case and accent sensitivity
"3 days ago" and correct plural categories without hand-written rules
the requested locale list, the fallback chain, and reading resolvedOptions
X. Language evolution and version gates
stage 0 through 4, and why only stage 4 may be taught as the language
backward compatibility is absolute, which is why the historical mistakes are permanent
test for the method or syntax, never for a runtime version number
syntax is compiled down; a missing built-in has to be added at runtime
with, substr and escape are specified for compatibility and still must not be used
setTimeout, console and module loading come from the host, not from ECMAScript
Keentune is not affiliated with or endorsed by the organizations whose documentation informs these maps.
Explore all skills on Keentune
All exam, test, and product names and trademarks are the property of their respective owners and are used here for identification and reference only. Keentune is independent study practice — not affiliated with, authorized, or endorsed by any of these organizations.
© 2026 SportaApp LLC