Compile Ready

JavaScript · Interview Prep

Interview Hub

The questions, coding challenges, output puzzles, and cheat sheets that show up again and again — curated for Senior and Staff interviews.

Top Interview Questions

The theory questions asked most often, with model answers.

What is the difference between == and === in JavaScript?Easy

Short answer

=== performs strict equality: values must have the same type and value. == performs loose equality: when types differ, JavaScript tries to coerce one or both operands before comparing.

Interview-grade explanation

Prefer === by default because it is predictable. Loose equality creates surprising truths such as 0 == false, "" == false, and null == undefined. The common intentional exception is value == null, which checks for both null and undefined.

Object.is is related but slightly different: it treats NaN as equal to itself and distinguishes +0 from -0.

EqualityCoercionFundamentals
How are var, let, and const different?Easy

Key differences

Featurevarletconst
ScopeFunction/global scopedBlock scopedBlock scoped
HoistingInitialised to undefinedHoisted but in TDZHoisted but in TDZ
ReassignmentAllowedAllowedNot allowed
Redeclaration in same scopeAllowedNot allowedNot allowed

Use const by default, let when reassignment is needed, and avoid var in modern code. const does not make objects immutable; it only prevents rebinding the variable. const user = {} can still be mutated with user.name = "Ada".

let and const also avoid the classic loop-closure problem because each iteration gets a fresh block-scoped binding.

ScopeES6Declarations
What is the difference between null and undefined?Easy

undefined usually means a value has not been assigned: an unpassed parameter, a missing object property, or a declared variable without an initial value. null is an intentional empty value chosen by the programmer.

Important details:

  • typeof undefined is "undefined"; typeof null is historically "object".
  • null == undefined is true, but null === undefined is false.
  • Optional chaining and nullish coalescing (?., ??) treat both as nullish.

A strong answer explains API intent: use undefined for omitted values and null when absence is a meaningful state, such as "user intentionally cleared profile image".

TypesNullishFundamentals
What is a closure and why is it useful?Medium

A closure is created when a function remembers variables from its lexical scope even after that outer scope has finished executing. The closed-over variables live as long as some reachable function can still access them.

Closures are useful for:

  • Data privacy: expose methods while hiding internal state.
  • Function factories: create configured functions like makeAdder(5).
  • Async callbacks: callbacks remember the state from when they were created.
  • Memoization and decorators: retain a cache between calls.

A senior candidate also mentions the risk: closures can keep large objects alive, so long-lived listeners and caches must be cleaned up.

ClosuresScopeFunctions
How does this work in JavaScript, and how do call, apply, and bind differ?Medium

this is determined mostly by how a function is called, not where it is defined.

Practical rules:

  1. new Fn() creates a new object and binds this to it.
  2. obj.method() binds this to obj.
  3. fn.call(x, a) / fn.apply(x, [a]) explicitly bind this for one call.
  4. fn.bind(x) returns a new function permanently bound to x.
  5. Plain fn() uses undefined in strict mode, or the global object in sloppy mode.

Arrow functions do not have their own this; they capture this lexically from the surrounding scope. call passes arguments individually, apply passes an array-like list, and bind does not execute immediately.

thisFunctionsBinding
Explain prototypal inheritance and the prototype chain.Medium

JavaScript objects inherit from other objects through an internal [[Prototype]] link. When you read obj.x, the engine checks obj first, then walks up the prototype chain until it finds x or reaches null.

Constructor functions use their .prototype object as the prototype for instances created with new. ES6 class syntax is mostly a cleaner syntax over the same prototype-based model.

Important interview points:

  • Own properties shadow prototype properties.
  • Methods are usually placed on the prototype so all instances share one function.
  • Object.create(proto) creates an object with an explicit prototype.
  • Object.getPrototypeOf(obj) is preferred over obj.__proto__ for inspection.

Prototype lookup is dynamic, so changing a prototype method can affect existing instances.

PrototypeInheritanceObjects
What is hoisting and what is the temporal dead zone?Medium

