Episode 1 — Fundamentals / 1.20 — Functions

1.20.a — Function Declaration vs Expression

In one sentence: JavaScript offers several ways to define functions — declarations are hoisted so you can call them before the line they appear on, while expressions (including named, anonymous, and IIFE) are not hoisted and give you finer control over when and where a function exists.

Navigation: ← 1.20 Overview · 1.20.b — Parameters and Arguments →


1. Function declaration syntax

A function declaration (also called a function statement) uses the function keyword followed by a name, parentheses for parameters, and a block body:

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

console.log(greet("Alice")); // "Hello, Alice!"

Key traits:

  • Must have a name (the identifier after function).
  • Creates a variable with that name in the current scope.
  • The entire function is hoisted to the top of its scope (see Section 4).

2. Function expression syntax

A function expression assigns a function to a variable (or passes it inline). The function itself can be anonymous or named:

// Anonymous function expression
const add = function (a, b) {
  return a + b;
};

// Named function expression
const subtract = function subtractFn(a, b) {
  return a - b;
};

console.log(add(3, 4));       // 7
console.log(subtract(10, 3)); // 7

Anonymous vs named expressions

AnonymousNamed
Syntaxconst fn = function() {}const fn = function myFn() {}
Stack tracesShows (anonymous) or the variable nameShows myFn — easier debugging
Self-referenceMust use the outer variable name for recursionCan call itself by myFn inside the body
Outer accessVia the variable (fn)The internal name (myFn) is not accessible outside

Best practice: Prefer named function expressions when you need recursion or clearer stack traces.

// Named expression — the name is only visible inside
const factorial = function fact(n) {
  if (n <= 1) return 1;
  return n * fact(n - 1); // 'fact' works here
};

console.log(factorial(5)); // 120
// console.log(fact);      // ReferenceError — not in outer scope

3. Functions as first-class citizens

In JavaScript, functions are values — just like numbers or strings. This means you can:

3a. Assign a function to a variable

const sayHi = function () {
  console.log("Hi!");
};

3b. Pass a function as an argument

function runTwice(fn) {
  fn();
  fn();
}

runTwice(sayHi);
// "Hi!"
// "Hi!"

3c. Return a function from another function

function multiplier(factor) {
  return function (num) {
    return num * factor;
  };
}

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

console.log(double(5));  // 10
console.log(triple(5));  // 15

3d. Store functions in data structures

const operations = {
  add: (a, b) => a + b,
  sub: (a, b) => a - b,
};

console.log(operations.add(2, 3)); // 5

This "first-class" nature is the foundation of callbacks, higher-order functions, and functional programming patterns in JavaScript.


4. Hoisting — the critical difference

Hoisting is JavaScript's behavior of moving declarations to the top of their scope before code runs.

Declarations ARE hoisted (fully)

// This works — greet is available before the declaration line
console.log(greet("Bob")); // "Hello, Bob!"

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

The engine effectively sees:

// Hoisted to top
function greet(name) {
  return `Hello, ${name}!`;
}

console.log(greet("Bob")); // "Hello, Bob!"

Expressions are NOT hoisted

// ReferenceError (with const/let) or TypeError (with var)
console.log(add(2, 3)); // ERROR!

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

With var, the variable is hoisted but its value is undefined until assignment:

console.log(typeof add); // "undefined"
console.log(add(2, 3));  // TypeError: add is not a function

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

Hoisting comparison table

Function DeclarationFunction Expression (const)Function Expression (var)
Hoisted?Yes — fullyNoReferenceError in TDZVariable hoisted as undefined
Callable before definition?YesNoNoTypeError
ScopeFunction or globalBlockFunction or global

5. When to use which — guidelines

ScenarioRecommendedWhy
Utility/helper that the whole file needsDeclarationHoisting lets you place helpers at bottom, main logic at top
Callback passed inlineExpression (often arrow)No need for a standalone name
Conditional function creationExpressionDeclarations inside if blocks behave inconsistently across engines
Immediately executed setupIIFERuns once, doesn't pollute scope
Method on an objectExpression or shorthandAssigned to a property

Consistency matters more than the choice itself. Pick a team convention and stick with it.


6. IIFE — Immediately Invoked Function Expression

An IIFE is a function that runs as soon as it is defined:

