Skip to main content

Documentation Index

Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt

Use this file to discover all available pages before exploring further.

Functions & Scope

Functions are first-class citizens in JavaScript. They can be assigned to variables, passed as arguments, and returned from other functions. This makes JavaScript incredibly flexible and powerful. “First-class citizen” means functions are treated like any other value. You can store a function in a variable the same way you store a number. You can pass a function into another function as an argument, the way you pass a string. You can return a function from a function, like returning a configured tool from a factory. This is the single most important idea in JavaScript — it unlocks closures, callbacks, higher-order functions, and most of the patterns you will use every day.

1. Function Declarations vs Expressions

Function Declaration (Hoisted)

// Can be called before it's defined -- this is "hoisting" in action.
// JavaScript reads all function declarations during the compilation phase,
// BEFORE it runs any code. So the function exists from the very first line.
greet('Alice'); // Works!

function greet(name) {
    return `Hello, ${name}!`;
}

Function Expression (Not Hoisted)

// Cannot be called before assignment.
// The variable 'greet' is hoisted, but its VALUE (the function) is not assigned
// until this line executes. Before that, greet is undefined (let/const) or
// would throw a ReferenceError.
greet('Alice'); // ReferenceError: Cannot access 'greet' before initialization

const greet = function(name) {
    return `Hello, ${name}!`;
};

Arrow Functions (ES6)

Concise syntax, and they don’t have their own this.
// Basic syntax -- implicit return for single expressions
const add = (a, b) => a + b;

// With body block -- explicit return required when using curly braces
const greet = (name) => {
    const message = `Hello, ${name}!`;
    return message; // Without this return, the function returns undefined!
};

// Single parameter -- parentheses optional (but many style guides require them)
const double = x => x * 2;

// No parameters -- parentheses required
const sayHi = () => 'Hi!';

// GOTCHA: Returning an object literal requires wrapping in parentheses
const makeUser = (name) => ({ name, role: 'user' });
// Without parens: const makeUser = (name) => { name, role: 'user' };
// JavaScript thinks the curly braces are a function body, not an object!
When to use arrow functions? Use them for short, anonymous functions (callbacks, map/filter). Use regular functions when you need your own this binding (object methods), when you need arguments object access, or when you need hoisting. Arrow functions do not have their own this, arguments, or super.

Function Types — Complete Comparison

FeatureDeclarationExpressionArrow
Syntaxfunction name() {}const name = function() {}const name = () => {}
HoistedYes (entire function)No (only variable, not value)No (only variable, not value)
Has own thisYesYesNo (inherits from enclosing scope)
Has argumentsYesYesNo (use rest params ...args)
Can be a constructor (new)YesYesNo (TypeError)
Has prototype propertyYesYesNo
Implicit returnNoNoYes (single expression only)
Can be a generator (function*)YesYesNo
Best used forNamed functions, methodsCallbacks, IIFEsShort callbacks, preserving this
Decision guide — which function syntax to use:
  • Object/class methods: Use method shorthand (greet() {}) or regular functions. They need their own this.
  • Callbacks for .map, .filter, .then: Use arrow functions. Concise and no this confusion.
  • Event handlers in classes: Use arrow functions or .bind() to preserve this.
  • Top-level named functions: Use declarations. Hoisting makes them available throughout the file.
  • IIFEs: Either works, but arrow IIFEs ((() => { ... })()) are more common in modern code.

2. Parameters & Arguments

Default Parameters (ES6)

function greet(name = 'Guest') {
    return `Hello, ${name}!`;
}

greet();        // 'Hello, Guest!'
greet('Alice'); // 'Hello, Alice!'

Rest Parameters

Collect remaining arguments into an array.
function sum(...numbers) {
    return numbers.reduce((total, n) => total + n, 0);
}

sum(1, 2, 3, 4); // 10

Spread Operator

Expand an array into individual arguments.
const nums = [1, 2, 3];
console.log(Math.max(...nums)); // 3

Rest vs Spread — They Look Identical But Do Opposite Things