Hoisting means declarations are processed before code execution within their scope. It does not literally move code; it is a consequence of the creation phase of an execution context.

var declarations are hoisted and initialised to undefined, so reading them before the declaration returns undefined. Function declarations are hoisted with their function value, so they can be called before they appear.

let and const are also hoisted, but they are not initialised until their declaration is evaluated. The period from entering the scope until initialisation is the temporal dead zone (TDZ). Accessing the binding during the TDZ throws ReferenceError.

HoistingTDZExecution Context
What is the difference between shallow copy and deep copy?Medium

A shallow copy copies the top-level container but keeps references to nested objects. For example, { ...user } creates a new outer object, but user.address and copy.address still point to the same object.

A deep copy recursively copies nested data so changes to the clone do not affect the original. In modern JavaScript, structuredClone(value) is the safest built-in for many serialisable values, including Dates, Maps, Sets, typed arrays, and cycles.

Common caveats:

  • Spread and Object.assign are shallow.
  • JSON.parse(JSON.stringify(obj)) drops functions, undefined, symbols, Date identity, Map, Set, BigInt, and cycles.
  • Deep cloning class instances, accessors, DOM nodes, or functions requires clear requirements.
ObjectsCopyingReferences
Compare promises with async/await, and Promise.all, allSettled, and race.Medium

A Promise represents a future value that is pending, fulfilled, or rejected. async/await is syntax built on promises: an async function always returns a promise, and await pauses that function until the awaited value settles while letting the event loop continue.

Combinators:

  • Promise.all([...]): resolves when all resolve, preserves input order, rejects fast on the first rejection.
  • Promise.allSettled([...]): waits for every promise and returns { status, value/reason } results; it does not reject because one input failed.
  • Promise.race([...]): settles as soon as the first input settles, whether fulfilled or rejected.
  • Promise.any([...]): fulfils on the first fulfilment, rejects with AggregateError only if all reject.

Use try/catch around await for local error handling. Start independent promises before awaiting if you want parallelism.

PromisesAsync AwaitConcurrency
What is the difference between debounce and throttle?Medium

Debounce waits until calls stop for a given delay, then runs once. It is ideal for "do this after the user pauses" tasks such as search suggestions, resize-final calculations, or autosave after typing stops.

Throttle guarantees at most one call per time window. It is ideal for continuous streams where you still need regular updates, such as scroll tracking, drag movement, or rate-limited analytics.

Interview distinction:

  • Debounce collapses a burst into the last call after quiet time.
  • Throttle samples a burst at a fixed maximum frequency.

Good implementations preserve this and arguments, expose cancellation, and document leading/trailing behavior.

PerformanceTimersFrontend
Explain the event loop, microtasks, and macrotasks.Hard

JavaScript runs synchronous code on a single call stack. The host environment provides async APIs and queues callbacks. The event loop repeatedly runs one task, lets the stack empty, drains the microtask queue, then may render, then moves to the next task.

Important ordering:

  1. Run the current synchronous script/task.
  2. Drain microtasks completely: promise .then, catch, finally, queueMicrotask, and await continuations.
  3. Run the next task/macrotask: setTimeout, setInterval, UI events, network callbacks, message events.

Because microtasks drain before timers, Promise.resolve().then(...) runs before setTimeout(..., 0). A long microtask chain can starve rendering and delay timers.

Event LoopMicrotasksAsync
Explain currying and partial application.Hard

Currying transforms a function of multiple arguments into a chain of unary functions: add(a, b, c) becomes curriedAdd(a)(b)(c). Each call captures one argument in a closure until enough arguments are collected.

Partial application fixes some arguments of a function and returns a new function waiting for the rest: partial(add, 2)(3, 4). It does not require one argument per call.

Why it matters:

  • Creates reusable specialised functions, like multiplyBy(10).
  • Enables functional composition and pipelines.
  • Helps dependency injection by pre-filling configuration.

A strong answer mentions trade-offs: curried APIs can improve composition but may reduce readability, and this handling must be intentional.

