DEV Community

John Still
John Still

Posted on

Interview Questions: JavaScript (With Local Dev Bonus Tip)

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!"
Enter fullscreen mode Exit fullscreen mode

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");
})();
Enter fullscreen mode Exit fullscreen mode

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 to while, but executes the block at least once before checking the condition.
  • forEach – an array method that takes a callback to run on each element (no break 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++]);
}
Enter fullscreen mode Exit fullscreen mode

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";
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 with var 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 like let, 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)
Enter fullscreen mode Exit fullscreen mode

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 applying callback to each element of the original array.
  • filter(callback) – returns a new array containing only the elements for which callback(element) is true.
  • reduce(callback, initial) – reduces the array to a single value by applying callback(accumulator, current) across elements, starting with the initial 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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));
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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” but typeof 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 global undefined value indicates absence of value.
  • Undeclared means a variable was never declared with var/let/const. Accessing an undeclared variable throws a ReferenceError. For example, console.log(z); // ReferenceError if z 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
Enter fullscreen mode Exit fullscreen mode

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, or undefined 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 inherit this 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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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"); } };
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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.

javascript #interview #beginners #webdev #macos*

Top comments (0)