Complete mock JavaScript technical interview with 10 progressively difficult questions, ideal answers, follow-up questions, and scoring criteria. Practice under realistic conditions.
How to Use This Mock Interview
This simulates a 60-minute JavaScript technical interview with 10 questions of increasing difficulty. Set a timer and try to answer each question before looking at the solution.
Scoring yourself:
- ⭐ Correct answer with explanation = Full marks
- 🔶 Partial answer or missing edge cases = Half marks
- ❌ Incorrect or no answer = Review this topic
Time allocation:
- Questions 1-4 (Conceptual): 2 minutes each
- Questions 5-7 (Short Coding): 5 minutes each
- Questions 8-10 (Problem Solving): 10 minutes each
Question 2: Closures
"What does this function return and why?"
function createCounter() {
let count = 0;
return {
increment: () => ++count,
decrement: () => --count,
getCount: () => count
};
}
const counter = createCounter();
counter.increment();
counter.increment();
counter.increment();
counter.decrement();
console.log(counter.getCount()); // ?
console.log(counter.count); // ?
<details> <summary>Answer</summary>
Explanation:
createCounter returns an object with three methods that close over the same count variable.- After 3 increments and 1 decrement: count = 3 - 1 = 2.
counter.count is undefined because count is not a property of the returned object — it's a closed-over variable in the function's scope. This demonstrates data privacy through closures.
</details>
Question 3: Event Loop
"What is the output order?"
console.log('A');
setTimeout(() => console.log('B'), 0);
Promise.resolve()
.then(() => console.log('C'))
.then(() => console.log('D'));
queueMicrotask(() => console.log('E'));
console.log('F');
<details> <summary>Answer</summary>
Explanation:
- Synchronous:
A and F execute immediately. - Microtask queue (processed before any macrotask):
- First
.then callback: C queueMicrotask: E- Second
.then callback: D (queued after C resolves)
- Macrotask queue:
B (setTimeout, even with 0ms delay)
Note: C prints before E because Promise .then callbacks are queued in order of registration, and queueMicrotask was registered after the first .then. </details>
Question 4: Prototypes and this
"What will each log statement print?"
const obj = {
name: 'Quiz',
getName: function() { return this.name; },
getNameArrow: () => this.name
};
console.log(obj.getName()); // ?
const fn = obj.getName;
console.log(fn()); // ?
console.log(obj.getNameArrow()); // ?
<details> <summary>Answer</summary>
'Quiz'
undefined (or '' in browser global scope)
undefined (or '' in browser global scope)
Explanation:
obj.getName() — implicit binding: this is obj, so returns 'Quiz'.fn() — default binding: this is undefined (strict mode) or window (sloppy mode). window.name is typically ''.obj.getNameArrow() — arrow functions don't have their own this. They inherit from the enclosing lexical scope (module/global scope), where this.name is undefined.
</details>
Round 2: Short Coding (15 minutes)
Question 5: Implement Array.prototype.myMap
"Implement a custom map method without using the built-in .map()."
// Your implementation here
Array.prototype.myMap = function(callback) {
// ...
};
// Should work like:
[1, 2, 3].myMap(x => x * 2); // [2, 4, 6]
<details> <summary>Solution</summary>
Array.prototype.myMap = function(callback, thisArg) {
if (typeof callback !== 'function') {
throw new TypeError(callback + ' is not a function');
}
const result = [];
for (let i = 0; i < this.length; i++) {
// Check if index exists (handle sparse arrays)
if (i in this) {
result[i] = callback.call(thisArg, this[i], i, this);
}
}
return result;
};
// Testing:
console.log([1, 2, 3].myMap(x => x * 2)); // [2, 4, 6]
console.log([1, 2, 3].myMap((x, i) => x + i)); // [1, 3, 5]
console.log([, , 3].myMap(x => x * 2)); // [empty × 2, 6]
// With thisArg:
const multiplier = { factor: 10 };
console.log([1, 2].myMap(function(x) {
return x * this.factor;
}, multiplier)); // [10, 20]
Key points interviewers check:
- Uses
callback.call(thisArg, ...) not just callback(...) - Passes three arguments: element, index, original array
- Handles sparse arrays with
i in this check - Input validation (typeof check)
</details>
Question 6: Deep Clone
"Implement a deep clone function that handles objects, arrays, dates, and nested structures."
<details> <summary>Solution</summary>
function deepClone(value, seen = new WeakMap()) {
// Primitives and null — return as-is
if (value === null || typeof value !== 'object') {
return value;
}
// Handle circular references
if (seen.has(value)) {
return seen.get(value);
}
// Handle Date
if (value instanceof Date) {
return new Date(value.getTime());
}
// Handle RegExp
if (value instanceof RegExp) {
return new RegExp(value.source, value.flags);
}
// Handle Array
if (Array.isArray(value)) {
const clone = [];
seen.set(value, clone);
value.forEach((item, index) => {
clone[index] = deepClone(item, seen);
});
return clone;
}
// Handle Object
const clone = Object.create(Object.getPrototypeOf(value));
seen.set(value, clone);
for (const key of Object.keys(value)) {
clone[key] = deepClone(value[key], seen);
}
return clone;
}
// Testing:
const original = {
name: 'Test',
date: new Date(),
nested: { arr: [1, { deep: true }] },
regex: /hello/gi
};
const cloned = deepClone(original);
cloned.nested.arr[1].deep = false;
console.log(original.nested.arr[1].deep); // true (not affected)
// Circular reference test:
const circular = { a: 1 };
circular.self = circular;
const clonedCircular = deepClone(circular);
console.log(clonedCircular.self === clonedCircular); // true (circular preserved)
Bonus: Mention structuredClone() as the modern built-in alternative, but show you can implement it manually. </details>
Question 7: Implement Throttle
"Write a throttle function that ensures a function is called at most once per specified time period."
<details> <summary>Solution</summary>
function throttle(fn, limit) {
let lastCallTime = 0;
let timeoutId = null;
return function(...args) {
const now = Date.now();
const timeSinceLastCall = now - lastCallTime;
if (timeSinceLastCall >= limit) {
// Enough time has passed — call immediately
lastCallTime = now;
fn.apply(this, args);
} else if (!timeoutId) {
// Schedule a trailing call
timeoutId = setTimeout(() => {
lastCallTime = Date.now();
timeoutId = null;
fn.apply(this, args);
}, limit - timeSinceLastCall);
}
};
}
// Usage:
const throttledScroll = throttle(() => {
console.log('Scroll event processed at:', Date.now());
}, 200);
window.addEventListener('scroll', throttledScroll);
// Difference from debounce:
// Debounce: waits until events STOP, then fires once
// Throttle: fires at regular intervals DURING continuous events
Follow-up question: "What's the difference between leading-edge and trailing-edge throttle?"
- Leading: fires immediately on first call, then waits
- Trailing: waits the full interval, then fires (our setTimeout version)
- Both: fires immediately AND after the interval
</details>
Round 3: Problem Solving (30 minutes)
Question 8: Flatten Nested Object Keys
"Given a nested object, flatten it so that nested keys are joined with dots."
// Input:
{ a: { b: { c: 1 } }, d: [2, 3], e: 'hello' }
// Output:
{ 'a.b.c': 1, 'd': [2, 3], 'e': 'hello' }
<details> <summary>Solution</summary>
function flattenObject(obj, prefix = '', result = {}) {
for (const key of Object.keys(obj)) {
const newKey = prefix ? `${prefix}.${key}` : key;
const value = obj[key];
if (
typeof value === 'object' &&
value !== null &&
!Array.isArray(value) &&
!(value instanceof Date)
) {
// Recursively flatten plain objects
flattenObject(value, newKey, result);
} else {
result[newKey] = value;
}
}
return result;
}
// Testing:
const nested = {
a: { b: { c: 1 } },
d: [2, 3],
e: 'hello',
f: { g: { h: { i: true } } }
};
console.log(flattenObject(nested));
// {
// 'a.b.c': 1,
// 'd': [2, 3],
// 'e': 'hello',
// 'f.g.h.i': true
// }
// Edge cases:
console.log(flattenObject({})); // {}
console.log(flattenObject({ a: null })); // { a: null }
console.log(flattenObject({ a: { b: undefined } })); // { 'a.b': undefined }
Time complexity: O(n) where n is the total number of leaf values (we visit each once). Space complexity: O(n) for the result object plus O(d) call stack depth where d is the maximum nesting depth. </details>
Question 9: Promise.all Polyfill
"Implement your own version of Promise.all."
<details> <summary>Solution</summary>
function promiseAll(promises) {
return new Promise((resolve, reject) => {
// Handle non-array input
const inputs = Array.from(promises);
if (inputs.length === 0) {
resolve([]);
return;
}
const results = new Array(inputs.length);
let resolvedCount = 0;
inputs.forEach((promise, index) => {
// Wrap non-promise values with Promise.resolve
Promise.resolve(promise)
.then(value => {
results[index] = value; // Maintain order!
resolvedCount++;
if (resolvedCount === inputs.length) {
resolve(results);
}
})
.catch(reject); // First rejection rejects the whole thing
});
});
}
// Testing:
async function test() {
// All resolve
const result1 = await promiseAll([
Promise.resolve(1),
Promise.resolve(2),
Promise.resolve(3)
]);
console.log(result1); // [1, 2, 3]
// Mixed values and promises
const result2 = await promiseAll([1, Promise.resolve(2), 'hello']);
console.log(result2); // [1, 2, 'hello']
// Order maintained regardless of resolution time
const result3 = await promiseAll([
new Promise(r => setTimeout(() => r('slow'), 100)),
Promise.resolve('fast'),
]);
console.log(result3); // ['slow', 'fast'] (order preserved!)
// Rejection case
try {
await promiseAll([
Promise.resolve(1),
Promise.reject(new Error('Failed!')),
Promise.resolve(3)
]);
} catch (error) {
console.log(error.message); // 'Failed!'
}
}
test();
Key points interviewers check:
- Results maintain the same order as input (not resolution order)
- Non-promise values are handled via
Promise.resolve() - First rejection immediately rejects the returned promise
- Empty array resolves with empty array
- Counter pattern to know when all have resolved
</details>
Question 10: LRU Cache
"Implement a Least Recently Used (LRU) cache with O(1) get and put operations."
<details> <summary>Solution</summary>
class LRUCache {
constructor(capacity) {
this.capacity = capacity;
this.cache = new Map(); // Map maintains insertion order
}
get(key) {
if (!this.cache.has(key)) {
return -1;
}
// Move to end (most recently used)
const value = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, value);
return value;
}
put(key, value) {
// If key exists, delete it first (to update position)
if (this.cache.has(key)) {
this.cache.delete(key);
}
// If at capacity, remove the least recently used (first item)
if (this.cache.size >= this.capacity) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, value);
}
// Bonus: display cache state
toString() {
return JSON.stringify([...this.cache.entries()]);
}
}
// Testing:
const cache = new LRUCache(3);
cache.put('a', 1);
cache.put('b', 2);
cache.put('c', 3);
console.log(cache.get('a')); // 1 (moves 'a' to most recent)
cache.put('d', 4); // Evicts 'b' (least recently used)
console.log(cache.get('b')); // -1 (evicted)
console.log(cache.get('c')); // 3
console.log(cache.get('d')); // 4
// Why Map works for O(1):
// - Map.get() is O(1)
// - Map.delete() is O(1)
// - Map.set() is O(1)
// - Map maintains insertion order, so first key = oldest
// - delete + set = move to end (most recent)
Why this is a great interview question:
- Tests knowledge of Map's ordering guarantees
- Tests understanding of cache eviction strategies
- Simple solution in JavaScript (harder in languages without ordered maps)
- Has real-world applications (browser cache, API response caching, memoization)
</details>
Scoring Guide
| Question | Topic | Max Points |
|---|
| 1 | Hoisting & Scope | 5 |
| 2 | Closures & Privacy | 5 |
| 3 | Event Loop | 10 |
| 4 | this Binding | 5 |
| 5 | Array Method Polyfill | 10 |
| 6 | Deep Clone | 15 |
| 7 | Throttle | 10 |
| 8 | Recursion & Objects | 15 |
| 9 | Promise Internals | 15 |
| 10 | Data Structures | 10 |
| Total | | 100 |
Score interpretation:
- 80-100: Strong hire — ready for senior roles
- 60-79: Hire — solid fundamentals, some gaps to fill
- 40-59: Maybe — review weak areas and retry in 2 weeks
- Below 40: Need more preparation — focus on fundamentals first
Summary
This mock interview covers the full spectrum of JavaScript interview topics: scope and hoisting, closures, event loop mechanics, this binding, polyfill implementation, recursion, async patterns, and data structures. Practice under timed conditions, explain your thinking out loud, and review any question where you scored below full marks. Repeat this mock weekly until you consistently score above 80.