Functional ProgrammingClosuresCurrying

Coding Questions

Implement the utilities interviewers love.

Implement debounceMedium

Problem

Problem

Implement debounce(fn, wait, options) that returns a debounced version of fn. The debounced function should postpone execution until wait milliseconds have passed since the most recent call.

Requirements

  • Preserve the caller's this and latest arguments.
  • Support options.leading === true to run immediately on the first call in a burst.
  • Support options.trailing !== false so trailing execution is enabled by default.
  • Expose cancel() to drop a pending call and flush() to immediately run the pending trailing call.

Approach

Track one timer and the latest call context. Every invocation replaces the pending arguments and resets the timer. If leading is enabled and there is no active timer, invoke immediately. When the timer expires, run the latest saved call only if trailing execution is enabled and a call is still pending.

javascript
1function debounce(fn, wait, options) {
2 if (typeof fn !== 'function') {
3 throw new TypeError('fn must be a function');
4 }
5
6 options = options || {};
7 var leading = options.leading === true;
8 var trailing = options.trailing !== false;
9 var timerId = null;
10 var lastArgs;
11 var lastThis;
12 var result;
13
14 function invoke() {
15 var args = lastArgs;
16 var context = lastThis;
17 lastArgs = undefined;
18 lastThis = undefined;
19 result = fn.apply(context, args);
20 return result;
21 }
22
23 function startTimer() {
24 timerId = setTimeout(function () {
25 timerId = null;
26 if (trailing && lastArgs) {
27 invoke();
28 } else {
29 lastArgs = undefined;
30 lastThis = undefined;
31 }
32 }, wait);
33 }
34
35 function debounced() {
36 lastArgs = arguments;
37 lastThis = this;
38
39 var shouldInvokeLeading = leading && timerId === null;
40
41 if (timerId !== null) {
42 clearTimeout(timerId);
43 }
44 startTimer();
45
46 if (shouldInvokeLeading) {
47 return invoke();
48 }
49
50 return result;
51 }
52
53 debounced.cancel = function () {
54 if (timerId !== null) {
55 clearTimeout(timerId);
56 }
57 timerId = null;
58 lastArgs = undefined;
59 lastThis = undefined;
60 };
61
62 debounced.flush = function () {
63 if (timerId === null) {
64 return result;
65 }
66
67 clearTimeout(timerId);
68 timerId = null;
69
70 if (lastArgs && trailing) {
71 return invoke();
72 }
73
74 lastArgs = undefined;
75 lastThis = undefined;
76 return result;
77 };
78
79 return debounced;
80}
81
82// Example:
83// var save = debounce(function (value) { console.log('save ' + value); }, 300);
84// save('a');
85// save('ab');
86// save('abc');

Time: O(1) per call · Space: O(1)

Edge cases interviewers care about: preserving this, using the latest arguments, cancelling pending work when a component unmounts, and defining leading/trailing behavior precisely. In UI code, debounce is useful for search boxes and resize handlers, but it can make interfaces feel laggy if the wait is too high.

Implement throttleMedium

Problem

Problem

Implement throttle(fn, wait, options) that returns a throttled version of fn. The throttled function should execute at most once every wait milliseconds.

Requirements

  • Preserve this and the latest arguments.
  • Run on the leading edge by default.
  • Run one trailing call by default if calls happened during the blocked window.
  • Support options.leading === false, options.trailing === false, plus cancel().

Approach

Store the time of the last real invocation. On each call, compute how much time remains in the current window. If the window has expired, invoke immediately. Otherwise, if trailing execution is allowed and no timer is scheduled, schedule one invocation using the latest saved arguments.