SyntaxNamePositionWhat it does
function fn(...args)RestFunction parameterCollects multiple arguments into one array
fn(...array)SpreadFunction callExpands one array into multiple arguments
const [a, ...rest] = arrRestDestructuringCollects remaining elements into an array
const copy = [...arr]SpreadArray/Object literalExpands elements into a new array/object
// Edge case: default parameters are evaluated left-to-right
// Earlier parameters can be used as defaults for later ones
function createRange(start, end = start + 10) {
    return { start, end };
}
createRange(5);    // { start: 5, end: 15 }

// Edge case: passing undefined triggers the default, but null does NOT
function greet(name = 'Guest') {
    return `Hello, ${name}`;
}
greet(undefined);  // 'Hello, Guest' -- undefined triggers default
greet(null);       // 'Hello, null' -- null is a value, not "missing"

// Edge case: arguments object vs rest parameters
function oldStyle() {
    console.log(arguments);        // { '0': 1, '1': 2, '2': 3 } -- array-LIKE, not an array
    console.log(arguments.map);    // undefined -- no array methods!
    console.log([...arguments]);   // [1, 2, 3] -- convert to real array
}
function newStyle(...args) {
    console.log(args);             // [1, 2, 3] -- a real array with all methods
}

3. Scope

Scope determines where variables are accessible. JavaScript has three types of scope.

Global Scope

Variables declared outside any function or block.
const globalVar = 'I am global';

function test() {
    console.log(globalVar); // Accessible
}

Function Scope

Variables declared inside a function (with var, let, or const).
function test() {
    var functionScoped = 'Only here';
}
console.log(functionScoped); // ❌ ReferenceError

Block Scope (ES6)

Variables declared with let or const inside {}.
if (true) {
    let blockScoped = 'Only in this block';
    const alsoBlockScoped = 'Same here';
}
console.log(blockScoped); // ❌ ReferenceError

Lexical Scope (Static Scope)

Functions are executed using the scope in which they were defined, not where they are called. Think of it like a postal address: a function’s “home address” is fixed at the time it is written. No matter where you call the function from later, it still looks up variables at its home address.
const name = 'Alice';

function outer() {
    const name = 'Bob';
    
    function inner() {
        // inner() was DEFINED inside outer(), so it looks up 'name'
        // in outer()'s scope first -- not wherever inner() gets called from
        console.log(name); // 'Bob' -- looks up the scope chain
    }
    
    return inner;
}

const fn = outer();
fn(); // 'Bob' -- even though we call fn() in the global scope,
      // it remembers the scope where it was born (lexical environment)

4. Closures

A closure is a function that remembers its lexical scope even when executed outside that scope. This is one of the most powerful concepts in JavaScript. The backpack analogy: When a function is created, it packs a “backpack” of all the variables from its surrounding scope. Wherever that function goes — passed as a callback, returned from another function, stored in a variable — it carries that backpack with it. Even after the outer function has finished executing and its local variables would normally be garbage collected, the inner function still has access to them through its backpack. That backpack is the closure.
function createCounter() {
    let count = 0; // This variable goes into the closure's "backpack"
    
    return {
        increment: () => ++count,  // These functions carry 'count' with them
        decrement: () => --count,
        getCount: () => count
    };
}

const counter = createCounter();
// createCounter() has finished executing, but 'count' lives on
// inside the backpack of the returned functions
counter.increment(); // 1
counter.increment(); // 2
counter.getCount();  // 2

// count is not accessible directly -- it is truly private
console.log(counter.count); // undefined

Practical Use Cases

1. Data Privacy (Module Pattern)
const bankAccount = (function() {
    let balance = 0; // Private
    
    return {
        deposit: (amount) => balance += amount,
        withdraw: (amount) => balance -= amount,
        getBalance: () => balance
    };
})();

bankAccount.deposit(100);
bankAccount.getBalance(); // 100
2. Function Factories
function multiply(factor) {
    return (number) => number * factor;
}

const double = multiply(2);
const triple = multiply(3);

