Skip to main content
Advertisement

4.1 Four Ways to Define Functions

JavaScript provides four main ways to define a function. Each differs in hoisting behavior, this binding, and syntax — choose the right one for the situation.


1. Function Declaration

The most traditional way to define a function. Starts with the function keyword, and the entire function is hoisted.

// Callable before declaration thanks to hoisting
console.log(add(2, 3)); // 5

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

// Recursive function — references itself by name
function factorial(n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}

console.log(factorial(5)); // 120

Hoisting mechanism

// Actual code
greet("Alice");
function greet(name) {
console.log(`Hello, ${name}!`);
}

// How the JS engine processes it (conceptually)
function greet(name) { // Entire function hoisted to the top
console.log(`Hello, ${name}!`);
}
greet("Alice"); // Hello, Alice!

2. Function Expression

Assigns a function to a variable. With var, only the variable name is hoisted; with const/let, TDZ (Temporal Dead Zone) prevents calls before declaration.

// const declaration — TDZ prevents calling before declaration
// console.log(multiply(2, 3)); // ReferenceError!

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

console.log(multiply(2, 3)); // 6

// Named function expression — name appears in stack traces for debugging
const divide = function divideNumbers(a, b) {
if (b === 0) throw new Error("Cannot divide by zero");
return a / b;
};

console.log(divide(10, 2)); // 5
// divideNumbers is only accessible inside the function

Difference between var and const

// var: only the variable name is hoisted (initialized to undefined)
console.log(typeof varFunc); // "undefined"
var varFunc = function() { return "var"; };

// const: TDZ — accessing before declaration throws an error
// console.log(typeof constFunc); // ReferenceError
const constFunc = function() { return "const"; };

3. Arrow Function

A concise function syntax introduced in ES6. It has no own this binding and always uses the outer this from the point of definition.

// Basic syntax
const square = (x) => x * x;
const greet = name => `Hello, ${name}!`; // parentheses optional for one param
const getZero = () => 0; // parentheses required for no params

// Multi-line body requires curly braces and explicit return
const clamp = (value, min, max) => {
if (value < min) return min;
if (value > max) return max;
return value;
};

console.log(square(5)); // 25
console.log(greet("Bob")); // Hello, Bob!
console.log(clamp(15, 0, 10)); // 10

// Wrap object literal in parentheses for implicit return
const makeUser = (name, age) => ({ name, age });
console.log(makeUser("Alice", 30)); // { name: 'Alice', age: 30 }

No this binding — practical example

class Timer {
constructor() {
this.seconds = 0;
}

start() {
// Arrow function: captures outer this (Timer instance)
setInterval(() => {
this.seconds++;
console.log(`${this.seconds} second(s) elapsed`);
}, 1000);
}
}

const timer = new Timer();
timer.start();
// 1 second(s) elapsed, 2 second(s) elapsed, 3 second(s) elapsed...

When NOT to use arrow functions

const obj = {
name: "Object",

// ❌ Arrow function: this is global (or undefined)
badMethod: () => {
console.log(this?.name); // undefined
},

// ✅ Method shorthand: this is obj
goodMethod() {
console.log(this.name); // "Object"
},
};

obj.badMethod(); // undefined
obj.goodMethod(); // "Object"

4. Method Shorthand

A shortened method definition syntax for objects and classes introduced in ES6. It behaves similarly to a function declaration internally, but supports the super keyword.

// Method shorthand in object literal
const calculator = {
value: 0,

add(n) { // function keyword omitted
this.value += n;
return this; // method chaining
},

subtract(n) {
this.value -= n;
return this;
},

result() {
return this.value;
},
};

console.log(calculator.add(10).subtract(3).result()); // 7

// Method shorthand in class
class Animal {
constructor(name) {
this.name = name;
}

speak() {
return `${this.name} makes a sound.`;
}

// Static methods use the same syntax
static create(name) {
return new Animal(name);
}
}

// super keyword — only works with method shorthand
class Dog extends Animal {
speak() {
return super.speak() + " Woof!";
}
}

