JavaScript Functions: Declarations, Arrows, Defaults, and First-Class Power
If you learn one thing about JavaScript deeply, make it functions. Not because the syntax is hard โ it isnโt โ but because JS is built on a premise many languages only bolted on later: functions are ordinary values. Theyโre assigned to variables, passed as arguments, returned from other functions, and stored in arrays and objects, exactly like numbers and strings.
Every distinctive JS pattern โ callbacks, event handlers, array methods, promises, middleware, React components โ is this one premise wearing different outfits. This guide covers the syntax forms and their real differences, parameter machinery, and the higher-order patterns that make JS code look the way it does.
Three Ways to Write a Function
// 1. Function declarationfunction add(a, b) { return a + b;}
// 2. Function expression (assigned to a variable)const subtract = function (a, b) { return a - b;};
// 3. Arrow function (ES2015 โ now the workhorse)const multiply = (a, b) => a * b;All three create callable function objects. The differences that actually matter:
Hoisting. Declarations are fully hoisted โ callable anywhere in their scope, even above the definition, which lets you organize files top-down (main flow first, helpers below). Expressions and arrows live under variable rules: callable only after the assignment line.
this behavior. The big one: arrows donโt have their own this โ they inherit it from the surrounding scope, while function functions get this determined at call time. This single difference is why callbacks inside classes/objects historically broke, and why arrows fixed them. It deserves โ and gets โ a whole guide; until then: arrows for callbacks is a safe default.
Arrows are expressions all the way down, with syntax that shrinks to fit:
const square = (x) => x * x; // one param โ parens optional: x => x * xconst rand = () => Math.random(); // no params โ parens requiredconst clamp = (x, lo, hi) => // multiple params Math.min(Math.max(x, lo), hi);
const bigger = (x) => { // block body โ 'return' required again const doubled = x * 2; return doubled + 1;};
const makeUser = (name) => ({ name, active: true }); // returning an object literal: // wrap in parens, or the braces // parse as a block!That last one is a real-world gotcha: => { name: name } is a block containing a label, silently returning undefined. The wrapping parens => ({ ... }) are mandatory for object-literal bodies.
Which to use? A sane modern default: function declarations for top-level named operations (hoisting + clearer stack traces + syntactic weight matching their importance), arrows for callbacks and small inline logic. Teams vary; consistency beats doctrine.
Parameters: Defaults, Rest, and the Absence of Overloading
JavaScript functions accept any number of arguments regardless of the parameter list โ missing ones are undefined, extras are ignored. No compile error, no overloading. The modern toolkit turns this looseness into expressiveness:
Default parameters โ evaluated per call, only when the argument is undefined:
function connect(host, port = 5432, retries = 3) { // ...}connect("db.local"); // port 5432, retries 3connect("db.local", 6543); // retries 3connect("db.local", undefined, 5); // explicit undefined triggers the defaultconnect("db.local", null, 5); // null does NOT โ port is null. ?? mindset applies.Rest parameters โ gather remaining arguments into a real array:
function logAll(prefix, ...items) { // items is a genuine Array for (const item of items) console.log(prefix, item);}logAll(">>", "a", "b", "c");(If you meet the older arguments object in legacy code: itโs the pre-2015 version of this โ array-like, not an array, and absent in arrows. Rest parameters replaced it.)
Destructured parameters โ the modern answer to โtoo many positional argumentsโ:
function createUser({ name, email, role = "viewer", active = true }) { // ...}createUser({ name: "Ada", email: "ada@x.com" }); // self-documenting call siteOnce a function needs more than two or three arguments โ especially same-typed ones that are easy to swap โ an options object with destructuring is strictly better: named at the call site, order-independent, defaults per field. This pattern is ubiquitous in modern APIs and covered mechanically in destructuring.
One discipline the language wonโt enforce for you: functions should return consistent types. A function that returns a number usually but false on failure (an old JS idiom) pushes type-checking burdens onto every caller. Return null for absence, throw for errors, or return a result object โ pick one contract and keep it.
Functions as Values: The Load-Bearing Idea
Because functions are values, you can build with them:
// stored in data structuresconst operations = { add: (a, b) => a + b, sub: (a, b) => a - b,};operations.add(2, 3); // 5 โ a lookup table of behavior
// passed as arguments โ the callback pattern[3, 1, 2].sort((a, b) => a - b); // sort takes a comparison FUNCTIONbutton.addEventListener("click", handleClick); // events take handler FUNCTIONS
// returned from functions โ function factoriesconst makeGreeter = (greeting) => (name) => `${greeting}, ${name}!`;const hello = makeGreeter("Hello");const namaste = makeGreeter("Namaste");hello("Ada"); // "Hello, Ada!"namaste("Arun"); // "Namaste, Arun!"Functions that take or return functions are called higher-order functions, and theyโre not an advanced topic in JS โ theyโre Tuesday. The three patterns above cover most usage:
- Behavior tables replace switch statements: dispatch by key, extend by adding an entry.
- Callbacks parameterize an algorithm with a step you supply โ the JS pattern.
sortknows how to sort; you tell it how to compare.mapknows how to traverse; you tell it how to transform. Every event API, array method, and async interface works this way. - Factories produce configured functions โ possible because returned functions remember the variables they were created with (
greetingabove). That memory is a closure, important enough to be its own chapter; here, just notice it works.
A practical corollary many beginners miss โ pass the function, donโt call it:
button.addEventListener("click", handleClick()); // BUG: calls now, passes the resultbutton.addEventListener("click", handleClick); // right: passes the function itselfbutton.addEventListener("click", () => handleClick(id)); // right: wrap when you need argsThe bare name is the value; parentheses invoke. This one confusion produces a steady stream of โmy handler runs immediately on page loadโ questions.
Pure Functions and Side Effects: A Vocabulary Worth Adopting Early
A pure function computes its return value from its inputs and does nothing else โ no mutation of outside state, no I/O, same inputs always same output:
const withTax = (paise, rate) => Math.round(paise * (1 + rate)); // pure
let total = 0;const addToTotal = (x) => { total += x; }; // impure: mutates outsidePurity isnโt a purity test โ real programs need side effects (rendering, network, storage). The engineering value is segregation: keep calculation pure and push effects to the edges. Pure functions are trivially testable (call, assert โ no setup, no mocks), safe to reorder and cache, and immune to spooky action at a distance. Frameworks have converged on this: Reactโs render logic, reducers, and most โbusiness logicโ layers are expected to be pure, with effects quarantined in designated places. Building the habit now โ โcould this function be pure?โ โ pays compounding dividends.
Related habit: name functions after what they return or do (formatPrice, isEligible, fetchUser โ verb or predicate), keep them small enough to name honestly, and extract when a name would need โand.โ
IIFEs and Immediate Invocation, Briefly
Legacy code is full of this shape:
(function () { var private = "hidden"; // ... whole libraries lived inside these})();An IIFE (immediately-invoked function expression) โ define and call in one breath โ was the pre-2015 way to create a private scope, since var had no block scope and there were no modules. Today, block scope and ES modules cover those needs, and the surviving modern use is small: initializing a const that needs multi-statement logic, now often written as an arrow: const config = (() => { ...; return result; })();. Recognize the pattern; rarely write it.
Frequently Asked Questions
When must I NOT use an arrow function? When you need a dynamic this: object methods defined with arrows canโt see their own object, and DOM handlers that rely on this being the element break. Also: arrows canโt be constructors and have no arguments. Full rules in the this guide โ the summary is arrows for callbacks, method shorthand for methods.
Are there truly private variables without classes? Yes โ closure over function scope is the original privacy mechanism (makeCounter-style factories), and itโs airtight. Classes later added #private fields; both are legitimate. The closures chapter builds this out.
Whatโs the difference between parameters and arguments? Parameters are the names in the definition; arguments are the values at the call. Sloppy usage is universal and harmless, but the distinction makes the defaults/rest rules crisper.
Can I overload by arity like Java? No โ one function per name; later declarations overwrite. The idioms instead: default parameters, options objects, or explicit branching on typeof/argument count inside one function. The options-object pattern scales best and dominates modern APIs.
How many parameters is too many? By the time you hit three โ especially with same-typed values (resize(300, 400, 200) โ which is which?) โ switch to a destructured options object. Future readers of call sites will thank you.
Naming and Shaping Functions: A Review-Room Consensus
A few conventions that recur in strong codebases, worth adopting before habits calcify:
Predicates read as questions: isEligible, hasAccess, canRetry โ and they return actual booleans (return count > 0, not return count && flag which returns a number-or-boolean cocktail). Actions read as verb-phrases (sendInvoice, buildReport); getters read as nouns or get-phrases (totalPaise, getConfig). A function whose honest name would contain โandโ (validateAndSave) is two functions in a trench coat.
Arity discipline: zero and one-argument functions compose best (notice how pipe in the self-check only works because each step takes one value). When a function grows optional knobs, they go in a trailing options object, never as a fourth positional boolean โ fetchUsers(true, false, true) is unreviewable; fetchUsers({ includeInactive: true }) documents itself at every call site.
Early returns beat arrow-shaped nesting: guard clauses first (if (!user) return null;), happy path unindented at the bottom. JSโs truthiness tools make guards terse โ use that.
Default to returning, not mutating โ the pure-function principle applied at the smallest scale: withDiscount(order) returning a new object beats applyDiscount(order) editing in place, because the caller controls what changes and the spread idioms make copies cheap to express.
None of these are enforced by the language โ thatโs the point. JS gives functions maximal freedom; teams that thrive impose the structure themselves, consistently.
Self-Check
// 1. What prints?const f = (x = 2, y = x * 2) => x + y;console.log(f()); // ?console.log(f(5)); // ?
// 2. What's wrong here?const getConfig = () => { env: "prod" };console.log(getConfig()); // ?
// 3. Spot the bug:setTimeout(cleanup(), 1000);
// 4. What does this build?const pipe = (...fns) => (x) => fns.reduce((acc, fn) => fn(acc), x);const shout = pipe((s) => s.trim(), (s) => s.toUpperCase(), (s) => s + "!");console.log(shout(" hey ")); // ?Answers: (1) 6 and 15 โ later defaults can use earlier parameters. (2) prints undefined โ the braces parse as a block, env: as a label; needs => ({ env: "prod" }). (3) cleanup runs immediately and its return value (probably undefined) is scheduled; drop the parens or wrap in an arrow. (4) "HEY!" โ pipe is a higher-order factory composing functions left to right; if you followed it, youโre reading real-world functional JS already.
Next up, the concept that makes function factories and private state possible โ and the most asked-about topic in JavaScript interviews: Closures.