Part 5: Functions Fundamentals

The Building Blocks of Reusable Code


What Are Functions?

Functions are reusable blocks of code that perform specific tasks. They are first-class citizens in JavaScript, meaning they can be assigned to variables, passed as arguments, and returned from other functions.
javascript
// A simple function function greet(name) { return `Hello, ${name}!`; } // Calling the function const message = greet('Alice'); console.log(message); // "Hello, Alice!" // Functions are values console.log(typeof greet); // "function" console.log(greet.name); // "greet"

Function Declarations

Function declarations are hoisted, meaning they can be called before they're defined in the code.
javascript
// Can call before declaration (hoisting) sayHello(); // "Hello!" function sayHello() { console.log('Hello!'); } // Function with parameters and return value function add(a, b) { return a + b; } const sum = add(5, 3); // 8 // Functions without return statement return undefined function logMessage(msg) { console.log(msg); // No return statement } const result = logMessage('Hi'); console.log(result); // undefined

Multiple Return Statements

javascript
function getGrade(score) { if (score >= 90) return 'A'; if (score >= 80) return 'B'; if (score >= 70) return 'C'; if (score >= 60) return 'D'; return 'F'; } console.log(getGrade(85)); // 'B' // Early return pattern function findUser(users, id) { for (const user of users) { if (user.id === id) { return user; // Exit function when found } } return null; // Not found }

Function Expressions

Function expressions assign a function to a variable. They are not hoisted.
javascript
// Cannot call before definition // sayGoodbye(); // ReferenceError const sayGoodbye = function() { console.log('Goodbye!'); }; sayGoodbye(); // "Goodbye!" // Named function expression const factorial = function fact(n) { if (n <= 1) return 1; return n * fact(n - 1); // Can reference itself }; console.log(factorial(5)); // 120 // console.log(fact(5)); // ReferenceError (name is local)

Anonymous vs Named Functions

javascript
// Anonymous function const anonymous = function(x) { return x * 2; }; console.log(anonymous.name); // "anonymous" (inferred) // Named function (better for stack traces) const named = function double(x) { return x * 2; }; console.log(named.name); // "double" // In callbacks [1, 2, 3].map(function double(x) { return x * 2; }); // Stack trace shows "double" instead of "anonymous"

Arrow Functions

Arrow functions provide a shorter syntax and don't have their own this binding.
javascript
// Basic arrow function const add = (a, b) => { return a + b; }; // Implicit return (single expression) const multiply = (a, b) => a * b; // Single parameter (no parentheses needed) const double = x => x * 2; // No parameters const greet = () => 'Hello!'; // Returning object literal (needs parentheses) const createUser = (name, age) => ({ name, age }); console.log(createUser('Alice', 30)); // { name: 'Alice', age: 30 }

Arrow Functions vs Regular Functions

javascript
// 1. No 'this' binding const obj = { name: 'Object', regularMethod: function() { console.log(this.name); // 'Object' }, arrowMethod: () => { console.log(this.name); // undefined (or global) } }; // 2. No 'arguments' object function regular() { console.log(arguments); // Arguments object } const arrow = () => { // console.log(arguments); // ReferenceError }; // Use rest parameters instead const arrowWithArgs = (...args) => { console.log(args); // Array }; // 3. Cannot be used as constructors function Person(name) { this.name = name; } new Person('Alice'); // Works const PersonArrow = (name) => { this.name = name; }; // new PersonArrow('Alice'); // TypeError // 4. No prototype property console.log(Person.prototype); // { constructor: f } console.log(PersonArrow.prototype); // undefined

When to Use Arrow Functions

javascript
// Great for callbacks const doubled = [1, 2, 3].map(n => n * 2); // Great for short functions const isEven = n => n % 2 === 0; // Great when you need lexical 'this' class Counter { constructor() { this.count = 0; } start() { setInterval(() => { this.count++; // 'this' refers to Counter instance console.log(this.count); }, 1000); } } // NOT for object methods (usually) const obj = { value: 42, getValue: () => this.value // Won't work as expected };

Parameters and Arguments

Default Parameters