double(5); // 10
triple(5); // 15
3. Event Handlers with State
function createClickHandler(buttonId) {
    let clickCount = 0; // Each handler gets its own private count
    
    return function() {
        clickCount++;
        console.log(`Button ${buttonId} clicked ${clickCount} times`);
    };
}

const handler = createClickHandler('submit');
// Each click remembers the count -- the closure persists clickCount
// between invocations. This is how you maintain state without globals.
Classic closure gotcha — loops with var: Before let existed, closures in loops were a notorious trap. A for loop with var creates a single shared variable, so all closures in the loop see the same final value. Using let in the loop header fixes this because let creates a new binding per iteration.
// BUG: All timeouts print 3, because var i is shared across iterations
for (var i = 0; i < 3; i++) {
    setTimeout(() => console.log(i), 100); // 3, 3, 3
}

// FIX: let creates a new 'i' for each loop iteration
for (let i = 0; i < 3; i++) {
    setTimeout(() => console.log(i), 100); // 0, 1, 2
}

5. The Execution Context and Call Stack

Every time a function is called, JavaScript creates an Execution Context — a box that holds the function’s local variables, its this value, and a reference to the outer scope. These contexts are managed in a Call Stack (Last In, First Out). Think of the call stack like a stack of plates at a buffet. Every time you call a function, a new plate goes on top. When that function returns, its plate is removed. You can only work with the plate on top. If you keep adding plates without removing any (infinite recursion), the stack crashes.
function first() {
    console.log('First');
    second();              // Pauses first(), pushes second() onto the stack
    console.log('First again'); // Resumes after second() returns
}

function second() {
    console.log('Second');
    third();               // Pauses second(), pushes third() onto the stack
}

function third() {
    console.log('Third');  // Returns, popped off the stack
}

first();
// Output: First, Second, Third, First again
Call Stack visualization:
1. [Global]
2. [Global] -> [first()]
3. [Global] -> [first()] -> [second()]
4. [Global] -> [first()] -> [second()] -> [third()]
5. [Global] -> [first()] -> [second()] <-- third() returns
6. [Global] -> [first()] <-- second() returns
7. [Global] <-- first() returns
Stack Overflow: If you have infinite recursion (a function calling itself without a base case), the call stack overflows and throws RangeError: Maximum call stack size exceeded. In Chrome, the default stack size is around 10,000-15,000 frames. You can see the exact call stack in your browser’s DevTools when this happens.

6. Higher-Order Functions

A higher-order function either takes a function as an argument or returns a function. They are central to functional programming.

Functions as Arguments

const numbers = [1, 2, 3, 4, 5];

// map: Transform each element (returns new array, does NOT mutate original)
const doubled = numbers.map(n => n * 2); // [2, 4, 6, 8, 10]

// filter: Keep elements that pass a test (returns new array)
const evens = numbers.filter(n => n % 2 === 0); // [2, 4]

// reduce: Accumulate to a single value
// The second argument (0) is the initial value for acc. ALWAYS provide it --
// omitting it uses the first array element, which causes bugs on empty arrays.
const sum = numbers.reduce((acc, n) => acc + n, 0); // 15

// find: Get first matching element (returns undefined if not found, not null)
const found = numbers.find(n => n > 3); // 4

// some/every: Boolean tests (short-circuit like && and ||)
numbers.some(n => n > 4);  // true (at least one matches)
numbers.every(n => n > 0); // true (all match)
Performance note: map, filter, and reduce each create a new array or value. If you chain three of them, you iterate the array three times. For small arrays this is fine. For arrays with tens of thousands of elements, consider combining operations in a single reduce, or using a for loop.

Array Methods — When to Use Which

