Welcome to Part 2 of our technical interview series, focusing on JavaScript fundamentals. Below are common JavaScript interview questions and concise answers, illustrated with code examples where helpful. These cover concepts from basic syntax to advanced behaviors.
What is an IIFE?
IIFE stands for Immediately Invoked Function Expression. It’s a function defined and invoked at the same time. In JavaScript, this idiom runs a function right when the interpreter reaches it, creating a new scope without polluting the global namespace. For example:
(function() {
let msg = "Hello IIFE!";
console.log(msg);
})(); // Immediately executes and logs "Hello IIFE!"
Here the anonymous function is wrapped in parentheses and immediately called. You can also use other forms like arrow functions:
(() => {
console.log("IIFE with arrow function");
})();
IIFEs are useful for initializing code or encapsulating variables to avoid global scope.
What loop types are available in JavaScript?
JavaScript supports several loops:
-
for
loop – the classic loop with initializer, condition, and increment. -
while
loop – repeats as long as a condition is true. -
do...while
loop – similar towhile
, but executes the block at least once before checking the condition. -
forEach
– an array method that takes a callback to run on each element (nobreak
support). -
for...in
loop – iterates over the keys of an object (or array indices). -
for...of
loop – iterates over the values of any iterable (arrays, strings, etc.).
Example:
const nums = [10, 20, 30];
// Traditional for-loop
for (let i = 0; i < nums.length; i++) {
console.log(nums[i]);
}
// forEach
nums.forEach(n => console.log(n));
// for...of (ES6)
for (const n of nums) {
console.log(n);
}
// while
let i = 0;
while (i < nums.length) {
console.log(nums[i++]);
}
Each loop has use-cases: forEach
/for...of
simplify array iteration; for...in
is for object keys; for/while
offer full control including early break
.
Explain hoisting
Hoisting is JavaScript’s default behavior of moving declarations to the top of their scope during compilation. Functions and variables declared with var
are hoisted, meaning their declaration is processed before code execution. For example, you can call a function before its definition:
console.log(greet()); // works because function is hoisted
function greet() {
return "Hello";
}
For variables: a var
declaration is hoisted (initialized as undefined
), whereas let
and const
are hoisted but not initialized, causing a temporal dead zone until their definition. For instance:
console.log(a); // undefined (var is hoisted)
var a = 5;
console.log(b); // ReferenceError (b is hoisted but uninitialized)
let b = 10;
This means you should declare variables before use, especially with let/const
, to avoid unexpected undefined
or errors.
What are the key ES6+ features?
ES6 (ES2015) introduced many syntax and API improvements. Key features include block-scoped variables (let
, const
), arrow functions (() => {}
), template literals (`Hello ${name}`
), default parameters, destructuring (unpacking objects/arrays), spread/rest operators (...
), and classes (syntactic sugar for prototypes). It also added modules (import
/export
), Promises for async work, Map/Set collections, and more. Subsequent versions added features like async/await (ES2017), optional chaining (?.
), nullish coalescing (??
), BigInt, and newer array methods (e.g. flat()
, flatMap()
).
Example (ES6 destructuring and arrow):
const person = { name: "Alice", age: 30 };
const {name, age} = person;
const greet = (n = "Friend") => `Hi, ${n}!`;
console.log(greet(name)); // Hi, Alice!
What are JavaScript data types?
JavaScript has eight fundamental types. Seven are primitive: Number
, String
, Boolean
, BigInt
(for large integers), Symbol
, Undefined
, and Null
; the eighth is Object
(which includes arrays, functions, dates, etc.). For example:
let num = 42; // Number
let str = "hello"; // String
let flag = true; // Boolean
let big = 9007199254740991n; // BigInt
let sym = Symbol("id");// Symbol
let nothing = null; // Null
let undef; // Undefined (declared but no value)
let obj = {x:1}; // Object
All primitive types except null
and undefined
have wrapper objects (e.g. String
, Number
) with useful methods. You can check a value’s type with typeof
. For example, typeof null
returns "object"
(a known quirk), but usually:
console.log(typeof num); // "number"
console.log(typeof str); // "string"
console.log(typeof sym); // "symbol"
console.log(typeof nothing); // "object" (null is a primitive)
console.log(typeof undef); // "undefined"
How does the event loop and async behavior work?
JavaScript runs on a single thread but can handle async tasks via an event loop and callback queues. The execution model uses a call stack (for the current function execution) and an event queue (for tasks waiting to run). When asynchronous operations (timers, HTTP requests, I/O) complete, their callbacks are queued. The event loop takes queued tasks one by one and pushes their callbacks onto the stack when it’s empty.
For example:
console.log("Start");
setTimeout(() => console.log("Timeout"), 0);
Promise.resolve().then(() => console.log("Promise"));
console.log("End");
// Output order: Start, End, Promise, Timeout
Here, the Promise
callback (a microtask) runs before the setTimeout
callback (a macrotask), even though both were scheduled. This is because the event loop processes all microtasks (like promise callbacks) before handling the next macrotask. In short, async JavaScript uses the event loop to defer work and avoid blocking; callbacks, promises, and async/await
all rely on this non-blocking mechanism.
What is a closure?
A closure is a function that “remembers” its lexical environment (variables in scope) even after the outer function has finished. In other words, it retains access to the outer scope where it was defined. For example:
function outer() {
let x = 10;
return function inner() {
console.log(x);
};
}
const fn = outer(); // outer has returned
fn(); // logs 10, because inner() closes over x
Even though outer()
has completed, the inner
function still has access to x
via closure. This happens because inner
preserves the context in which it was created. Closures enable patterns like private variables and function factories. They are common in callbacks, modules, and functional programming techniques.
var
vs let
vs const
-
var
: function-scoped (or global if outside a function). Variables declared withvar
are hoisted and can be re-declared and updated. -
let
: block-scoped. Cannot be re-declared in the same scope (within the same{}
block), but can be updated. It is hoisted but has a temporal dead zone until initialization. -
const
: block-scoped likelet
, but cannot be reassigned or re-declared. It must be initialized when declared.
Example:
if (true) {
var a = 1;
let b = 2;
const c = 3;
}
console.log(a); // 1 (var is function/global-scoped)
console.log(b); // ReferenceError (b is block-scoped)
console.log(c); // ReferenceError (c is block-scoped)
Also, const
doesn’t make the value immutable if it’s an object; it just prevents reassigning the variable name. These rules help write safer code. Using let
/const
(ES6+) avoids many var
pitfalls like unintended globals or hoisting surprises.
What do map
, filter
, and reduce
do?
These are higher-order array methods for transforming and aggregating data:
-
map(callback)
– returns a new array by applyingcallback
to each element of the original array. -
filter(callback)
– returns a new array containing only the elements for whichcallback(element)
is true. -
reduce(callback, initial)
– reduces the array to a single value by applyingcallback(accumulator, current)
across elements, starting with theinitial
value.
Example:
const nums = [1, 2, 3, 4];
// map: double each element
const doubles = nums.map(n => n * 2); // [2, 4, 6, 8]
// filter: keep only even numbers
const evens = nums.filter(n => n % 2 === 0); // [2, 4]
// reduce: sum all elements
const sum = nums.reduce((acc, n) => acc + n, 0); // 10
These functions do not mutate the original array; they return new arrays or values. They promote functional programming patterns and eliminate common loop patterns.
What are prototypes?
In JavaScript, prototypes are how objects inherit features. Every object has an internal link to a prototype object. If you access a property or method not found on the object itself, JS looks up the prototype chain. The chain ends at null
. For example, when you call myObj.toString()
, JS finds toString
on Object.prototype
, because myObj
inherits from it.
MDN describes it: “Every object in JavaScript has a built-in property called its prototype. The prototype itself can have a prototype, forming a chain.”. You set an object’s prototype with Object.setPrototypeOf()
or via class
syntax. Under the hood, classes are just syntactic sugar over prototypes. Prototypes are fundamental to JS’s inheritance model.
What are getters and setters?
Getters and setters (accessors) allow custom code when properties are read or written. Using the get
and set
keywords inside an object (or class) definition defines these special methods. A getter runs when a property is accessed; a setter runs when a property is assigned. For example:
const obj = {
_x: 0,
get x() {
return this._x;
},
set x(value) {
this._x = value;
}
};
console.log(obj.x); // calls getter, outputs 0
obj.x = 5; // calls setter
console.log(obj.x); // now outputs 5
MDN notes that a getter defers calculating the property’s value until it’s accessed, and can encapsulate logic. Similarly, setters can validate or transform values. Getters/setters make object properties behave more like computed values or enforce invariants.
What are callbacks and promises?
A callback is a function passed into another function, to be invoked after some operation. For example, setTimeout(() => { ... }, 1000)
uses a callback to run after 1 second. MDN defines a callback as “a function passed into another function as an argument, which is then invoked inside the outer function”. Callbacks can be synchronous or asynchronous, but without structure they can lead to “callback hell” when nested.
A Promise is an object representing the eventual result of an asynchronous operation. It can be in pending, fulfilled, or rejected state. Instead of supplying a callback to a function, a promise lets you attach handlers via .then()
and .catch()
. For example:
// Using callback (older style)
function asyncTask(callback) {
setTimeout(() => callback(null, "Done"), 100);
}
asyncTask((err, result) => {
if (!err) console.log(result);
});
// Using promise (modern)
function asyncTaskP() {
return new Promise(resolve =>
setTimeout(() => resolve("Done"), 100));
}
asyncTaskP().then(result => console.log(result));
Promises improve readability and error handling over raw callbacks. They allow chaining (.then().catch()
) and combine well with async/await
. Both callbacks and promises handle async flows, but promises (and async/await
) generally lead to cleaner code.
What is async/await
?
async/await
is syntax sugar over promises introduced in ES2017. It makes asynchronous code look like synchronous code. An async
function automatically returns a promise. Within it, await
pauses execution until a promise resolves (or rejects). For example:
async function fetchData() {
try {
let res = await fetch('/api/data');
let data = await res.json();
console.log(data);
} catch (err) {
console.error(err);
}
}
fetchData();
Here, await fetch(...)
waits for the fetch promise to resolve, then proceeds. This avoids nested .then()
calls, flattening the code and making it easier to read. If an awaited promise is rejected, it throws an error you can catch with try...catch
. In summary, async/await
simplifies promise-based code by allowing the use of imperative-style control flow while still handling asynchronous operations.
What’s the difference between null
, undefined
, and undeclared?
-
null
is an assignment value that represents “no value” or “empty” and is a primitive. It is explicitly assigned:let x = null;
. (It’s loosely “falsy” buttypeof null
returns"object"
.) -
undefined
means a variable has been declared but not assigned a value, or a function didn’t return anything. E.g.let y; console.log(y); // undefined
. The special globalundefined
value indicates absence of value. -
Undeclared means a variable was never declared with
var/let/const
. Accessing an undeclared variable throws aReferenceError
. For example,console.log(z); // ReferenceError
ifz
was never declared.
In short: null
is a deliberate empty value, undefined
is the default for “not set,” and undeclared means the identifier isn’t defined at all.
What is a Singleton?
A Singleton is a design pattern that ensures a class or module has only one instance and provides a global access point to it. When the instance is requested repeatedly, the same instance is returned. For example, a logging utility might be a singleton so all parts of an app use the same logger. In JavaScript, this can be implemented via an IIFE or module that creates one object:
const Logger = (function() {
let instance;
function createInstance() {
return { log: msg => console.log(msg) };
}
return {
getInstance: function() {
if (!instance) instance = createInstance();
return instance;
}
};
})();
const logger1 = Logger.getInstance();
const logger2 = Logger.getInstance();
console.log(logger1 === logger2); // true: same instance
As GeeksforGeeks explains, a Singleton controls instantiation so that only one instance is created (often stored in a static variable). In JS, modules (imported via ES6 export
) are singletons by default, since the module is evaluated once.
How does the this
keyword work?
The value of this
in JavaScript depends on how a function is called (its invocation context), not where it is defined. In general:
- In an object method (e.g.
obj.method()
),this
refers to the object (obj
) that owns the method. - In a regular function (not called as a method),
this
is the global object (window
in browsers) in non-strict mode, orundefined
in strict mode. - In a constructor (called with
new
),this
is the new object being created. -
Arrow functions do not have their own
this
; they inheritthis
from the enclosing (lexical) scope.
For example:
const obj = {
value: 5,
print() { console.log(this.value); }
};
obj.print(); // this -> obj, logs 5
const f = obj.print;
f(); // this -> global or undefined, not obj
const arrow = () => console.log(this);
arrow(); // this is from outer scope (often global or module)
Thus, this
is dynamically bound by call site. Methods like bind
, call
, or apply
can explicitly set this
. Arrow functions keep the this
of where they were defined.
Functional vs Object-Oriented paradigms
JavaScript is multi-paradigm and supports both functional and object-oriented styles. In a functional style, code emphasizes pure functions, immutability, and passing functions around. It uses first-class functions, higher-order functions (like map/filter
), and avoids shared mutable state. In contrast, the object-oriented style uses objects (often created via classes or prototypes), encapsulation, and inheritance or composition. For example, an OOP approach might define a class User
with methods, whereas a functional approach might process user data through chained functions.
JS supports both: ES6 classes (class
) give an OOP syntax, while features like first-class functions and array methods facilitate functional programming. As MDN notes, JS supports imperative, functional, and object-oriented paradigms, so you can mix styles as needed. The key difference is how you organize code and manage state: FP avoids changing data and uses functions as values, while OOP groups data and behavior in objects and uses methods.
What are higher-order functions?
A higher-order function is a function that takes other functions as arguments or returns a function as its result. In JavaScript, functions are first-class, so you can pass them around like any other value. For example:
function twice(fn, x) {
return fn(fn(x));
}
function add2(n) { return n + 2; }
console.log(twice(add2, 5)); // 9 (adds 2 twice)
Here, twice
is higher-order because it takes fn
as a parameter and calls it. This makes code more reusable and modular. Common higher-order functions include array methods like map
, filter
, and reduce
(they take callbacks). Using HOFs lets you abstract behavior.
Object literals and classes
In JavaScript, you can create objects directly with object literal syntax:
const car = { make: "Toyota", year: 2020, start() { console.log("Vroom"); } };
This uses curly braces {}
to list properties and methods. Object literals are a quick way to build objects on the fly.
ES6 also introduced classes as a cleaner syntax for constructor functions and prototypes. For example:
class Person {
constructor(name) {
this.name = name;
}
greet() {
return `Hello, ${this.name}!`;
}
}
const alice = new Person("Alice");
console.log(alice.greet()); // Hello, Alice!
Under the hood, class
is syntactic sugar for setting up a constructor function and its prototype
. Classes support extends
for inheritance, static methods, getters/setters, etc. In short, object literals are literal notations for single objects, while class
defines a blueprint for multiple objects (instances). Both are commonly used to structure data and behavior in JS.
Bonus Tip: Local Dev Setup on macOS
If you’re practicing interviews or building apps locally on a Mac, setting up servers and databases (PHP, MySQL, Node.js, etc.) by hand (or via Docker) can be cumbersome. A handy tool is ServBay, a native macOS app that lets you run MySQL, Redis, PHP, Node.js and more instantly without manual configuration or containers.
With ServBay you can easily switch Node.js or PHP versions per project, serve local sites over SSL, and manage all services through a simple GUI or CLI. It’s lightweight and fast, removing the friction of Docker for typical dev tasks. Perfect for frontend/backend developers doing interview prep, prototyping, or juggling multiple project environments.
Top comments (0)