javascript
// ES6 default parameters function greet(name = 'Guest', greeting = 'Hello') { return `${greeting}, ${name}!`; } console.log(greet()); // "Hello, Guest!" console.log(greet('Alice')); // "Hello, Alice!" console.log(greet('Alice', 'Hi')); // "Hi, Alice!" console.log(greet(undefined, 'Hey')); // "Hey, Guest!" // Defaults can be expressions function createId(prefix = 'id', num = Date.now()) { return `${prefix}_${num}`; } // Defaults can reference previous parameters function createFullName(first, last, full = `${first} ${last}`) { return { first, last, full }; }

Rest Parameters

javascript
// Gather remaining arguments into an array function sum(...numbers) { return numbers.reduce((acc, n) => acc + n, 0); } console.log(sum(1, 2, 3)); // 6 console.log(sum(1, 2, 3, 4, 5)); // 15 // Combined with regular parameters function log(level, ...messages) { console.log(`[${level}]`, ...messages); } log('ERROR', 'Failed to', 'connect to', 'database'); // [ERROR] Failed to connect to database // Rest must be last parameter // function invalid(a, ...rest, b) { } // SyntaxError

Spread in Function Calls

javascript
const numbers = [1, 2, 3, 4, 5]; // Spread array as arguments console.log(Math.max(...numbers)); // 5 // Combine with other arguments console.log(Math.max(0, ...numbers, 10)); // 10 // Multiple spreads const moreNumbers = [6, 7, 8]; console.log(Math.max(...numbers, ...moreNumbers)); // 8

Arguments Object (Legacy)

javascript
// In regular functions (not arrow) function oldSum() { let total = 0; for (let i = 0; i < arguments.length; i++) { total += arguments[i]; } return total; } console.log(oldSum(1, 2, 3, 4)); // 10 // Converting to array function example() { const args = Array.from(arguments); // or: const args = [...arguments]; return args; } // Prefer rest parameters in modern code

Scope

Scope determines where variables are accessible.

Global Scope

javascript
// Variables declared outside functions are global const globalVar = 'I am global'; function checkGlobal() { console.log(globalVar); // Accessible } checkGlobal(); // "I am global" // In browsers, global variables are properties of window var browserGlobal = 'test'; console.log(window.browserGlobal); // "test" (in browser)

Function Scope

javascript
function outer() { const outerVar = 'outer'; function inner() { const innerVar = 'inner'; console.log(outerVar); // Accessible (from parent scope) console.log(innerVar); // Accessible (own scope) } inner(); // console.log(innerVar); // ReferenceError } outer(); // console.log(outerVar); // ReferenceError

Block Scope

javascript
// let and const are block-scoped if (true) { let blockLet = 'let'; const blockConst = 'const'; var blockVar = 'var'; } // console.log(blockLet); // ReferenceError // console.log(blockConst); // ReferenceError console.log(blockVar); // "var" (not block-scoped!) // Loops for (let i = 0; i < 3; i++) { setTimeout(() => console.log(i), 100); } // 0, 1, 2 (let creates new binding each iteration) for (var j = 0; j < 3; j++) { setTimeout(() => console.log(j), 100); } // 3, 3, 3 (var shares single binding)

Lexical Scope

javascript
const name = 'Global'; function outer() { const name = 'Outer'; function inner() { const name = 'Inner'; console.log(name); // 'Inner' (nearest scope) } function innerNoLocal() { console.log(name); // 'Outer' (parent scope) } inner(); innerNoLocal(); } outer();

Closures

A closure is a function that remembers variables from its outer scope even after that scope has finished executing.
javascript
function createCounter() { let count = 0; // Private variable return function() { count++; return count; }; } const counter = createCounter(); console.log(counter()); // 1 console.log(counter()); // 2 console.log(counter()); // 3 // Each call to createCounter creates new closure const counter2 = createCounter(); console.log(counter2()); // 1 (independent count)

Practical Closure Examples

javascript
// 1. Data privacy function createBankAccount(initialBalance) { let balance = initialBalance; // Private return { deposit(amount) { balance += amount; return balance; }, withdraw(amount) { if (amount > balance) { throw new Error('Insufficient funds'); } balance -= amount; return balance; }, getBalance() { return balance; } }; } const account = createBankAccount(100); console.log(account.deposit(50)); // 150 console.log(account.withdraw(30)); // 120 console.log(account.getBalance()); // 120 // console.log(account.balance); // undefined (private!) // 2. Function factories function createMultiplier(factor) { return function(number) { return number * factor; }; } const double = createMultiplier(2); const triple = createMultiplier(3); console.log(double(5)); // 10 console.log(triple(5)); // 15 // 3. Memoization function memoize(fn) { const cache = {}; return function(...args) { const key = JSON.stringify(args); if (key in cache) { return cache[key]; } const result = fn.apply(this, args); cache[key] = result; return result; }; } const expensiveOperation = memoize(function(n) { console.log('Computing...'); return n * n; }); console.log(expensiveOperation(5)); // Computing... 25 console.log(expensiveOperation(5)); // 25 (cached)

Immediately Invoked Function Expressions (IIFE)

IIFEs run immediately after being defined, creating a private scope.
javascript
// Basic IIFE (function() { const private = 'secret'; console.log('IIFE executed'); })(); // With parameters (function(name) { console.log(`Hello, ${name}!`); })('World'); // Arrow function IIFE (() => { console.log('Arrow IIFE'); })(); // Returning a value const result = (function() { const a = 10; const b = 20; return a + b; })(); console.log(result); // 30 // Module pattern (before ES6 modules) const Calculator = (function() { // Private let result = 0; // Public API return { add(n) { result += n; return this; }, subtract(n) { result -= n; return this; }, getResult() { return result; } }; })(); Calculator.add(10).add(5).subtract(3); console.log(Calculator.getResult()); // 12

Higher-Order Functions

Functions that take functions as arguments or return functions.
javascript
// Function as argument function repeat(n, action) { for (let i = 0; i < n; i++) { action(i); } } repeat(3, console.log); // 0, 1, 2 repeat(3, i => { console.log(`Iteration ${i}`); }); // Function returning function function greaterThan(n) { return function(m) { return m > n; }; } const greaterThan10 = greaterThan(10); console.log(greaterThan10(15)); // true console.log(greaterThan10(5)); // false // Composition function compose(...fns) { return function(value) { return fns.reduceRight((acc, fn) => fn(acc), value); }; } const addOne = x => x + 1; const double = x => x * 2; const square = x => x * x; const compute = compose(square, double, addOne); console.log(compute(3)); // ((3 + 1) * 2)² = 64

Pure Functions

Pure functions always produce the same output for the same input and have no side effects.
javascript
// Pure function function add(a, b) { return a + b; } // Always returns same output for same input add(2, 3); // Always 5 // Impure - depends on external state let counter = 0; function incrementCounter() { counter++; // Side effect return counter; } // Impure - modifies input function addItem(array, item) { array.push(item); // Mutates array return array; } // Pure version function addItemPure(array, item) { return [...array, item]; // Returns new array } const original = [1, 2, 3]; const newArray = addItemPure(original, 4); console.log(original); // [1, 2, 3] (unchanged) console.log(newArray); // [1, 2, 3, 4]

Summary

Function types:
  • Declarations are hoisted and can be called before definition
  • Expressions are not hoisted and create anonymous or named functions
  • Arrow functions have shorter syntax, lexical this, no arguments
Parameters:
  • Default parameters provide fallback values
  • Rest parameters gather remaining arguments into an array
  • Spread expands arrays into individual arguments
Scope:
  • Global scope is accessible everywhere
  • Function scope is created by functions
  • Block scope is created by let/const in blocks
  • Lexical scope means inner functions access outer variables
Closures:
  • Functions remember their outer scope
  • Enable data privacy and function factories
  • Powerful but can cause memory issues if not careful

Questions to Think About

  1. When should you use arrow functions vs regular functions?
Use arrow functions for callbacks, short functions, and when you need lexical this. Use regular functions for methods that need their own this, constructors, and when you need the arguments object.
  1. What are the benefits of closures?
Closures enable data privacy (encapsulation), function factories, memoization, and maintaining state between function calls without global variables.
  1. Why is function scope important for var vs let?
var is function-scoped, so it can leak out of blocks like if and for. let is block-scoped, preventing accidental variable leaks and making code more predictable.
  1. What makes a function "pure"?
A pure function: (1) always returns the same output for the same input, (2) has no side effects (doesn't modify external state or its inputs). Pure functions are easier to test and reason about.
  1. When would you use an IIFE?
IIFEs were common before ES6 modules for creating private scopes. Today they're still useful for one-time initialization, avoiding polluting global scope, and immediately executing async code.

Next: Part 6 - Objects Deep Dive, where we explore object creation, properties, methods, and the this keyword.
All Blogs
Tags:javascriptfunctionsarrow-functionsscopeclosures