(function () {
  const secret = "hidden";
  console.log("IIFE ran!");
})();

// console.log(secret); // ReferenceError — not in outer scope

Syntax patterns

// Classic — wrapping parentheses
(function () { /* ... */ })();

// Alternative — parentheses around the call
(function () { /* ... */ }());

// With arrow function
(() => {
  console.log("Arrow IIFE");
})();

// Named IIFE (for stack traces)
(function setup() {
  console.log("Named IIFE");
})();

Why use IIFE?

  1. Scope isolation — variables declared inside do not leak to global scope.
  2. Module pattern (pre-ES6) — creating private variables with a public API:
const counter = (function () {
  let count = 0; // private

  return {
    increment() { count++; },
    decrement() { count--; },
    getCount()  { return count; },
  };
})();

counter.increment();
counter.increment();
console.log(counter.getCount()); // 2
// console.log(count);            // ReferenceError
  1. Avoiding global pollution in scripts that share a page.
  2. Async IIFE for top-level await in older environments:
(async function () {
  const data = await fetch("/api/data");
  console.log(await data.json());
})();

Modern note: With ES modules (import/export), each file already has its own scope, so IIFEs are less common. But they still appear in bundled code, polyfills, and legacy codebases.


7. Function constructor (rare)

JavaScript also lets you create functions with the Function constructor:

const add = new Function("a", "b", "return a + b");
console.log(add(2, 3)); // 5

Why you should almost never use this:

  • The body is a string — no syntax highlighting, no linting, no static analysis.
  • Similar security risks to eval() — can execute arbitrary code.
  • Functions created this way do not close over local scope — they always run in the global scope.
  • Significantly slower to parse than normal functions.

When it appears: Dynamic code generation in templating engines, some meta-programming scenarios. Mention for completeness; avoid in production code.


8. Real examples — hoisting behavior differences

Example 1: Organizing code with declarations

// Main logic at the top — reads like a story
const result = calculateTotal(100, 0.08);
const formatted = formatCurrency(result);
console.log(formatted); // "$108.00"

// Helper declarations at the bottom — hoisted
function calculateTotal(price, taxRate) {
  return price + price * taxRate;
}

function formatCurrency(amount) {
  return `$${amount.toFixed(2)}`;
}

Example 2: Conditional function creation (expression)

let validator;

if (strictMode) {
  validator = function (input) {
    return input.length >= 8 && /[A-Z]/.test(input);
  };
} else {
  validator = function (input) {
    return input.length >= 4;
  };
}

console.log(validator("Hello")); // depends on strictMode

Example 3: Functions in an array (first-class)

const pipeline = [
  function trim(str) { return str.trim(); },
  function lower(str) { return str.toLowerCase(); },
  function removeSpaces(str) { return str.replace(/\s+/g, "-"); },
];

let slug = "  Hello World  ";
for (const transform of pipeline) {
  slug = transform(slug);
}
console.log(slug); // "hello-world"

Example 4: Hoisting pitfall with var

console.log(greet);   // undefined (var is hoisted, but value is not)
// greet();            // TypeError: greet is not a function

var greet = function () {
  return "Hi!";
};

console.log(greet()); // "Hi!" — now it works

Key takeaways

  1. Function declarations use function name() {} and are fully hoisted — callable anywhere in the scope.
  2. Function expressions assign a function to a variable and are not hoisted — the variable exists only after assignment.
  3. Named function expressions help with debugging and self-reference; anonymous ones are shorter but less traceable.
  4. Functions are first-class citizens — they can be stored, passed, and returned like any other value.
  5. IIFE creates an isolated scope that runs immediately — useful for initialization and the module pattern.
  6. The Function constructor exists but should be avoided for security and performance reasons.
  7. Hoisting is the single biggest behavioral difference — understanding it prevents subtle bugs.

Explain-It Challenge

Explain without notes:

  1. What does "first-class citizen" mean when applied to JavaScript functions?
  2. Why does calling a function expression before its const declaration throw a ReferenceError, while calling a declaration works fine?
  3. Give one practical use case for an IIFE in modern JavaScript.
  4. A colleague wrote a function inside an if block. What might go wrong?

Navigation: ← 1.20 Overview · 1.20.b — Parameters and Arguments →