Skip to content

Functions and Parameters

beginner18 min read

Functions Are the Building Blocks of Everything

Variables store data. Functions store behavior. Every event handler, every API call, every React component, every utility in your codebase is a function. They're the fundamental unit of reusable logic in JavaScript.

But here's the thing most beginners miss: JavaScript has three ways to create functions, and they're not just different syntax for the same thing. They behave differently in ways that will bite you if you don't understand the differences.

Mental Model

Think of functions as recipes on index cards. A function declaration is a recipe card filed in the recipe box before you even start cooking — it's available from the very first moment. A function expression is a recipe you write on a card and hand to someone mid-cooking — it only exists from that point forward. An arrow function is a sticky note version of the recipe — shorter, lighter, but it can't be used for everything a full recipe card can.

Function Declarations

A function declaration uses the function keyword, followed by a name:

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

greet("Alice"); // "Hello, Alice!"

The key behavior: function declarations are hoisted. The engine registers them during the creation phase, so you can call them before their line in the code:

sayHi(); // "Hi!" — works because declarations are hoisted

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

This isn't magic — the engine scans for function declarations before executing any code and makes them available immediately.

Function Expressions

A function expression assigns a function to a variable:

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

greet("Bob"); // "Hello, Bob!"

The critical difference: function expressions are NOT hoisted in the way declarations are. The variable is hoisted (if it's var), but the function assignment happens at runtime:

sayHi(); // TypeError: sayHi is not a function

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

With var, sayHi is hoisted as undefined. Calling undefined() throws a TypeError — not a ReferenceError. With let or const, you'd get a ReferenceError instead because of the Temporal Dead Zone.

Execution Trace
Creation
Register 'sayHi' (var) → undefined
Only the variable is hoisted, not the function
Line 1
sayHi() → TypeError!
sayHi is undefined — calling undefined as a function throws
Line 3
sayHi = function() { ... }
Now sayHi holds the function (too late)
Quiz
What happens when you call a function expression declared with var before its assignment line?

Arrow Functions

Arrow functions (introduced in ES2015) provide a shorter syntax:

const greet = (name) => `Hello, ${name}!`;

const add = (a, b) => a + b;

const getUser = () => ({ name: "Alice", age: 30 });

const process = (items) => {
  const filtered = items.filter(Boolean);
  return filtered.length;
};

But arrows aren't just syntactic sugar. They have three behavioral differences that change how they work:

1. No Own this Binding

This is the big one. Arrow functions don't get their own this. They inherit this from the enclosing lexical scope — the scope where they were defined:

const user = {
  name: "Alice",
  greetRegular: function() {
    console.log(this.name); // "Alice" — this = user
  },
  greetArrow: () => {
    console.log(this.name); // undefined — this = enclosing scope (module/global)
  }
};

user.greetRegular(); // "Alice"
user.greetArrow();   // undefined

The arrow function doesn't care that it was called as user.greetArrow(). It doesn't have its own this — it uses whatever this was in the scope where the arrow was created. Since it was created at the top level of the object literal (which isn't a scope — it's just an expression), this refers to the surrounding scope.

Where arrows shine is callbacks inside methods:

const user = {
  name: "Alice",
  friends: ["Bob", "Carol"],
  showFriends() {
    this.friends.forEach((friend) => {
      console.log(`${this.name} knows ${friend}`);
      // Arrow inherits 'this' from showFriends — works!
    });
  }
};

user.showFriends();
// "Alice knows Bob"
// "Alice knows Carol"

If you used a regular function inside forEach, this would be undefined (in strict mode) because it's called as a plain function, not as a method.

2. No arguments Object

Arrow functions don't have the arguments object:

function regular() {
  console.log(arguments); // [1, 2, 3] — works
}

const arrow = () => {
  console.log(arguments); // ReferenceError (or inherits from outer function)
};

regular(1, 2, 3);

This is rarely a problem because rest parameters (...args) are better in every way. More on that shortly.

3. Cannot Be Used as Constructors

Arrow functions can't be called with new:

const Person = (name) => {
  this.name = name;
};

new Person("Alice"); // TypeError: Person is not a constructor

Arrow functions don't have a [[Construct]] internal method or a prototype property, so they can't create new objects via new.

Quiz
Why does this code log undefined instead of Alice?

The Three Types Compared

FeatureDeclarationExpressionArrow
HoistedYes (fully)NoNo
Own thisYes (call-site)Yes (call-site)No (lexical)
arguments objectYesYesNo
Can use newYesYesNo
Syntaxfunction name() {}const name = function() {}const name = () => {}
Best forTop-level functionsCallbacks, conditionalCallbacks, inline logic

Parameters vs Arguments

These terms get used interchangeably, but they mean different things:

  • Parameters are the variables listed in the function definition (the placeholders)
  • Arguments are the actual values passed when calling the function
function add(a, b) {  // a, b are parameters
  return a + b;
}

add(3, 5);             // 3, 5 are arguments

JavaScript is lenient about argument count. Missing arguments default to undefined. Extra arguments are silently ignored (but accessible via arguments in regular functions):

function show(a, b) {
  console.log(a, b);
}

show(1);        // 1, undefined — missing b
show(1, 2, 3);  // 1, 2 — extra 3 is ignored

Default Parameters

Default parameters let you specify fallback values when an argument is undefined or not provided:

function createUser(name, role = "viewer", active = true) {
  return { name, role, active };
}

createUser("Alice");                // { name: "Alice", role: "viewer", active: true }
createUser("Bob", "admin");         // { name: "Bob", role: "admin", active: true }
createUser("Carol", "editor", false); // { name: "Carol", role: "editor", active: false }

Default values are evaluated at call time

This is important. The default expression runs fresh each time the function is called without that argument — it's not evaluated once when the function is defined:

function addItem(item, list = []) {
  list.push(item);
  return list;
}

addItem("a"); // ["a"] — new array each call
addItem("b"); // ["b"] — new array, not ["a", "b"]

This is the correct behavior. In Python, mutable default arguments are evaluated once and shared across calls, which is a notorious bug source. JavaScript got this right.

Defaults only trigger on undefined

Passing null, 0, "", or false does not trigger the default:

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

greet(undefined); // "Hello, stranger!" — default triggered
greet(null);      // "Hello, null!" — null is not undefined
greet("");        // "Hello, !" — empty string is not undefined
greet(0);         // "Hello, 0!" — 0 is not undefined
Common Trap

Default parameters only activate when the argument is undefined — not when it's any other falsy value. If you pass null, 0, "", or false, the default is not used. This trips up developers who expect defaults to work like value || defaultValue. The || operator falls back on any falsy value, but default parameters only fall back on undefined.

Rest Parameters

Rest parameters collect all remaining arguments into a real array:

function sum(...numbers) {
  return numbers.reduce((total, n) => total + n, 0);
}

sum(1, 2, 3);    // 6
sum(10, 20);     // 30

Rest parameters must be the last parameter:

function tag(name, ...attributes) {
  console.log(name);       // "div"
  console.log(attributes); // ["class=main", "id=app"]
}

tag("div", "class=main", "id=app");

Rest vs arguments

The arguments object is an array-like object (not a real array). Rest parameters give you a real Array with all array methods:

function oldWay() {
  // arguments is not a real array — no .map(), .filter(), etc.
  const args = Array.from(arguments); // manual conversion needed
  return args.map(x => x * 2);
}

function newWay(...args) {
  // args is a real Array — use any array method directly
  return args.map(x => x * 2);
}

There's zero reason to use arguments in modern code. Rest parameters are cleaner, work with arrow functions, and give you a real array.

Quiz
What does sum(1, 2, 3) return?

Return Values

Every function in JavaScript returns a value. If you don't explicitly return, the function returns undefined:

function noReturn() {
  const x = 42;
  // no return statement
}

noReturn(); // undefined

Implicit return with arrows

Arrow functions with no curly braces have an implicit return — the expression's value is returned automatically:

const double = (x) => x * 2;       // implicit return: x * 2
const greet = (name) => `Hi ${name}`; // implicit return: template string

Add curly braces, and you need an explicit return:

const double = (x) => {
  return x * 2; // explicit return required with curly braces
};

The Parentheses Trap: Returning Objects

This is one of the most common arrow function gotchas. If you want to implicitly return an object literal, you must wrap it in parentheses:

// WRONG — the engine thinks { } is a block, not an object
const makeUser = (name) => { name: name };
makeUser("Alice"); // undefined (!)

// RIGHT — parentheses tell the engine it's an expression
const makeUser = (name) => ({ name: name });
makeUser("Alice"); // { name: "Alice" }

Without parentheses, the engine parses { name: name } as a block with a label statement (name:) followed by the expression name. Labels are an obscure JavaScript feature rarely used outside loops. The block has no return, so the function returns undefined.

Common Trap

When an arrow function returns an object literal without parentheses, it silently returns undefined instead of the object. No error, no warning. The curly braces get interpreted as a function body block, and name: name becomes a label statement. Always wrap returned object literals in parentheses: () => ({ key: value }).

First-Class Functions

In JavaScript, functions are first-class citizens. That means functions are values — just like numbers, strings, and objects. You can:

  1. Assign them to variables:
const greet = function(name) { return `Hi, ${name}`; };
  1. Pass them as arguments:
function repeat(fn, times) {
  for (let i = 0; i < times; i++) {
    fn(i);
  }
}

repeat(console.log, 3); // logs 0, 1, 2
  1. Return them from other functions:
function multiplier(factor) {
  return (number) => number * factor;
}

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

double(5);  // 10
triple(5);  // 15
  1. Store them in data structures:
const operations = {
  add: (a, b) => a + b,
  subtract: (a, b) => a - b,
  multiply: (a, b) => a * b,
};

operations.add(2, 3);      // 5
operations.multiply(4, 5); // 20

This is what makes patterns like callbacks, higher-order functions, and functional programming possible in JavaScript.

Callbacks

A callback is simply a function passed as an argument to another function, to be called later:

function fetchData(url, onSuccess, onError) {
  fetch(url)
    .then(response => response.json())
    .then(data => onSuccess(data))
    .catch(error => onError(error));
}

fetchData(
  "/api/users",
  (data) => console.log("Got users:", data),
  (error) => console.error("Failed:", error)
);

You've already used callbacks if you've used addEventListener, setTimeout, forEach, map, filter, or reduce. The function you pass to each of these is a callback.

Quiz
What does multiplier(4)(3) return?

IIFE: Immediately Invoked Function Expressions

An IIFE is a function that runs the moment it's defined:

(function() {
  const secret = "hidden";
  console.log(secret); // "hidden"
})();

// secret is not accessible here

The parentheses around the function turn the declaration into an expression, and the () at the end immediately invoke it.

Why IIFEs Exist

Before ES2015 modules, JavaScript had no native way to create private scopes. var doesn't have block scope — it leaks out to the enclosing function. IIFEs solved this by creating a function scope:

// Without IIFE — var pollutes the global scope
var counter = 0;
var increment = function() { counter++; };

// With IIFE — everything stays private
const counterModule = (function() {
  var counter = 0; // private — not accessible outside
  return {
    increment() { counter++; },
    getCount() { return counter; }
  };
})();

counterModule.increment();
counterModule.increment();
counterModule.getCount(); // 2
// counterModule.counter → undefined

IIFEs in Modern Code

With let, const (which have block scope) and ES modules (which give every file its own scope), IIFEs are rarely needed. But you'll still see them in:

  • Legacy codebases
  • Bundled output from tools like webpack
  • Isolating await at the top level in older environments (before top-level await)
The IIFE Syntax Variants

You might see IIFEs written a few different ways:

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

// Douglas Crockford style — invocation inside the wrapping parens
(function() { /* ... */ }());

// Arrow function IIFE
(() => { /* ... */ })();

// With arguments
((name) => {
  console.log(`Hello, ${name}`);
})("Alice");

They all do the same thing. The arrow version is the most common in modern code when an IIFE is actually needed.

Pure Functions

A pure function is a function that:

  1. Always returns the same output for the same input (deterministic)
  2. Has no side effects (doesn't modify anything outside itself)
// PURE — same input always gives same output, no side effects
function add(a, b) {
  return a + b;
}

// PURE — creates and returns a new array without modifying the original
function doubled(numbers) {
  return numbers.map(n => n * 2);
}

// IMPURE — modifies external state (side effect)
let total = 0;
function addToTotal(amount) {
  total += amount; // mutates external variable
  return total;
}

// IMPURE — same input can give different output
function getTimestamp() {
  return Date.now(); // depends on when you call it
}

// IMPURE — side effect (I/O)
function logMessage(msg) {
  console.log(msg); // writing to console is a side effect
  return msg;
}

Why Pure Functions Matter

Pure functions are:

  • Predictable — easy to test, easy to reason about
  • Cacheable — since the same input always gives the same output, you can memoize results
  • Parallelizable — no shared state means no race conditions
  • Composable — you can chain them together safely

React leans heavily on this concept. Components are meant to be pure functions of their props — same props, same output. This is what makes React's rendering model work.

Pure in practice

You can't build a real application with only pure functions — at some point you need to read user input, fetch data, update the DOM, or write to a database. The goal isn't purity everywhere. It's isolating side effects to the edges of your system and keeping your core logic pure. This makes your code dramatically easier to test and debug.

Quiz
Which of these functions is pure?

Putting It All Together

Key Rules
  1. 1Function declarations are hoisted, expressions and arrows are not
  2. 2Arrow functions inherit this from the enclosing scope — they never get their own
  3. 3Arrow functions have no arguments object and cannot be used with new
  4. 4Default parameters only trigger on undefined, not other falsy values
  5. 5Rest parameters must be last and produce a real Array, unlike arguments
  6. 6Wrap returned object literals in parentheses when using arrow implicit return
  7. 7Pure functions: same input, same output, no side effects
What developers doWhat they should do
Using an arrow function as an object method and expecting this to be the object
Arrow functions don't get their own this — they inherit from the enclosing scope. In an object literal, the enclosing scope is not the object, it's the surrounding code.
Use a regular function or shorthand method for object methods that need this
Returning an object from an arrow without parentheses: () => { key: value }
Without parentheses, the curly braces are parsed as a function body block, not an object literal. The function silently returns undefined.
Wrap in parentheses: () => ({ key: value })
Using || for default values instead of default parameters
The || operator falls back on any falsy value (0, empty string, false). Default parameters only fall back on undefined, which is usually what you want. The ?? operator falls back on null or undefined.
Use default parameters or the ?? (nullish coalescing) operator
Using arguments in arrow functions
Arrow functions don't have their own arguments object. If arguments appears to work, you're accidentally accessing arguments from an enclosing regular function. Rest parameters always work and give you a real Array.
Use rest parameters (...args) instead
Quiz
What does this code output?