MethodReturnsMutates OriginalUse When
.map(fn)New array (same length)NoTransforming every element
.filter(fn)New array (subset)NoKeeping elements that pass a test
.reduce(fn, init)Single value (any type)NoAccumulating: sums, grouping, building objects
.find(fn)First match or undefinedNoFinding one element by condition
.findIndex(fn)Index or -1NoFinding the position of one element
.some(fn)booleanNo”Does at least one match?” (short-circuits)
.every(fn)booleanNo”Do all match?” (short-circuits)
.forEach(fn)undefinedNoSide effects only (logging, DOM updates)
.sort(fn)Same array (sorted)YesSorting in place (use .toSorted() for immutable)
.includes(val)booleanNo”Is this exact value in the array?”
.flat(depth)New array (flattened)NoRemoving nesting levels
.flatMap(fn)New array (mapped + flat(1))NoMap where each element expands to multiple items

Chaining

const result = [1, 2, 3, 4, 5]
    .filter(n => n % 2 === 0)  // [2, 4]
    .map(n => n ** 2)          // [4, 16]
    .reduce((a, b) => a + b);  // 20

7. IIFE (Immediately Invoked Function Expression)

A function that runs immediately after it is defined. Used to create a private scope. Before ES6 modules existed, IIFEs were the only way to avoid polluting the global scope — every major library (jQuery, Lodash, Backbone) was wrapped in one.
// Classic IIFE -- the outer parentheses tell JavaScript this is an expression,
// not a declaration. The trailing () immediately invokes it.
(function() {
    const secret = 'hidden'; // Not accessible outside this function
    console.log('IIFE executed');
})();

// Arrow function version
(() => {
    console.log('Arrow IIFE');
})();
Modern note: With ES6 modules (import/export) and block-scoped let/const, IIFEs are far less common in new code. But you will still encounter them in legacy codebases and in patterns where you need to execute setup logic immediately with a contained scope.

Summary

  • Function Types: Declarations are hoisted, expressions are not. Arrow functions are concise.
  • Scope: Global → Function → Block. JavaScript uses lexical (static) scoping.
  • Closures: Functions remember their lexical environment. Use for data privacy.
  • Higher-Order Functions: map, filter, reduce are your bread and butter.
  • Call Stack: LIFO structure for managing function execution.
Next, we’ll explore Objects & Prototypes, the foundation of JavaScript’s object model.

Interview Deep-Dive

Strong Answer:
  • A closure is a function bundled together with references to its surrounding lexical environment. When a function is defined inside another function, the inner function retains access to the outer function’s variables even after the outer function has returned and its execution context has been popped off the call stack. The runtime keeps those variables alive as long as the inner function exists.
  • In implementation terms, when V8 creates a function, it attaches a hidden [[Environment]] reference pointing to the lexical environment where the function was defined. That environment object holds the variable bindings. If the inner function references count from the outer scope, count is stored in a “context” object on the heap, not on the stack. The stack frame for the outer function is deallocated, but the heap-allocated context survives because the inner function still points to it.
  • Memory leak scenario: if you create a closure inside an event listener or a setInterval, and you never remove the listener or clear the interval, the closure keeps its entire enclosing scope alive indefinitely. I have debugged a production leak where a setInterval callback inside a React component captured a large data array from the component’s scope. The component unmounted, but the interval was never cleared. Every 5 seconds, the callback ran, holding a reference to a 50MB dataset that could never be garbage collected. After a few hours, the browser tab consumed 2GB of memory.
  • The fix is always cleanup: clearInterval, removeEventListener, AbortController for fetch, or React’s useEffect cleanup return. The pattern is: if a closure outlives the scope that created it (which is the whole point of closures), you must have a plan for when to release it.