javascript
1function throttle(fn, wait, options) {
2 if (typeof fn !== 'function') {
3 throw new TypeError('fn must be a function');
4 }
5
6 options = options || {};
7 var leading = options.leading !== false;
8 var trailing = options.trailing !== false;
9 var timerId = null;
10 var lastInvokeTime = 0;
11 var lastArgs;
12 var lastThis;
13 var result;
14
15 function invoke(time) {
16 lastInvokeTime = time;
17 var args = lastArgs;
18 var context = lastThis;
19 lastArgs = undefined;
20 lastThis = undefined;
21 result = fn.apply(context, args);
22 return result;
23 }
24
25 function remainingWait(time) {
26 return wait - (time - lastInvokeTime);
27 }
28
29 function timerExpired() {
30 timerId = null;
31
32 if (trailing && lastArgs) {
33 invoke(Date.now());
34 } else {
35 lastArgs = undefined;
36 lastThis = undefined;
37 }
38 }
39
40 function throttled() {
41 var now = Date.now();
42
43 if (lastInvokeTime === 0 && leading === false) {
44 lastInvokeTime = now;
45 }
46
47 lastArgs = arguments;
48 lastThis = this;
49
50 var remaining = remainingWait(now);
51
52 if (remaining <= 0 || remaining > wait) {
53 if (timerId !== null) {
54 clearTimeout(timerId);
55 timerId = null;
56 }
57 return invoke(now);
58 }
59
60 if (timerId === null && trailing) {
61 timerId = setTimeout(timerExpired, remaining);
62 }
63
64 return result;
65 }
66
67 throttled.cancel = function () {
68 if (timerId !== null) {
69 clearTimeout(timerId);
70 }
71 timerId = null;
72 lastInvokeTime = 0;
73 lastArgs = undefined;
74 lastThis = undefined;
75 };
76
77 return throttled;
78}
79
80// Example:
81// var onScroll = throttle(function (y) { console.log('scroll ' + y); }, 100);

Time: O(1) per call · Space: O(1)

Throttle is about regular sampling, not waiting for silence. Clarify whether the first call should run immediately and whether the final call should be delivered. In production, requestAnimationFrame can be better for visual updates because it aligns work with browser rendering.

Implement a deep cloneMedium

Problem

Problem

Implement deepClone(value) for common JavaScript data structures.

Requirements

  • Return primitives and functions as-is.
  • Clone arrays, plain objects, class instances, Date, RegExp, Map, Set, and ArrayBuffer.
  • Preserve circular references.
  • Preserve property descriptors and symbol keys for objects where practical.

Approach

Use recursion plus a WeakMap from original objects to their clones. Store the clone in the map before cloning children so cycles can point back to it. Handle built-ins with special constructors, then fall back to creating an object with the same prototype and copying own property descriptors.

javascript
1function deepClone(value, seen) {
2 if (value === null || typeof value !== 'object') {
3 return value;
4 }
5
6 if (typeof seen === 'undefined') {
7 seen = new WeakMap();
8 }
9
10 if (seen.has(value)) {
11 return seen.get(value);
12 }
13
14 if (value instanceof Date) {
15 return new Date(value.getTime());
16 }
17
18 if (value instanceof RegExp) {
19 var copiedRegExp = new RegExp(value.source, value.flags);
20 copiedRegExp.lastIndex = value.lastIndex;
21 return copiedRegExp;
22 }
23
24 if (value instanceof ArrayBuffer) {
25 return value.slice(0);
26 }
27
28 if (value instanceof Map) {
29 var copiedMap = new Map();
30 seen.set(value, copiedMap);
31 value.forEach(function (mapValue, mapKey) {
32 copiedMap.set(deepClone(mapKey, seen), deepClone(mapValue, seen));
33 });
34 return copiedMap;
35 }
36
37 if (value instanceof Set) {
38 var copiedSet = new Set();
39 seen.set(value, copiedSet);
40 value.forEach(function (setValue) {
41 copiedSet.add(deepClone(setValue, seen));
42 });
43 return copiedSet;
44 }
45
46 var clone = Array.isArray(value)
47 ? []
48 : Object.create(Object.getPrototypeOf(value));
49
50 seen.set(value, clone);
51
52 Reflect.ownKeys(value).forEach(function (key) {
53 var descriptor = Object.getOwnPropertyDescriptor(value, key);
54 if (!descriptor) {
55 return;
56 }
57
58 if (Object.prototype.hasOwnProperty.call(descriptor, 'value')) {
59 descriptor.value = deepClone(descriptor.value, seen);
60 }
61
62 Object.defineProperty(clone, key, descriptor);
63 });
64
65 return clone;
66}
67
68// Example:
69// var original = { name: 'Ada', meta: { active: true } };
70// original.self = original;
71// var copy = deepClone(original);
72// console.log(copy !== original);
73// console.log(copy.meta !== original.meta);
74// console.log(copy.self === copy);

