Engineering Notes
Home/Languages/JavaScript/Functions
Languages · JavaScript

Functions

Default parameters are evaluated at call time, left to right, and can reference earlier parameters:

💻 Languages● Beginner⏱ 6 min read🛠️ 3 exercises🎤 5 interview Q&As

🎯 What you'll learn

  • Four Ways to Create a Function
  • Default Parameters
  • Rest Parameters vs arguments
  • Named Function Arguments via Destructuring
  • Function length and name

1Four Ways to Create a Function

js
function declared(a, b) { return a + b; }        // 1. Function declaration — hoisted whole

const expressed = function (a, b) { return a + b; }; // 2. Function expression — not hoisted

const named = function sum(a, b) { return a + b; };  // 3. Named function expression —
                                                       //    `sum` is only visible INSIDE its own body (useful for recursion)

const arrow = (a, b) => a + b;                    // 4. Arrow function — no own `this`, no `arguments`, cannot be a constructor
Hoisted fullyHas own thisHas argumentsCan be new'd
Declaration✅✅✅✅
Expression❌✅✅✅
Arrow❌❌ (lexical)❌ (lexical)❌

2Default Parameters

js
function greet(name = "friend", punctuation = "!") {
  return `Hello, ${name}${punctuation}`;
}
greet();               // "Hello, friend!"
greet("Ana");           // "Hello, Ana!"
greet(undefined, "?"); // "Hello, friend?" — undefined triggers the default, but null does NOT

Default parameters are evaluated at call time, left to right, and can reference earlier parameters:

js
function makeRange(start, end = start + 10) { return [start, end]; }
makeRange(5); // [5, 15]

3Rest Parameters vs arguments

js
function sum(...numbers) {      // rest parameter — a REAL array
  return numbers.reduce((a, b) => a + b, 0);
}
sum(1, 2, 3); // 6

function legacySum() {
  return Array.from(arguments).reduce((a, b) => a + b, 0); // arguments is array-LIKE, not a real array
}

Prefer rest parameters (...args) over arguments: they're real arrays (all array methods work directly), they can be combined with named parameters cleanly, and arrow functions don't have arguments at all.

4Named Function Arguments via Destructuring

js
function createUser({ name, age, isAdmin = false }) {
  return { name, age, isAdmin };
}
createUser({ name: "Ana", age: 30 }); // order doesn't matter, defaults still apply

This pattern replaces long positional parameter lists and self-documents each call site.

5Function length and name

js
function example(a, b, c = 1, ...rest) {}
example.length; // 2 — counts only parameters BEFORE the first default or rest parameter
example.name;   // "example"

const anon = function () {};
anon.name; // "anon" — inferred from the variable it's assigned to

6IIFE (Immediately Invoked Function Expression)

js
(function () {
  console.log("runs immediately, creates an isolated scope");
})();

(() => {
  console.log("arrow IIFE form");
})();

Historically used for module-style encapsulation before ES modules existed; still occasionally used to avoid polluting scope in scripts.

7Pure Functions vs Side Effects

A pure function always returns the same output for the same input and causes no observable side effects (no mutation of external state, no I/O).

js
// Pure
function add(a, b) { return a + b; }

// Impure — depends on and mutates external state
let total = 0;
function addToTotal(n) { total += n; return total; }

Pure functions are easier to test, memoize, and reason about — prefer them wherever practical.

Under the HoodHow a Function Actually Gets Executed▾

A function isn't compiled to fast machine code the instant it's defined — engines use a tiered execution pipeline that starts slow and general, then progressively specializes hot functions based on how they're actually observed running.

The tiers, roughly

Bytecode interpreter warms up to a baseline compiler, then to optimized machine code, with deoptimization falling back
Bytecode interpreter warms up to a baseline compiler, then to optimized machine code, with deoptimization falling back
text
source code
   │  parse
   ▼
bytecode  ──(interpreted by Ignition)──►  runs immediately, no compile delay, moderate speed
   │  function called often ("warm")
   ▼
baseline compiled machine code (Sparkplug)  ──►  faster, still fairly general
   │  function called VERY often ("hot"), with STABLE argument/shape patterns
   ▼
optimizing compiler output (Maglev / TurboFan)  ──►  highly specialized machine code,
                                                        assumes the shapes/types it has seen so far