Follow-up: In the classic loop-with-var closure problem, explain precisely WHY var causes all callbacks to print the same value, at the specification level.With var, the loop for (var i = 0; i < 3; i++) declares i in the function scope (not the block scope). There is exactly one i variable shared across all iterations. Each setTimeout callback creates a closure over that same i. By the time the callbacks execute (after the synchronous loop completes), i has been incremented to 3. All three callbacks read the same i, which is now 3. With let, the ECMAScript spec mandates that each iteration of a for loop creates a new lexical environment with a fresh binding for i. So each callback closes over a different i binding with the value at the time of that specific iteration: 0, 1, 2. The pre-ES6 workaround was an IIFE: (function(j) { setTimeout(() => console.log(j), 100); })(i) — the IIFE creates a new function scope for each iteration with its own j parameter.
Strong Answer:
  • The call stack is a LIFO data structure that tracks which function is currently executing. Each entry on the stack is an execution context. An execution context is the environment in which code is evaluated — it contains the variable environment (where var declarations live), the lexical environment (where let/const declarations live), the this binding, and a reference to the outer lexical environment (for scope chain lookups).
  • When a function is called, the engine: (1) creates a new execution context for that function, (2) pushes it onto the call stack, (3) sets up the variable environment (hoists var declarations and function declarations), (4) initializes let/const bindings (but leaves them uninitialized — the TDZ), (5) determines the this value based on how the function was called, (6) begins executing the function body line by line, (7) when the function returns (or throws), the execution context is popped off the stack.
  • The scope chain is built through lexical environments. Each execution context’s lexical environment has an “outer reference” pointing to the lexical environment of the enclosing scope. When the engine looks up a variable, it walks this chain: current environment, then outer, then outer’s outer, until it reaches the global environment. If not found, it throws a ReferenceError.
  • In practice, you encounter the call stack most directly when debugging. The call stack in Chrome DevTools shows you the chain of execution contexts. A RangeError: Maximum call stack size exceeded means you have pushed more contexts than the engine’s limit (typically around 10,000-15,000 frames in V8, though it depends on the size of each frame).
Follow-up: How does tail call optimization (TCO) relate to the call stack, and does JavaScript support it?Tail call optimization means that if a function’s last action is calling another function (a “tail position” call), the engine can reuse the current stack frame instead of pushing a new one. This would make recursive algorithms like factorial(100000) possible without blowing the stack. ES6 formally specifies TCO (called “Proper Tail Calls”), but in practice, only Safari/JavaScriptCore implements it. V8 (Chrome, Node.js) and SpiderMonkey (Firefox) chose not to implement it, citing concerns about developer tooling (stack traces become useless when frames are reused) and implicit performance cliffs. The practical implication: do not rely on TCO in JavaScript. If you need deep recursion, convert to an iterative approach with an explicit stack (an array you push to and pop from) or use trampolining (a pattern where a recursive function returns a thunk instead of calling itself, and a loop repeatedly invokes the thunks).
Strong Answer:
  • Arrow functions: use for callbacks, array method chains (.map, .filter, .reduce), and any context where you want to inherit this from the enclosing scope. They are concise, and the lexical this binding eliminates the most common class of this bugs.
  • Regular functions: use for object methods (where you need this to be the object), constructors (arrow functions cannot be used with new), and functions that need the arguments object (arrow functions do not have one).
  • Bug scenario 1 — arrow function as an object method: const obj = { name: "Alice", greet: () => this.name }. Calling obj.greet() returns undefined (or throws in strict mode) because the arrow function captures this from the enclosing scope (the module or global scope), not from obj. The fix is greet() { return this.name; } (method shorthand).
  • Bug scenario 2 — regular function as a callback inside a method: person.listFriends = function() { this.friends.forEach(function(friend) { console.log(this.name + " knows " + friend); }); }. The inner function has its own this, which is undefined in strict mode. The fix is either an arrow function (this.friends.forEach((friend) => {...})) or .bind(this).
  • Bug scenario 3 — arrow function with prototype: const Foo = () => {}; Foo.prototype is undefined. Arrow functions do not have a prototype property and cannot be used as constructors. new Foo() throws TypeError: Foo is not a constructor.
  • In practice, my rule is: arrow for lambdas, regular for anything that needs its own this or arguments or will be called with new.