Time: O(n), where n is the number of reachable entries/properties · Space: O(n) for recursion and the WeakMap

Deep cloning is requirement-sensitive. This implementation handles many interview cases, but production code may prefer structuredClone when supported. Discuss limitations: functions are shared, WeakMap/WeakSet cannot be enumerated, DOM nodes need DOM APIs, and cloning class instances may not preserve private fields.

Implement memoizeMedium

Problem

Problem

Implement memoize(fn, resolver) that caches results of expensive function calls.

Requirements

  • Without a resolver, cache by the full argument list using argument identity for objects.
  • With a resolver, use resolver(...args) as the cache key.
  • Preserve this when invoking the original function.
  • Expose clear() to empty the cache.

Approach

A single JSON.stringify(args) key is fragile because object key order, cycles, and reference identity can break it. Instead, use a tree of nested Map objects: each argument moves one level deeper. At the leaf, store the computed result under a private symbol.

javascript
1function memoize(fn, resolver) {
2 if (typeof fn !== 'function') {
3 throw new TypeError('fn must be a function');
4 }
5
6 var root = new Map();
7 var RESULT = Symbol('result');
8
9 function memoized() {
10 var keyParts;
11
12 if (typeof resolver === 'function') {
13 keyParts = [resolver.apply(this, arguments)];
14 } else {
15 keyParts = Array.prototype.slice.call(arguments);
16 }
17
18 var node = root;
19 for (var i = 0; i < keyParts.length; i += 1) {
20 var key = keyParts[i];
21 if (!node.has(key)) {
22 node.set(key, new Map());
23 }
24 node = node.get(key);
25 }
26
27 if (node.has(RESULT)) {
28 return node.get(RESULT);
29 }
30
31 var result = fn.apply(this, arguments);
32 node.set(RESULT, result);
33 return result;
34 }
35
36 memoized.clear = function () {
37 root.clear();
38 };
39
40 memoized.cache = root;
41 return memoized;
42}
43
44// Example:
45// var slowSquare = memoize(function (n) {
46// console.log('computing');
47// return n * n;
48// });
49// console.log(slowSquare(9));
50// console.log(slowSquare(9));

Time: O(k) per cache lookup, where k is the number of key parts, plus original function cost on a miss · Space: O(m * k) for m cached argument paths

Memoization is best for pure functions: same inputs should always produce the same output and no important side effects should be skipped. Discuss cache invalidation, memory growth, object identity vs structural equality, and whether rejected promises should be cached for async functions.

Machine Coding

Build larger features end to end.

Implement an EventEmitterMedium

Problem

Problem

Build an EventEmitter supporting on, off, emit, and once.

Requirements

  • on(eventName, listener) registers a listener and returns an unsubscribe function.
  • off(eventName, listener) removes matching listeners.
  • emit(eventName, ...args) calls listeners with the supplied arguments and returns true if any listener existed.
  • once(eventName, listener) registers a listener that runs at most once.
  • Listeners added during an emit should not run until the next emit.

Approach

Store a Map from event names to arrays of listener records. Use a snapshot copy during emit so mutation while emitting is predictable. For once, wrap the original listener, remove the wrapper before invoking it, and remember the original so off(event, original) still works.

