Output questions aren't about memorizing edge cases. They're about having a clear enough mental model of JavaScript's execution that you can trace code mentally, step by step. Each question below tests a specific concept. Read the question, think it through, then check the answer and explanation.
Section 1: Hoisting & Scope
Question 1
console.log(a)
console.log(b)
var a = 1
let b = 2
Output: undefined, then ReferenceError: Cannot access 'b' before initialization
var a is hoisted and initialized to undefined. let b is hoisted but sits in the Temporal Dead Zone — reading it throws. The error stops execution so b's log never prints a value.
---
Question 2
function outer() {
var x = 10
function inner() {
console.log(x)
var x = 20
console.log(x)
}
inner()
}
outer()
Output: undefined, 20
There's a var x inside inner() — it's hoisted to the top of inner's scope and initialized to undefined. The first console.log(x) sees the local x (hoisted, undefined) — not the outer x = 10. Then x is assigned 20.
---
Question 3
console.log(typeof foo)
console.log(typeof bar)
function foo() {}
var bar = function() {}
Output: 'function', 'undefined'
Function declarations are fully hoisted — foo is a function before the console.log. bar is a var declaration, hoisted and initialized to undefined. The function expression hasn't been assigned yet.
---
Section 2: Closures & Loops
Question 4
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0)
}
Output: 3, 3, 3
All three callbacks close over the same var i. By the time any of them fire, the loop has completed and i is 3. There's only one i in memory.
---
Question 5
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0)
}
Output: 0, 1, 2
let is block-scoped and creates a new binding per iteration. Each callback closes over a different i. This is the fix to Question 4.
---
Question 6
const funcs = []
for (var i = 0; i < 3; i++) {
funcs.push(function() { return i })
}
console.log(funcs[0](), funcs[1](), funcs[2]())
Output: 3 3 3
Same root cause as Question 4 — all three functions share one var i. Fix: let i, or funcs.push(((j) => () => j)(i)) to capture by IIFE.
---
Question 7
function makeAdder(x) {
return function(y) {
return x + y
}
}
const add5 = makeAdder(5)
const add10 = makeAdder(10)
console.log(add5(3))
console.log(add10(3))
console.log(add5 === add10)
Output: 8, 13, false
Each makeAdder call creates a new closure with its own x. add5 and add10 are different function objects in memory.
---
Section 3: this Keyword
Question 8
const obj = {
name: 'Alice',
greet: function() {
console.log(this.name)
}
}
const greet = obj.greet
obj.greet()
greet()
Output: 'Alice', undefined (or error in strict mode)
obj.greet() — method call, this is obj. greet() — standalone call, this is undefined in strict mode or the global object in sloppy mode. In a browser without strict mode, this.name would be window.name (empty string).
---
Question 9
const obj = {
name: 'Alice',
greet: () => {
console.log(this.name)
}
}
obj.greet()
Output: undefined
Arrow functions don't have their own this. They inherit it from the lexical scope where they were defined. greet was defined at the top level (inside an object literal, which is not a scope), so this is the global object or undefined in strict mode.
---
Question 10
function Person(name) {
this.name = name
}
Person.prototype.greet = function() {
const inner = () => console.log(this.name)
inner()
}
const alice = new Person('Alice') alice.greet()
Output: 'Alice'
The arrow function inner inherits this from greet's execution context. When greet is called as a method on alice, this is alice. The arrow correctly captures it.
---
Question 11
const obj = { x: 10 }
function logX() { console.log(this.x) }
logX.call(obj) logX.apply(obj) const bound = logX.bind(obj) bound() console.log(logX === bound)
Output: 10, 10, 10, false
call and apply invoke immediately with explicit this. bind returns a new function — logX and bound are different objects.
---
Section 4: Event Loop & Async
Question 12
console.log('start')
setTimeout(() => console.log('timeout'), 0)
Promise.resolve().then(() => console.log('promise'))
console.log('end')
Output: start, end, promise, timeout
Sync runs first. Then microtasks (Promise). Then macrotasks (setTimeout). Even with 0ms delay, setTimeout is a macrotask and always runs after all microtasks.
---
Question 13
setTimeout(() => console.log('A'), 0)
setTimeout(() => console.log('B'), 0)
Promise.resolve()
.then(() => console.log('C'))
.then(() => console.log('D'))
Output: C, D, A, B
Both Promises queue as microtasks. All microtasks drain completely before any macrotask. So C, then D (chained microtask), then A, then B.
---
Question 14
async function main() {
console.log('1')
const result = await Promise.resolve('2')
console.log(result)
console.log('3')
}
console.log('before')
main()
console.log('after')
Output: before, 1, after, 2, 3
main() runs synchronously until await — logs 1, then suspends. Control returns to the caller. after logs. Then the microtask resumes: logs 2 and 3.
---
Question 15
const p = new Promise(resolve => {
console.log('executor')
resolve(1)
})
console.log('after new Promise')
p.then(v => console.log('then:', v))
console.log('end')
Output: executor, after new Promise, end, then: 1
The executor runs synchronously inside new Promise(). Resolution is immediate, but the .then() callback is always scheduled asynchronously as a microtask — it runs after the current synchronous code finishes.
---
Section 5: Type Coercion
Question 16
console.log(1 + '2')
console.log('3' - 1)
console.log(true + true)
console.log([] + [])
console.log({} + [])
Output: '12', 2, 2, '', '[object Object]'
+ with a string coerces to string concatenation. - has no string case — both sides convert to number. true coerces to 1. [] + [] — both become empty strings, concatenate to ''. {} + [] — {} becomes '[object Object]', [] becomes ''.
---
Question 17
console.log(0 == false)
console.log('' == false)
console.log(null == undefined)
console.log(null == false)
console.log(NaN == NaN)
Output: true, true, true, false, false
== coerces: false → 0, so 0 == 0. null == undefined is true by spec — the only things null loosely equals. null does NOT loosely equal false. NaN is never equal to anything, including itself.
---
Question 18
console.log(typeof null)
console.log(typeof undefined)
console.log(typeof NaN)
console.log(typeof function(){})
console.log(typeof [])
Output: 'object', 'undefined', 'number', 'function', 'object'
null → 'object' (historical bug). NaN is type number. Functions have their own typeof result. Arrays are objects.
---
Section 6: Prototypes & Classes
Question 19
function Animal(name) { this.name = name }
Animal.prototype.speak = function() { return this.name }
function Dog(name) { Animal.call(this, name) } Dog.prototype = Object.create(Animal.prototype) Dog.prototype.constructor = Dog
const d = new Dog('Rex') console.log(d.speak()) console.log(d instanceof Dog) console.log(d instanceof Animal) console.log(d.constructor === Dog)
Output: 'Rex', true, true, true
Classical inheritance: Dog.prototype is linked to Animal.prototype. instanceof walks the chain. The constructor reset is why d.constructor === Dog is true.
---
Question 20
class A {
constructor() { this.x = 1 }
getX() { return this.x }
}
class B extends A {
constructor() {
super()
this.x = 2
}
}
const b = new B()
console.log(b.getX())
console.log(b instanceof A)
Output: 2, true
super() runs A's constructor setting x = 1, then B's constructor sets x = 2. getX returns this.x which is 2 (the own property). instanceof checks the chain: B extends A so both are true.
---
Section 7: Objects & References
Question 21
const a = { value: 1 }
const b = a
b.value = 2
console.log(a.value)
console.log(a === b)
Output: 2, true
b = a copies the reference, not the object. Both point to the same object in memory. Mutating through b mutates a. They're === equal because they're literally the same reference.
---
Question 22
const obj = { a: 1 }
Object.freeze(obj)
obj.a = 99
obj.b = 2
console.log(obj.a)
console.log(obj.b)
Output: 1, undefined
Freeze prevents mutation in strict mode (throws) or silently fails in sloppy mode. obj.a stays 1. obj.b was never added.
---
Question 23
const obj = {}
Object.defineProperty(obj, 'x', {
value: 42,
writable: false,
enumerable: false,
})
console.log(obj.x)
console.log(Object.keys(obj))
obj.x = 100
console.log(obj.x)
Output: 42, [], 42
Non-enumerable means Object.keys doesn't see it. Non-writable means assignment is silently ignored (or throws in strict mode).
---
Section 8: Functions
Question 24
function foo(a, b = a * 2) {
return a + b
}
console.log(foo(3))
console.log(foo(3, 4))
Output: 9, 7
Default parameter b = a * 2 — when b is omitted, b = 6, a + b = 9. When b is provided as 4, default is ignored: 3 + 4 = 7.
---
Question 25
const obj = {
x: 10,
getX() {
return () => this.x
}
}
const fn = obj.getX()
console.log(fn())
Output: 10
getX() is a method call — this is obj. The returned arrow function captures that this lexically. Calling fn() as a standalone function doesn't change this for the arrow — it still refers to obj.
---
Section 9: Advanced
Question 26
let x = 1
function outer() {
let x = 2
function inner() {
let x = 3
console.log(x)
}
inner()
console.log(x)
}
outer()
console.log(x)
Output: 3, 2, 1
Each function has its own x. inner sees its own 3. outer sees its own 2. Global sees 1. The scope chain is read outward, never inward.
---
Question 27
async function fetchData() {
return 42
}
const result = fetchData()
console.log(result)
result.then(v => console.log(v))
Output: Promise { 42 }, 42
async functions always return a Promise, even when returning a plain value. result is a Promise, not 42. .then() unwraps it.
---
Question 28
console.log([1,2,3].map(parseInt))
Output: [1, NaN, NaN]
map passes three arguments to its callback: (element, index, array). parseInt takes two: (string, radix). So the calls are parseInt(1, 0), parseInt(2, 1), parseInt(3, 2). Radix 0 is treated as 10 (returns 1). Radix 1 is invalid (returns NaN). '3' in base 2 has no digit '3' (returns NaN).
---
Question 29
const promise = new Promise((resolve) => {
resolve(1)
resolve(2)
resolve(3)
})
promise.then(v => console.log(v))
Output: 1
A Promise settles exactly once. Only the first resolve call has any effect. All subsequent calls are silently ignored.
---
Question 30
function* gen() {
yield 1
yield 2
return 3
}
const g = gen()
console.log(g.next())
console.log(g.next())
console.log(g.next())
console.log(g.next())
Output: { value: 1, done: false } { value: 2, done: false } { value: 3, done: true } { value: undefined, done: true }
yield pauses and returns the value with done: false. return ends the generator with done: true. Any call after completion returns { value: undefined, done: true }.
---
How to Use These
Work through each question before reading the answer. If you got it wrong, don't just accept the answer — trace the code manually using the execution model: what's on the call stack, what's in scope, what queues are active.
The questions you get wrong are exactly the concepts worth drilling. Every one of them maps to a topic with dedicated questions on JSPrep.