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
| Anonymous | Named | |
|---|---|---|
| Syntax | const fn = function() {} | const fn = function myFn() {} |
| Stack traces | Shows (anonymous) or the variable name | Shows myFn — easier debugging |
| Self-reference | Must use the outer variable name for recursion | Can call itself by myFn inside the body |
| Outer access | Via 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 Declaration | Function Expression (const) | Function Expression (var) | |
|---|---|---|---|
| Hoisted? | Yes — fully | No — ReferenceError in TDZ | Variable hoisted as undefined |
| Callable before definition? | Yes | No | No — TypeError |
| Scope | Function or global | Block | Function or global |
5. When to use which — guidelines
| Scenario | Recommended | Why |
|---|---|---|
| Utility/helper that the whole file needs | Declaration | Hoisting lets you place helpers at bottom, main logic at top |
| Callback passed inline | Expression (often arrow) | No need for a standalone name |
| Conditional function creation | Expression | Declarations inside if blocks behave inconsistently across engines |
| Immediately executed setup | IIFE | Runs once, doesn't pollute scope |
| Method on an object | Expression or shorthand | Assigned 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?
- Scope isolation — variables declared inside do not leak to global scope.
- 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
- Avoiding global pollution in scripts that share a page.
- Async IIFE for top-level
awaitin 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
- Function declarations use
function name() {}and are fully hoisted — callable anywhere in the scope. - Function expressions assign a function to a variable and are not hoisted — the variable exists only after assignment.
- Named function expressions help with debugging and self-reference; anonymous ones are shorter but less traceable.
- Functions are first-class citizens — they can be stored, passed, and returned like any other value.
- IIFE creates an isolated scope that runs immediately — useful for initialization and the module pattern.
- The
Functionconstructor exists but should be avoided for security and performance reasons. - Hoisting is the single biggest behavioral difference — understanding it prevents subtle bugs.
Explain-It Challenge
Explain without notes:
- What does "first-class citizen" mean when applied to JavaScript functions?
- Why does calling a function expression before its
constdeclaration throw aReferenceError, while calling a declaration works fine? - Give one practical use case for an IIFE in modern JavaScript.
- A colleague wrote a
functioninside anifblock. What might go wrong?
Navigation: ← 1.20 Overview · 1.20.b — Parameters and Arguments →