javascript
1function EventEmitter() {
2 this.events = new Map();
3}
4
5EventEmitter.prototype._add = function (eventName, listener, original, once) {
6 if (typeof listener !== 'function') {
7 throw new TypeError('listener must be a function');
8 }
9
10 if (!this.events.has(eventName)) {
11 this.events.set(eventName, []);
12 }
13
14 var record = {
15 listener: listener,
16 original: original || listener,
17 once: once === true
18 };
19
20 this.events.get(eventName).push(record);
21
22 var self = this;
23 return function unsubscribe() {
24 self.off(eventName, listener);
25 };
26};
27
28EventEmitter.prototype.on = function (eventName, listener) {
29 return this._add(eventName, listener, listener, false);
30};
31
32EventEmitter.prototype.once = function (eventName, listener) {
33 var self = this;
34
35 function wrapped() {
36 self.off(eventName, wrapped);
37 return listener.apply(this, arguments);
38 }
39
40 return this._add(eventName, wrapped, listener, true);
41};
42
43EventEmitter.prototype.off = function (eventName, listener) {
44 var list = this.events.get(eventName);
45 if (!list) {
46 return this;
47 }
48
49 var filtered = list.filter(function (record) {
50 return record.listener !== listener && record.original !== listener;
51 });
52
53 if (filtered.length === 0) {
54 this.events.delete(eventName);
55 } else {
56 this.events.set(eventName, filtered);
57 }
58
59 return this;
60};
61
62EventEmitter.prototype.emit = function (eventName) {
63 var list = this.events.get(eventName);
64 if (!list || list.length === 0) {
65 return false;
66 }
67
68 var args = Array.prototype.slice.call(arguments, 1);
69 var snapshot = list.slice();
70
71 for (var i = 0; i < snapshot.length; i += 1) {
72 var record = snapshot[i];
73 if (record.once) {
74 this.off(eventName, record.listener);
75 }
76 record.listener.apply(this, args);
77 }
78
79 return true;
80};
81
82EventEmitter.prototype.listenerCount = function (eventName) {
83 var list = this.events.get(eventName);
84 return list ? list.length : 0;
85};
86
87// Example:
88// var bus = new EventEmitter();
89// var unsubscribe = bus.on('message', function (text) { console.log(text); });
90// bus.emit('message', 'hello');
91// unsubscribe();

Time: `on`: O(1), `emit`: O(n), `off`: O(n) for n listeners on the event · Space: O(n) listeners plus O(n) snapshot during emit

Production emitters may support wildcard events, max-listener warnings, async listeners, error channels, and listener priority. Be explicit about mutation semantics during emit; snapshot semantics are easy to reason about and prevent newly added listeners from firing in the same cycle.

Implement Promise.all from scratchHard

Problem

Problem

Implement promiseAll(iterable) with behavior similar to Promise.all.

Requirements

  • Accept any iterable of values or promises.
  • Resolve to an array of fulfilled values in the original input order.
  • Resolve immediately with [] for an empty iterable.
  • Reject as soon as any input rejects.
  • Treat non-promise values as already fulfilled values.

Approach

Convert the iterable to an array so indexes are stable. Create a result array of the same length and a remaining counter. Wrap each item with Promise.resolve to assimilate values and thenables. Store each fulfillment at its original index; when the counter reaches zero, resolve. Use the outer promise's reject directly for fast rejection.

javascript
1function promiseAll(iterable) {
2 return new Promise(function (resolve, reject) {
3 var items;
4
5 try {
6 items = Array.from(iterable);
7 } catch (error) {
8 reject(error);
9 return;
10 }
11
12 var results = new Array(items.length);
13 var remaining = items.length;
14
15 if (remaining === 0) {
16 resolve([]);
17 return;
18 }
19
20 items.forEach(function (item, index) {
21 Promise.resolve(item).then(
22 function (value) {
23 results[index] = value;
24 remaining -= 1;
25
26 if (remaining === 0) {
27 resolve(results);
28 }
29 },
30 function (reason) {
31 reject(reason);
32 }
33 );
34 });
35 });
36}
37
38// Example:
39// promiseAll([Promise.resolve(1), 2, Promise.resolve(3)])
40// .then(function (values) { console.log(values); });