Follow-up: What happens if you define a generator function as an arrow function?You cannot. There is no =>* syntax in JavaScript. Arrow functions cannot be generators. If you try const gen = *() => { yield 1; }, you get a SyntaxError. You must use function* syntax: const gen = function*() { yield 1; } or function* gen() { yield 1; }. This is a deliberate language design choice — generators need their own execution context that can be suspended and resumed, which conflicts with the lightweight, lexically-bound nature of arrow functions.
Strong Answer:
  • A higher-order function is a function that either (a) takes one or more functions as arguments, or (b) returns a function. In JavaScript, this is possible because functions are first-class values — they can be stored in variables, passed as arguments, and returned from other functions, just like numbers or strings.
  • Common higher-order functions: Array.prototype.map (takes a transform function), Array.prototype.filter (takes a predicate function), setTimeout (takes a callback), addEventListener (takes a handler). Factory functions like debounce and throttle are higher-order because they accept a function and return a new function with modified behavior.
  • A basic map implementation:
Array.prototype.myMap = function(callback, thisArg) {
  const result = new Array(this.length);
  for (let i = 0; i < this.length; i++) {
    // Check for sparse array holes -- map should preserve them
    if (i in this) {
      result[i] = callback.call(thisArg, this[i], i, this);
    }
  }
  return result;
};
  • The key details that separate a strong answer: (1) callback.call(thisArg, ...) — the real map accepts an optional thisArg as the second argument to map, not just the callback. (2) The i in this check handles sparse arrays — [1, , 3].map(x => x * 2) should produce [2, empty, 6], not [2, undefined, 6]. (3) The callback receives three arguments: the current element, the index, and the original array. (4) map always returns a new array of the same length — it does not mutate the original.
  • Performance nuance: in a hot path iterating over 100,000 elements, .map().filter().reduce() creates two intermediate arrays. A single for loop or a single .reduce() that combines all operations is more memory-efficient. But for typical use cases (hundreds to low thousands of elements), the clarity of chaining wins over the micro-optimization.
Follow-up: What is the difference between .map() and .forEach()? Can you use .map() everywhere you would use .forEach()?.map() returns a new array with the transformed values. .forEach() returns undefined and is used purely for side effects (logging, DOM manipulation, pushing to an external array). You can technically use .map() where you would use .forEach(), but it is a code smell: you are creating and discarding an array for no reason, which signals to other developers that the return value matters when it does not. Linting rules like no-unused-expressions or array-callback-return will flag this. The reverse is not true: you cannot use .forEach() where you need .map() because .forEach() does not return the transformed array. Also, .forEach() cannot be short-circuited (no break equivalent), while a for...of loop can.
Strong Answer:
  • An IIFE (Immediately Invoked Function Expression) is a function that is defined and executed in one step: (function() { ... })(). The outer parentheses force JavaScript to treat the function keyword as an expression (not a declaration), and the trailing () immediately invoke it.
  • Before ES6, JavaScript had no module system and only function-level scope (no block scope). The only way to create a private scope and avoid polluting the global namespace was to wrap code in a function. Every major pre-ES6 library (jQuery, Lodash, Backbone, Angular 1.x) was wrapped in an IIFE. The revealing module pattern — const module = (function() { let private = 0; return { getPrivate: () => private }; })() — was the standard way to achieve encapsulation.
  • Modern valid use cases still exist: (1) In scripts that are not ES modules (plain <script> tags without type="module"), an IIFE still provides scope isolation. (2) When you need to await at the top level in an environment that does not support top-level await, you use an async IIFE: (async () => { const data = await fetch(...); })(). (3) In some build tool configurations and legacy codebases, IIFEs are still the output format for bundled code.
  • In a modern ES module environment with let/const, IIFEs are rarely needed. Block scoping with {} and let/const provides the same isolation that IIFEs provided with var. If you see an IIFE in modern code, it is usually either legacy or the async IIFE pattern.
Follow-up: What is the difference between (function(){})() and (function(){}()) — and does it matter?Both work identically. The first invokes the function outside the grouping parentheses; the second invokes it inside. Douglas Crockford (of “JavaScript: The Good Parts” fame) preferred the inner invocation style, arguing it makes the intent clearer by keeping the invocation visually inside the expression. In practice, it makes zero functional difference — the parser handles both the same way. Most modern code uses the first style. The truly important detail is the outer parentheses: without them, function(){}() is a syntax error because JavaScript sees function at the start of a statement and expects a declaration (which requires a name).