(Tier names above are V8-specific; other engines use different internal names for a broadly similar warm-up pipeline.) Every function starts at the top of this ladder and only climbs higher if it's actually called enough times to be worth the extra compilation cost — a function called once, ever, never needs to be more than bytecode.

Optimized code makes assumptions — and can be thrown away

The optimizing compiler bakes in assumptions based on what it's observed: "this parameter has always been a SMI," "this object has always had hidden class X" (see the Prototypes topic). If a later call violates one of those assumptions, the engine performs a deoptimization ("bailout") — it discards the optimized machine code for that function and falls back to the slower interpreted/baseline tier, at least temporarily, until it can re-specialize.

js
function add(a, b) { return a + b; }

for (let i = 0; i < 100000; i++) add(1, 2);       // called with numbers every time -> gets optimized for numbers
add("1", "2");                                       // suddenly called with strings -> can trigger a deopt

This is the deeper mechanical reason consistency matters for performance: a function called the same way, with the same argument shapes, every time, is the ideal case for this pipeline. A function called with wildly different argument types call-to-call forces the engine to either generalize its optimized code (slower than the specialized version) or repeatedly deoptimize and re-optimize.

arguments, rest params, and defaults are real bytecode operations

None of section 2-4's conveniences are "magic" — they compile to concrete bytecode instructions executed at function entry: default parameters become a conditional check-and-assign per parameter; rest parameters become a loop that copies the remaining actual arguments into a freshly allocated array object; the legacy arguments object is itself a special array-like object the engine has to construct (historically expensive enough that using it can prevent certain optimizations ...rest doesn't — part of why rest parameters are preferred in modern code).

A function is itself an object

example.length and example.name (section 5) aren't special syntax — they're ordinary, non-enumerable own properties sitting on the function object itself, which has its own hidden class exactly like any other object (Objects topic), plus an internal [[Call]] (and sometimes [[Construct]]) capability that makes it invocable.

⚠️ Common Pitfalls

  1. Using arguments inside an arrow function — arrows have no arguments object; they inherit it lexically from the enclosing regular function (or it's undefined at the top level), causing confusing bugs.
  2. function.length ignoring rest/default parameters — don't rely on it to count "how many args this function accepts" once defaults or rest params are involved.
  3. Passing null expecting a default parameter to kick in — defaults only trigger for undefined, not null.
  4. Forgetting default parameters are re-evaluated every call — function f(arr = []) {} gives a fresh array every call, not a shared one (this is actually the safe/correct behavior, but surprises people used to Python's mutable-default-argument bug).

📋 Quick Reference

text
function f(a, b = 1, ...rest) {}
f.length          → 1 (params before first default/rest)
...rest            → real array of remaining arguments
{a, b} destructure → named-argument style calls
arrow functions    → no this, no arguments, not constructible

🎤 Interview Q&A

Questions interviewers ask about this topic. Answer out loud first, then open a card to compare.

Q1Function declarations vs expressions vs arrow functions?core▾

Declarations are hoisted and named; expressions are assigned to variables (not hoisted as values); arrow functions are concise, have lexical this/arguments, and can't be constructors.

Follow-ups interviewers ask
  • When not to use arrows? Object methods needing this, constructors, generators.
Q2What are default, rest and named parameters?core▾

Defaults: function f(a = 1). Rest: function f(...args) gathers remaining args into an array. Named parameters via destructuring an options object: function f({ timeout = 1000, retries = 3 } = {}).

Follow-ups interviewers ask
  • Why options objects? Readable calls and easy extension.
Q3What are pure functions and why do they matter?core▾

Functions whose output depends only on inputs and which have no side effects. They're easy to test, cache and reason about; React components and reducers rely on purity.

Follow-ups interviewers ask
  • Side effect examples? Mutating arguments, I/O, Date.now(), random.
Q4What is currying and partial application?senior▾

Currying transforms f(a, b, c) into f(a)(b)(c); partial application fixes some arguments (bind or wrappers). Useful for configuring reusable functions.

Follow-ups interviewers ask
  • Example? const log = level => msg => console.log(level, msg).
Q5What is an IIFE and is it still used?core▾

An Immediately Invoked Function Expression (() => { … })() created private scope before modules and block scoping. Today it's mostly used for top-level async code in scripts.

Follow-ups interviewers ask
  • Modern replacement? ES modules and block scope.

🛠️ Practice exercises

Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.