Time: O(n) setup plus the time for input promises to settle · Space: O(n) for the copied items and result array

The subtle requirement is preserving input order even when promises settle out of order. Real ECMAScript Promise.all has additional specification details, such as iterator closing and species constructors, but this is the core behavior interviewers expect.

Implement an LRU CacheHard

Problem

Problem

Implement an LRUCache with fixed capacity. It should evict the least recently used key when inserting beyond capacity.

Requirements

  • get(key) returns the value and marks the key as recently used, or returns undefined if missing.
  • set(key, value) inserts or updates and marks the key as recently used.
  • has(key), delete(key), clear(), size, and keys() are useful extras.
  • Average-case get and set should be O(1).

Approach

In JavaScript, Map preserves insertion order. Treat the oldest map entry as least recently used and the newest as most recently used. On get or update, delete and reinsert the key to move it to the back. When size exceeds capacity, delete map.keys().next().value.

javascript
1class LRUCache {
2 constructor(capacity) {
3 if (!Number.isInteger(capacity) || capacity < 1) {
4 throw new RangeError('capacity must be a positive integer');
5 }
6
7 this.capacity = capacity;
8 this.map = new Map();
9 }
10
11 get size() {
12 return this.map.size;
13 }
14
15 get(key) {
16 if (!this.map.has(key)) {
17 return undefined;
18 }
19
20 var value = this.map.get(key);
21 this.map.delete(key);
22 this.map.set(key, value);
23 return value;
24 }
25
26 set(key, value) {
27 if (this.map.has(key)) {
28 this.map.delete(key);
29 }
30
31 this.map.set(key, value);
32
33 if (this.map.size > this.capacity) {
34 var oldestKey = this.map.keys().next().value;
35 this.map.delete(oldestKey);
36 }
37
38 return this;
39 }
40
41 has(key) {
42 return this.map.has(key);
43 }
44
45 delete(key) {
46 return this.map.delete(key);
47 }
48
49 clear() {
50 this.map.clear();
51 }
52
53 keys() {
54 return Array.from(this.map.keys());
55 }
56}
57
58// Example:
59// var cache = new LRUCache(2);
60// cache.set('a', 1).set('b', 2);
61// cache.get('a');
62// cache.set('c', 3);
63// console.log(cache.has('b'));
64// console.log(cache.keys());

Time: O(1) average for get, set, has, and delete · Space: O(capacity)

A Map-based LRU is concise and interview-friendly. In languages without ordered maps, use a hash map plus a doubly linked list. Discuss whether has should refresh recency; this implementation does not, while get and set do.

Output Prediction Puzzles

Trace the output before you reveal it.

Predict the output #1

javascript
1console.log('A');
2setTimeout(function () {
3 console.log('B');
4}, 0);
5Promise.resolve().then(function () {
6 console.log('C');
7});
8console.log('D');

Predict the output #2

javascript
1for (var i = 0; i < 3; i += 1) {
2 setTimeout(function () {
3 console.log('var ' + i);
4 }, 0);
5}
6
7for (let j = 0; j < 3; j += 1) {
8 setTimeout(function () {
9 console.log('let ' + j);
10 }, 0);
11}

Predict the output #3

javascript
1console.log(typeof null);
2console.log(typeof NaN);
3console.log(typeof undefined);
4console.log(Number.isNaN(NaN));

Predict the output #4

javascript
1console.log(0.1 + 0.2);
2console.log(0.1 + 0.2 === 0.3);
3console.log(Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON);

Predict the output #5

javascript
1console.log('x' + ([] + []) + 'y');
2console.log([] + {});
3console.log([1, 2] + [3, 4]);

Predict the output #6

javascript
1console.log(a);
2var a = 10;
3console.log(b);
4let b = 20;

Predict the output #7

javascript
1async function run() {
2 console.log('async start');
3 await Promise.resolve();
4 console.log('async end');
5}
6
7console.log('script start');
8run();
9Promise.resolve().then(function () {
10 console.log('promise then');
11});
12console.log('script end');

Predict the output #8

