Functions are the building blocks of JavaScript programs. The language offers function declarations, function expressions, arrow functions, methods, and constructor functions. Each form has a small but important difference in how it binds this and how it is hoisted. This lesson walks through all of them and the patterns you will use in real code.
Function declaration
A function declaration starts with the function keyword, has a name, and is hoisted to the top of its scope. You can call it from code that appears textually before the declaration.
console.log(square(5)); // 25
function square(n) {
return n * n;
}
Function expression
A function expression assigns a function to a binding. It is not hoisted, so you cannot call it before the line that creates it. The name after function is optional and only visible inside the function body.
const square = function (n) {
return n * n;
};
console.log(square(5)); // 25
Arrow functions
Arrow functions are a compact form. The body is implicitly returned when it is a single expression. Use them for short callbacks. Avoid them for object methods where you need this to refer to the receiver.
const square = n => n * n;
const add = (a, b) => a + b;
const greet = name => `Hello, ${name}!`;
const stats = nums => ({
min: Math.min(...nums),
max: Math.max(...nums),
avg: nums.reduce((a, b) => a + b, 0) / nums.length,
});
console.log(square(5));
console.log(add(2, 3));
console.log(greet("Asha"));
console.log(stats([1, 2, 3, 4, 5]));
Default and rest parameters
Default parameter values fill in when the argument is the missing-value primitive. A call that passes no value for that parameter, or explicitly passes the missing-value primitive, both trigger the default. Note that other falsy values such as 0 or the empty string do NOT trigger the default, only the missing-value primitive. Rest parameters collect the rest of the arguments into a real array. They replaced the array-like arguments object in modern code.
function makeCoffee(size, drink = "espresso", sugar = 0) {
return `${size} ${drink}, ${sugar} sugar`;
}
console.log(makeCoffee("large")); // large espresso, 0 sugar
console.log(makeCoffee("medium", "latte")); // medium latte, 0 sugar
console.log(makeCoffee("small", "cappuccino", 2)); // small cappuccino, 2 sugar
function max(...nums) {
return nums.reduce((a, b) => a > b ? a : b);
}
console.log(max(1, 5, 3, 9, 2)); // 9
this and the difference that matters
Regular functions bind this based on how they are called. Arrow functions inherit this from the enclosing lexical scope and do not have their own. That single difference explains most of the bugs you will see in real code.
const timer = {
seconds: 0,
start() {
setInterval(() => {
this.seconds += 1; // arrow inherits this from start()
console.log(this.seconds);
}, 1000);
},
};
// timer.start(); // uncomment to see it run
IIFE and the module pattern
An IIFE is a function expression that runs immediately. It used to be the way to create a private scope. Modern modules (covered in a later lesson) make IIFEs mostly obsolete, but you will see them in older code.
(function () {
const secret = "hidden";
console.log("IIFE ran with secret =", secret);
})();
// secret is not visible here
Summary
Use function declarations for top-level helpers, function expressions when you need a value, and arrow functions for short callbacks and lexical this. The next lesson covers closures and the scope chain, which is what makes lexical this, factory functions, and module patterns possible.
Best practices for writing functions
Keep functions small. A function that fits on one screen and does one thing is far easier to test and reuse than a function that scrolls for two pages. Use descriptive parameter names; a function named createUser(name, email) is more readable than createUser(a, b). Keep the number of parameters low; if a function needs more than three or four, pass an options object.
// Bad: many positional parameters
function createUser(name, email, age, role, active, sendWelcome) { /* ... */ }
// Better: one options object with sensible defaults
function createUser({ name, email, age = 18, role = "viewer", active = true, sendWelcome = true }) {
return { name, email, age, role, active, sendWelcome };
}
console.log(createUser({ name: "Asha", email: "[email protected]" }));
call, apply, and bind for explicit this
Regular functions expose three methods that let you control what this refers to. call(thisArg, ...args) calls the function immediately with the given this and arguments. apply(thisArg, [args]) does the same with an array of arguments. bind(thisArg, ...args) returns a new function that calls the original with the bound this and any leading arguments.
function greet(greeting, punctuation) {
return greeting + ", " + this.name + punctuation;
}
const person = { name: "Asha" };
console.log(greet.call(person, "Hello", "!")); // Hello, Asha!
console.log(greet.apply(person, ["Hi", "?"])); // Hi, Asha?
const bound = greet.bind(person, "Hey");
console.log(bound(".")); // Hey, Asha.
Pure functions and why they matter
A pure function returns the same value for the same arguments and has no side effects. Pure functions are easy to test, easy to memoize, and easy to reason about. Most of your utility functions should be pure. Reserve side effects for the edges of your program: input handling, network calls, persistence, and the DOM. The arrow function form is a strong signal of intent: a pure transform takes inputs and returns a value without touching anything outside.
// Pure
const addTax = (price, rate = 0.2) => price * (1 + rate);
// Impure (writes outside)
let total = 0;
function addToTotal(price) {
total += price; // side effect
}
Comparison with Python functions
Python and JavaScript both treat functions as first-class values you can assign, pass, and return. Python's def is roughly equivalent to a JavaScript function declaration, and Python's lambda is roughly equivalent to an arrow function. The big differences: Python supports keyword arguments natively, while JavaScript uses an options object. Python's generator functions use yield, like JavaScript's function*, but JavaScript's iteration protocol is async by default and Python's is synchronous unless you opt into asyncio.