JavaScript Functions and Arrow Functions 2026 Tutorial

JavaScript Functions & Arrow Functions: Parameters, Scope, and Patterns

Functions are the core building block of JavaScript. Every non-trivial program uses them — to organize code into reusable chunks, abstract complex operations, handle events, and build APIs. JavaScript offers multiple ways to define functions, each with different behavior around this, hoisting, and syntax. This guide covers everything from classic function declarations to modern arrow functions, with practical patterns you’ll use in every project.

Make sure you’re comfortable with variables and data types before continuing.

What Are Functions

A function is a reusable block of code designed to perform a particular task. You define it once, then call it as many times as needed with different inputs. Functions can take parameters, perform operations, and return results. In JavaScript, functions are also objects — they can be assigned to variables, passed as arguments, returned from other functions, and have properties.

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

console.log(greet('Chirag')); // "Hello, Chirag!"
console.log(greet('World'));  // "Hello, World!"

Function Declarations

The traditional way to define a function uses the function keyword followed by a name:

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

function isEven(n) {
  return n % 2 === 0;
}

function printReport(data) {
  console.log('=== Report ===');
  for (const item of data) {
    console.log(`- ${item.name}: ${item.value}`);
  }
}

Function declarations are hoisted — they’re available throughout their scope, even before the line where they’re defined. This is unique to declarations and doesn’t apply to other function forms.

Function Expressions

A function expression assigns a function to a variable. It can be named or anonymous:

// Anonymous function expression
const multiply = function(a, b) {
  return a * b;
};

// Named function expression
const factorial = function fact(n) {
  return n <= 1 ? 1 : n * fact(n - 1);
};

Named function expressions are useful for recursion and better stack traces. The name (fact) is only available inside the function body.

Function expressions are not hoisted — you can't call them before the line where they're defined.

Arrow Functions

Arrow functions (introduced in ES6) provide a shorter syntax and different this behavior:

// Full syntax
const add = (a, b) => {
  return a + b;
};

// Implicit return (single expression)
const add = (a, b) => a + b;

// Single parameter — parentheses optional
const double = n => n * 2;

// No parameters
const getTimestamp = () => Date.now();

// Returning an object literal (wrap in parentheses)
const makeUser = (name, age) => ({ name, age });

Key Differences from Regular Functions

Arrow functions differ from regular functions in several critical ways:

No own this — arrow functions inherit this from their surrounding (lexical) scope. This is the biggest practical difference and why arrows are preferred in callbacks:

class Timer {
  constructor() {
    this.seconds = 0;
  }
  start() {
    // Arrow inherits `this` from start()
    setInterval(() => {
      this.seconds++; // works correctly
      console.log(this.seconds);
    }, 1000);
  }
}

// With a regular function, `this` would be `undefined` or `window`

No arguments object — use rest parameters instead:

// Regular function has `arguments`
function sum() {
  return [...arguments].reduce((a, b) => a + b, 0);
}

// Arrow uses rest params
const sum = (...nums) => nums.reduce((a, b) => a + b, 0);

Cannot be used as constructors — you can't use new with arrow functions. No prototype property. Cannot be used as generators (no yield).

When to Use Arrow vs Regular Functions

Use arrow functions for callbacks, array methods, and short utility functions. Use regular functions for methods on objects/classes (where you need this), constructors, and generator functions.

Parameters: Default, Rest, and Destructuring

Default Parameters (ES6)

function greet(name = 'stranger', greeting = 'Hello') {
  return `${greeting}, ${name}!`;
}

greet();              // "Hello, stranger!"
greet('Chirag');      // "Hello, Chirag!"
greet('Chirag', 'Hey'); // "Hey, Chirag!"

Defaults are evaluated at call time, so you can use expressions and even earlier parameters:

function createArray(length, fill = length * 2) {
  return new Array(length).fill(fill);
}
createArray(3); // [6, 6, 6]

Rest Parameters

function sum(...numbers) {
  return numbers.reduce((total, n) => total + n, 0);
}
sum(1, 2, 3, 4, 5); // 15

function log(level, ...messages) {
  console[level](...messages);
}
log('warn', 'Disk space low:', '92% used');

Rest must be the last parameter. It collects remaining arguments into a real array (unlike the legacy arguments object which is array-like).

Destructured Parameters

function createUser({ name, email, role = 'viewer' }) {
  return { name, email, role, createdAt: new Date() };
}

createUser({ name: 'Chirag', email: 'chirag@sudoflare.com' });

// Array destructuring
function getFirst([first, ...rest]) {
  return { first, remaining: rest.length };
}

Return Values

Functions return undefined by default. Use return to send back a value:

function divide(a, b) {
  if (b === 0) return null; // Early return
  return a / b;
}

// Returning multiple values via object
function getMinMax(arr) {
  return {
    min: Math.min(...arr),
    max: Math.max(...arr),
  };
}
const { min, max } = getMinMax([3, 1, 4, 1, 5, 9]);

Function Hoisting

