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 —
varshares 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:
newbinding —new Foo()→thisis the new object.- Explicit binding —
f.call(obj),f.apply(obj),f.bind(obj)→thisisobj. - Implicit binding —
obj.f()→thisisobj. - Default — plain
f()→thisisundefined(strict) or global (sloppy). - 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: new → explicit (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).