const dog = new Dog("Rex");
console.log(dog.speak()); // "Rex makes a sound. Woof!"

Practical Example: Event System

A practical example using all four function types.

// Function declaration: utility (leverages hoisting)
function createEventEmitter() {
// Function expression: internal implementation (private state via closure)
const listeners = new Map();

return {
// Method shorthand: public API
on(event, callback) {
if (!listeners.has(event)) {
listeners.set(event, []);
}
listeners.get(event).push(callback);
return this;
},

off(event, callback) {
if (listeners.has(event)) {
const filtered = listeners.get(event).filter(cb => cb !== callback);
listeners.set(event, filtered);
}
return this;
},

emit(event, ...args) {
if (listeners.has(event)) {
// Arrow function: no this concerns inside callback
listeners.get(event).forEach(callback => callback(...args));
}
return this;
},

once(event, callback) {
// Function expression: one-time handler wrapper
const wrapper = function(...args) {
callback(...args);
this.off(event, wrapper);
}.bind(this);

return this.on(event, wrapper);
},
};
}

const emitter = createEventEmitter();

emitter
.on("data", (msg) => console.log(`Received: ${msg}`))
.on("data", (msg) => console.log(`Log: ${msg}`))
.once("connect", () => console.log("Connected (first time only)"));

emitter.emit("connect"); // Connected (first time only)
emitter.emit("connect"); // Nothing happens
emitter.emit("data", "Hello"); // Received: Hello \n Log: Hello

Comparison Table

FeatureFunction DeclarationFunction ExpressionArrow FunctionMethod Shorthand
HoistingFull hoistingVariable onlyVariable onlyN/A (inside object/class)
this bindingBased on call siteBased on call siteCaptures outer scopeBased on call site
arguments objectYesYesNoYes
new constructorYesYesNoNo
super keywordNoNoNoYes
Self-referenceBy nameNamed expr onlyVia outer variableBy name
Primary use caseStandalone utilities, recursionVariable assignment, callbacksCallbacks, inner methodsObject/class methods

When to Use Each

Use function declaration when:

  • Standalone utility functions at the top level
  • Recursive functions
  • Intentional use of hoisting

Use function expression when:

  • Conditionally assigning a function to a variable
  • Immediately Invoked Function Expressions (IIFE)
  • Named function for better debugging visibility

Use arrow function when:

  • Array method callbacks (map, filter, reduce)
  • Callbacks defined inside class/object methods
  • Simple transformation/return functions

Use method shorthand when:

  • Methods in object literals
  • Instance/static methods in classes
  • Override methods that need super

Pro Tips

Tip 1: IIFE (Immediately Invoked Function Expression) for scope isolation

// Scope isolation pattern from the pre-module era
const counter = (function() {
let count = 0;
return {
increment: () => ++count,
decrement: () => --count,
value: () => count,
};
})();

counter.increment();
counter.increment();
console.log(counter.value()); // 2

Tip 2: Simulating function overloading

// JavaScript doesn't support overloading — use conditional branching
function process(input) {
if (typeof input === "string") return input.toUpperCase();
if (typeof input === "number") return input * 2;
if (Array.isArray(input)) return input.map(process);
throw new TypeError(`Unsupported type: ${typeof input}`);
}

console.log(process("hello")); // HELLO
console.log(process(21)); // 42
console.log(process(["a", 1, "b"])); // ["A", 2, "B"]

Tip 3: Function returning function — deferred execution

// Function factory capturing configuration via closure
const createLogger = (prefix, level = "INFO") => {
const timestamp = () => new Date().toISOString();
return (message) => console.log(`[${timestamp()}] [${level}] [${prefix}] ${message}`);
};

const appLog = createLogger("App");
const dbLog = createLogger("DB", "DEBUG");

appLog("Server started"); // [2024-01-01T00:00:00.000Z] [INFO] [App] Server started
dbLog("Query executed"); // [2024-01-01T00:00:00.000Z] [DEBUG] [DB] Query executed
Advertisement