Function declarations are fully hoisted — the entire function body is moved to the top of the scope:

// This works!
sayHello(); // "Hello!"

function sayHello() {
  console.log('Hello!');
}

// This does NOT work
// greet(); // TypeError: greet is not a function
const greet = function() {
  console.log('Hi!');
};

With let/const, the variable exists in the temporal dead zone until the declaration is reached, so calling a function expression before its definition throws a ReferenceError (with let/const) or gets undefined (with var).

First-Class Functions

In JavaScript, functions are first-class citizens — they can be treated like any other value:

// Assign to variable
const fn = function() { return 42; };

// Store in data structures
const operations = {
  add: (a, b) => a + b,
  sub: (a, b) => a - b,
  mul: (a, b) => a * b,
};
console.log(operations.add(5, 3)); // 8

// Pass as argument
[1, 2, 3].map(n => n * 2); // [2, 4, 6]

// Return from function
function multiplier(factor) {
  return (n) => n * factor;
}
const triple = multiplier(3);
triple(5); // 15

Higher-Order Functions

A higher-order function either takes a function as an argument or returns a function (or both). They're everywhere in JavaScript:

// Takes a function
function repeat(n, action) {
  for (let i = 0; i < n; i++) action(i);
}
repeat(3, console.log); // 0, 1, 2

// Returns a function (closure)
function prefix(pre) {
  return (str) => `${pre}: ${str}`;
}
const warn = prefix('WARNING');
const info = prefix('INFO');
warn('Disk full');  // "WARNING: Disk full"
info('Server up');  // "INFO: Server up"

// Compose functions
function compose(...fns) {
  return (x) => fns.reduceRight((acc, fn) => fn(acc), x);
}
const process = compose(
  s => s.toUpperCase(),
  s => s.trim(),
  s => s.replace(/\s+/g, ' ')
);
process('  hello   world  '); // "HELLO WORLD"

IIFE — Immediately Invoked Function Expressions

An IIFE runs immediately after it's defined. Before modules existed, IIFEs were the primary way to create private scope:

(function() {
  const secret = 'hidden';
  console.log('IIFE ran');
})();
// `secret` is not accessible here

// Arrow IIFE
(() => {
  console.log('Arrow IIFE');
})();

// With parameters
((name) => {
  console.log(`Hello, ${name}`);
})('Chirag');

IIFEs are less common now that we have let/const (block scoping) and ES modules, but you'll still see them in legacy code and certain initialization patterns.

Recursion

A recursive function calls itself. Every recursive function needs a base case to stop the recursion:

function factorial(n) {
  if (n <= 1) return 1;        // base case
  return n * factorial(n - 1); // recursive case
}
factorial(5); // 120

// Tree traversal
function flattenTree(node) {
  const result = [node.value];
  if (node.children) {
    for (const child of node.children) {
      result.push(...flattenTree(child));
    }
  }
  return result;
}

// Deep clone
function deepClone(obj) {
  if (obj === null || typeof obj !== 'object') return obj;
  if (Array.isArray(obj)) return obj.map(deepClone);
  return Object.fromEntries(
    Object.entries(obj).map(([k, v]) => [k, deepClone(v)])
  );
}

Watch out for stack overflow — JavaScript has a limited call stack (typically 10,000-25,000 frames). For deep recursion, consider converting to an iterative approach with an explicit stack.

Generator Functions

Generators are functions that can pause and resume execution using yield:

function* range(start, end, step = 1) {
  for (let i = start; i < end; i += step) {
    yield i;
  }
}

for (const n of range(0, 10, 2)) {
  console.log(n); // 0, 2, 4, 6, 8
}

// Infinite sequence
function* fibonacci() {
  let [a, b] = [0, 1];
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

// Take first 10 fibonacci numbers
const fib = fibonacci();
const first10 = Array.from({ length: 10 }, () => fib.next().value);
// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

Generators are lazy — they compute values on demand, making them memory-efficient for large or infinite sequences.

Pure Functions and Side Effects

A pure function always returns the same output for the same input and has no side effects (doesn't modify external state):

// Pure
function add(a, b) { return a + b; }
function formatName(first, last) { return `${first} ${last}`; }

// Impure — modifies external state
let total = 0;
function addToTotal(n) { total += n; return total; }

// Impure — depends on external state
function getDiscount(price) { return price * config.discountRate; }

Pure functions are easier to test, debug, cache (memoize), and reason about. Aim for purity when possible, but side effects are necessary for I/O, DOM updates, and API calls.

Best Practices

Keep functions small and focused — each should do one thing. Use descriptive names that start with a verb (getUser, calculateTotal, validateEmail). Prefer arrow functions for callbacks and short utilities. Use default parameters instead of checking for undefined inside the function. Limit parameters to three or fewer — use an options object for more. Return early to avoid deep nesting. Document complex functions with JSDoc comments.

Functions are the gateway to understanding scope and closures — one of JavaScript's most powerful (and confusing) features. Continue to Scope & Closures.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *