Topics in this subject
JavaScript 8 min read Updated 5 Aug 2026

Functions

Ways to define, Arrow vs regular — the differences that matter 🎯, Parameters: defaults, rest, destructuring, arguments, Higher-order functions, callbacks, pure/impure

Functions are first-class values: assign them, pass them, return them.

Ways to define#

function decl(a, b) { return a + b; }          // declaration — hoisted
const expr = function (a, b) { return a + b; }; // expression — not hoisted
const arrow = (a, b) => a + b;                  // arrow — concise, lexical this
const named = function fac(n){ return n<2?1:n*fac(n-1); }; // named expr (self-ref)
(function(){ console.log("IIFE"); })();          // runs immediately

Arrow vs regular — the differences that matter 🎯#

Regular function Arrow function
this dynamic (depends how it's called) lexical (inherits from enclosing scope)
arguments object yes no (use rest ...args)
Usable as constructor (new) yes no
Has own prototype yes no
Hoisted if declaration no
Can be generator yes no
const obj = {
  name: "X",
  regular() { return this.name; },     // "X"
  arrow: () => this.name,              // undefined — this is the outer scope
};

🟢 Use arrows for callbacks and when you want to keep the surrounding this (e.g., inside a class method's setTimeout). Use regular functions for object methods and anything used with new.

Named vs anonymous functions#

function greet() {}               // named function DECLARATION
const a = function () {};         // anonymous function expression
const b = function fac(n) {};     // NAMED function expression
const c = () => {};               // arrow — always anonymous (name inferred from const)

Why a name helps:

  • Self-reference / recursion — a named function expression can call itself by that name even if the variable is later reassigned: const f = function fac(n){ return n < 2 ? 1 : n * fac(n - 1); };.
  • Stack traces — named functions show a useful name in errors and the debugger; anonymous ones show <anonymous>.
  • Hoisting — only function declarations are hoisted; expressions (named or arrow) are not.

Arrows are always anonymous, but JS infers a name from the binding (const foo = () => {}foo.name === "foo"). They can't be named expressions and can't recurse by an internal name.

Parameters: defaults, rest, destructuring#

function connect({ host = "localhost", port = 5432 } = {}) { /* ... */ }
connect();                     // uses all defaults thanks to `= {}`
function log(level, ...msgs) { }  // rest gathers remaining args

⚠️ Default params are evaluated at call time, left to right, and can reference earlier params: (a, b = a * 2) => {}.

arguments#

Legacy array-like (not a real array) available in non-arrow functions. 🟢 Prefer rest params ...args, which give a real array.

First-order vs higher-order functions 🎯#

  • First-order function — neither takes a function as an argument nor returns one. It just works on data.
  • Higher-order function (HOF) — does at least one of: takes a function as an argument, or returns a function. Only possible because functions are first-class values.
// first-order — plain data in, data out
const square = n => n * n;

// higher-order — RECEIVES a function (a callback)
function applyTwice(fn, x) { return fn(fn(x)); }
applyTwice(square, 2);              // 16

// higher-order — RETURNS a function
function multiplier(factor) {
  return n => n * factor;          // returned fn closes over `factor`
}
const triple = multiplier(3);      // triple is itself a function
triple(10);                        // 30

map, filter, reduce, forEach, setTimeout, addEventListener, debounce, and bind are all higher-order — they take a function.

Callbacks — a function you hand off to be called later 🎯#

A callback is a function passed into another function so it can be called back at the right moment — now (synchronously) or later (asynchronously).

// SYNC callback — invoked during the call
[1, 2, 3].map(n => n * 2);                 // the arrow is the callback

// ASYNC callback — invoked later, after the current task finishes
setTimeout(() => console.log("1s later"), 1000);
button.addEventListener("click", (e) => console.log("clicked"));

Interconnected example — first-order + higher-order + callbacks in one flow. A tiny pipeline: keep active users, format their names, then print — each stage is a callback handed to a higher-order array method:

const users = [
  { name: "Ada", active: true },
  { name: "Bob", active: false },
  { name: "Cy",  active: true },
];

const isActive = u => u.active;              // first-order predicate
const toName   = u => u.name.toUpperCase();  // first-order transform

const activeNames = users
  .filter(isActive)   // HOF takes the `isActive` callback → [Ada, Cy]
  .map(toName);       // HOF takes the `toName` callback   → ["ADA", "CY"]

activeNames.forEach(name => console.log(name)); // HOF + callback: the side-effect stage

isActive / toName are plain first-order functions; filter / map / forEach are higher-order functions that call those callbacks for you. Composing small functions like this is the heart of functional-style JS.

⚠️ Callback hell — nesting async callbacks deeply (the "pyramid of doom") with error handling at every level is exactly why Promises and async/await exist (see Asynchronous JavaScript).

Pure vs impure functions#

  • Pure — same input → same output, no side effects. Easy to test, cache (memoize), and reason about.
  • Impure — reads or changes outside state (I/O, Date.now(), mutation, DOM, network).
const double = x => x * 2;                    // ✅ pure
let total = 0;
const addToTotal = x => { total += x; };      // ❌ impure — mutates outer state
const stampedLog = m => console.log(Date.now(), m); // ❌ impure — clock + I/O

Closures — the crown jewel 🎯#

ELI12. A closure is a function that remembers the variables from where it was born, even after that place has finished running. Like a backpack the function carries around with the outer variables inside.

function counter() {
  let count = 0;                 // private state
  return () => ++count;          // inner fn closes over `count`
}
const next = counter();
next(); // 1
next(); // 2  — `count` survived because the returned fn still references it

Why it exists / when to use: data privacy (module pattern), function factories, memoization, event handlers with retained state, currying.

graph LR
    subgraph "counter() scope (kept alive)"
    C["count = 2"]
    end
    N["next (the returned function)"] -->|closes over| C

⚠️ Closure traps:

  • The loop bug in Variables — var shares one binding.
  • Closures keep their entire scope reachable → memory leaks if a long-lived closure holds a big object it no longer needs.

Lexical environment#

Each execution creates a lexical environment: a record of local variables + a reference to the parent environment. The chain of these is the scope chain; closures are simply functions holding onto their lexical environment.

this — the four rules 🎯#

this is decided by how a function is called, not where it's defined (except arrows). In order:

  1. new bindingnew Foo()this is the new object.
  2. Explicit bindingf.call(obj), f.apply(obj), f.bind(obj)this is obj.
  3. Implicit bindingobj.f()this is obj.
  4. Default — plain f()this is undefined (strict) or global (sloppy).
  5. Arrow — ignores all the above; uses the enclosing lexical this.
function who() { return this?.name; }
const a = { name: "A", who };
a.who();                 // "A"  (implicit)
const loose = a.who;
loose();                 // undefined (default) ⚠️ lost `this`
who.call({ name: "C" }); // "C"  (explicit)

⚠️ Passing a method as a callback (setTimeout(a.who, 0)) loses this. Fix with .bind(a), an arrow wrapper, or a class field arrow.

call, apply, bind#

f.call(thisArg, a, b);     // invoke now, args listed
f.apply(thisArg, [a, b]);  // invoke now, args as array
const g = f.bind(thisArg, a); // returns a NEW function, partially applied

Currying, partial application, composition, memoization#

// currying: f(a)(b)(c)
const curry = f => a => b => c => f(a, b, c);

// partial application: fix some args now
const add = (a, b) => a + b;
const add10 = add.bind(null, 10);   // add10(5) → 15

// composition: pipe data through functions
const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x);
const pipe    = (...fns) => x => fns.reduce((v, f) => f(v), x);
const shout = pipe(s => s.trim(), s => s.toUpperCase(), s => s + "!");
shout("  hi ");  // "HI!"

// memoization: cache by args
function memoize(fn) {
  const cache = new Map();
  return (...args) => {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key);
    const val = fn(...args);
    cache.set(key, val);
    return val;
  };
}

Generators & async functions (intro; more in Advanced JavaScript / Asynchronous JavaScript)#

function* ids() { let i = 0; while (true) yield i++; }
const gen = ids();
gen.next().value;  // 0
gen.next().value;  // 1  — generators pause at yield and resume on next()

async function load() { const r = await fetch("/api"); return r.json(); }

Interview Q&A#

Q1. First-order vs higher-order function? A first-order function neither takes nor returns a function — it just processes data. A higher-order function takes a function as an argument and/or returns one (map, filter, bind, a function factory). Possible because functions are first-class values.

Q2. What is a callback? Give a sync and an async example. A function passed to another function to be invoked later. Sync: the arrow in [1,2].map(x => x*2) (called during the call). Async: setTimeout(fn, 1000) or an event handler (called after the current task).

Q3. Arrow vs regular function — the differences that matter? Arrows have lexical this (inherited from the enclosing scope), no arguments, can't be used with new, have no prototype, and can't be generators. Regular functions get a dynamic this based on how they're called. Use arrows for callbacks; regular functions for object methods and constructors.

Q4. Named vs anonymous function — why name one? A named function expression can recurse by its own name and shows a readable name in stack traces; anonymous functions appear as <anonymous>. Arrows are anonymous (name inferred from the variable).

Q5. What is a closure? Give a real use. A function that keeps access to variables from the scope where it was defined, even after that scope has returned. Uses: private state (module pattern), function factories, memoization, and stateful event handlers.

Q6. What are the four this binding rules, and how do arrows differ? Precedence: newexplicit (call/apply/bind) → implicit (obj.f()) → default (undefined in strict, global otherwise). Arrows ignore all four and use the enclosing lexical this.

Q7. call vs apply vs bind? call(thisArg, a, b) invokes now with listed args; apply(thisArg, [a, b]) invokes now with an args array; bind(thisArg, ...) returns a new function with this (and any args) permanently fixed — it does not invoke.

Q8. Predict: a method detached and called standalone.

const a = { name: "A", who() { return this?.name; } };
const loose = a.who;
loose();   // undefined — lost implicit `this` (plain call → default binding)

Fix with a.who.bind(a) or an arrow wrapper.

Q9. Pure vs impure function? Pure: same input → same output, no side effects (testable, cacheable). Impure: depends on or mutates outside state (I/O, Date.now(), DOM, mutation).

Q10. Implement once(fn) (runs at most once) using a closure.

function once(fn) {
  let called = false, result;
  return (...args) => {
    if (!called) { called = true; result = fn(...args); }
    return result;
  };
}

Practice: implement curry, debounce, throttle, and memoize from scratch; build a private counter module using closures (increment, decrement, value).