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.
How are var, let, and const different?Easy
Key differences
| Feature | var | let | const |
|---|---|---|---|
| Scope | Function/global scoped | Block scoped | Block scoped |
| Hoisting | Initialised to undefined | Hoisted but in TDZ | Hoisted but in TDZ |
| Reassignment | Allowed | Allowed | Not allowed |
| Redeclaration in same scope | Allowed | Not allowed | Not 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.
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 undefinedis"undefined";typeof nullis historically"object".null == undefinedistrue, butnull === undefinedisfalse.- 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".
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.
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:
new Fn()creates a new object and bindsthisto it.obj.method()bindsthistoobj.fn.call(x, a)/fn.apply(x, [a])explicitly bindthisfor one call.fn.bind(x)returns a new function permanently bound tox.- Plain
fn()usesundefinedin 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.
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 overobj.__proto__for inspection.
Prototype lookup is dynamic, so changing a prototype method can affect existing instances.
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.
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.assignare shallow. JSON.parse(JSON.stringify(obj))drops functions,undefined, symbols,Dateidentity,Map,Set,BigInt, and cycles.- Deep cloning class instances, accessors, DOM nodes, or functions requires clear requirements.
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 withAggregateErroronly if all reject.
Use try/catch around await for local error handling. Start independent promises before awaiting if you want parallelism.
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.
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:
- Run the current synchronous script/task.
- Drain microtasks completely: promise
.then,catch,finally,queueMicrotask, andawaitcontinuations. - 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.
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.
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
thisand latest arguments. - Support
options.leading === trueto run immediately on the first call in a burst. - Support
options.trailing !== falseso trailing execution is enabled by default. - Expose
cancel()to drop a pending call andflush()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.
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
thisand 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, pluscancel().
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.
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, andArrayBuffer. - 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.
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
thiswhen 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.
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 returnstrueif any listener existed.once(eventName, listener)registers a listener that runs at most once.- Listeners added during an
emitshould not run until the nextemit.
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.
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 === true18 };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.
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 returnsundefinedif missing.set(key, value)inserts or updates and marks the key as recently used.has(key),delete(key),clear(),size, andkeys()are useful extras.- Average-case
getandsetshould 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.
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
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
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
1console.log(typeof null);2console.log(typeof NaN);3console.log(typeof undefined);4console.log(Number.isNaN(NaN));Predict the output #4
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
1console.log('x' + ([] + []) + 'y');2console.log([] + {});3console.log([1, 2] + [3, 4]);Predict the output #6
1console.log(a);2var a = 10;3console.log(b);4let b = 20;Predict the output #7
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
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
| Operator | Meaning | Use |
|---|---|---|
=== | Strict equality, no type coercion | Default choice |
== | Loose equality with coercion | Rare; sometimes value == null |
Object.is | SameValue comparison | NaN, +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])isNaN.null == undefinedistrue; both are nullish for??and?..
Scope & closures
Scope rules
varis function scoped and initialised toundefined.letandconstare 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
- Run the current synchronous task until the call stack is empty.
- Drain the microtask queue completely.
- Render if the browser chooses to.
- Run the next task/macrotask.
Microtasks
- Promise
.then,.catch,.finally awaitcontinuationsqueueMicrotask
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
new Fn()→thisis the new object.fn.call(x)/fn.apply(x)/fn.bind(x)→ explicit binding.obj.method()→thisisobj.- Plain
fn()→undefinedin 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
| Method | Returns | Typical use |
|---|---|---|
map | New array | Transform each item |
filter | New array | Keep matching items |
reduce | Any value | Accumulate/group/sum |
find | First item or undefined | Locate one item |
some | Boolean | Any item matches |
every | Boolean | All items match |
flatMap | New flattened array | Map 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 withMap.Object.assign(target, source)and spread make shallow copies.Object.create(proto)creates an object with a chosen prototype.
Async patterns
Promise combinators
| API | Settles when | Rejects when |
|---|---|---|
Promise.all | All fulfil | First rejection |
Promise.allSettled | All settle | Never due to one failed input |
Promise.race | First settles | First settled value is rejection |
Promise.any | First fulfils | All reject (AggregateError) |
async/await
asyncfunctions always return promises.awaitpauses only the current async function, not the whole thread.- Use
try/catchfor awaited errors. - Start independent work before awaiting to keep concurrency.
Patterns
- Sequential:
for...ofwithawait. - Parallel fail-fast:
await Promise.all(tasks). - Parallel collect-all:
await Promise.allSettled(tasks). - Timeout:
Promise.race([work, timeoutPromise]).