javascript
1var a = {};
2var b = {};
3var store = {};
4
5store[a] = 'first';
6store[b] = 'second';
7
8console.log(a === b);
9console.log(store[a]);
10console.log(Object.keys(store)[0]);

Cheat Sheets

One-page revision for the night before.

Equality & coercion

Equality

OperatorMeaningUse
===Strict equality, no type coercionDefault choice
==Loose equality with coercionRare; sometimes value == null
Object.isSameValue comparisonNaN, +0 vs -0 edge cases

Falsy values

Only these are falsy: false, 0, -0, 0n, "", null, undefined, NaN. Everything else, including [], {}, and "0", is truthy.

Coercion reminders

  • + with a string performs string concatenation.
  • Number([]) === 0, Number([1]) === 1, Number([1,2]) is NaN.
  • null == undefined is true; both are nullish for ?? and ?..

Scope & closures

Scope rules

  • var is function scoped and initialised to undefined.
  • let and const are block scoped and have a temporal dead zone.
  • Function declarations are hoisted with their function value.
  • Modules run in strict mode and have their own top-level scope.

Closure mental model

A closure is a function plus references to variables from its lexical environment. It is created naturally whenever an inner function outlives or is passed away from its outer scope.

Common uses

  • Private state
  • Function factories
  • Callbacks that remember context
  • Memoization and decorators

Watch out

Long-lived closures can keep large objects alive. Remove event listeners and clear caches when they are no longer needed.

The event loop

Ordering model

  1. Run the current synchronous task until the call stack is empty.
  2. Drain the microtask queue completely.
  3. Render if the browser chooses to.
  4. Run the next task/macrotask.

Microtasks

  • Promise .then, .catch, .finally
  • await continuations
  • queueMicrotask

Tasks / macrotasks

  • setTimeout, setInterval
  • DOM events
  • Network callbacks
  • Message channel callbacks

Interview line

Promise.resolve().then(...) runs before setTimeout(..., 0) because microtasks drain before the next task. Too many microtasks can starve timers and rendering.

`this` binding rules

Binding priority

  1. new Fn()this is the new object.
  2. fn.call(x) / fn.apply(x) / fn.bind(x) → explicit binding.
  3. obj.method()this is obj.
  4. Plain fn()undefined in strict mode, global object in sloppy mode.

Arrow functions

Arrow functions do not bind their own this; they capture it lexically from the surrounding scope. They are great for callbacks but poor as object prototype methods when dynamic this is needed.

call vs apply vs bind

  • call(thisArg, a, b) invokes now with listed args.
  • apply(thisArg, [a, b]) invokes now with array-like args.
  • bind(thisArg, a) returns a new function for later.

Array/Object methods worth memorising

Array methods

MethodReturnsTypical use
mapNew arrayTransform each item
filterNew arrayKeep matching items
reduceAny valueAccumulate/group/sum
findFirst item or undefinedLocate one item
someBooleanAny item matches
everyBooleanAll items match
flatMapNew flattened arrayMap then flatten one level

Object methods

  • Object.keys(obj) → own enumerable string keys.
  • Object.values(obj) → own enumerable values.
  • Object.entries(obj)[key, value] pairs, great with Map.
  • Object.assign(target, source) and spread make shallow copies.
  • Object.create(proto) creates an object with a chosen prototype.

Async patterns

Promise combinators

APISettles whenRejects when
Promise.allAll fulfilFirst rejection
Promise.allSettledAll settleNever due to one failed input
Promise.raceFirst settlesFirst settled value is rejection
Promise.anyFirst fulfilsAll reject (AggregateError)

async/await

  • async functions always return promises.
  • await pauses only the current async function, not the whole thread.
  • Use try/catch for awaited errors.
  • Start independent work before awaiting to keep concurrency.

Patterns

  • Sequential: for...of with await.
  • Parallel fail-fast: await Promise.all(tasks).
  • Parallel collect-all: await Promise.allSettled(tasks).
  • Timeout: Promise.race([work, timeoutPromise]).