JavaScript Zero to Advanced #4: Functions, Arrow Functions, Scope & Closures
Functions are the heart of JavaScript. They let you give a name to a piece of logic, reuse it as many times as you want, and pass it around like any other value. In this part you will learn the different ways to write functions, how parameters and return values work, and two concepts that every JavaScript developer must understand: scope and closures.
Function declarations
A function declaration starts with the function keyword, a name, a list of parameters, and a body. Use return to send a value back to the caller:
function greet(name) {
return `Hello, ${name}!`
}
function add(a, b) {
return a + b
}
console.log(greet("Budi"))
console.log(add(7, 5))
console.log(add(add(1, 2), 3))
Output:
Hello, Budi!
12
6
A function without a return statement returns undefined. Parameters that are not passed are also undefined, so you can give them default values:
function logMessage(message) {
console.log(`[LOG] ${message}`)
}
const result = logMessage("Server started")
console.log(result)
function createUser(name, role = "member") {
return `${name} (${role})`
}
console.log(createUser("Siti"))
console.log(createUser("Andi", "admin"))
Output:
[LOG] Server started
undefined
Siti (member)
Andi (admin)
Function expressions and arrow functions
Functions are values. You can store one in a variable, which is called a function expression. ES2015 added a shorter syntax: the arrow function.
// Function expression
const square = function (n) {
return n * n
}
// Arrow function with a body
const cube = (n) => {
return n * n * n
}
// Arrow function with an implicit return (one expression, no braces)
const double = (n) => n * 2
// No parameters
const sayHi = () => "Hi!"
console.log(square(4), cube(3), double(21), sayHi())
Output:
16 27 42 Hi!
When an arrow function body is a single expression, you can drop the braces and the return. If you want to return an object literal that way, wrap it in parentheses: () => ({ ok: true }).
Arrow functions also differ in how they handle this: they do not get their own this, they use the one from the surrounding code. That matters inside objects and classes, which we will cover in Parts 6 and 9.
Functions as arguments (callbacks)
Because functions are values, you can pass them to other functions. A function passed this way is called a callback:
function repeat(times, action) {
for (let i = 1; i <= times; i++) {
action(i)
}
}
repeat(3, (n) => console.log(`Run number ${n}`))
Output:
Run number 1
Run number 2
Run number 3
You will use callbacks all the time: in array methods (Part 5), event listeners (Part 7) and asynchronous code (Part 8).
Scope: where variables live
Scope decides where a variable can be accessed. JavaScript has three levels:
- Global scope: declared outside any function or block; visible everywhere.
- Function scope: declared inside a function; visible only inside that function.
- Block scope: declared with
letorconstinside{ }(anif, a loop); visible only inside that block.
const appName = "FullStackNotes" // global
function showInfo() {
const version = "1.0" // function scope
if (true) {
const status = "active" // block scope
console.log(appName, version, status)
}
console.log(typeof status) // status is not visible here
}
showInfo()
console.log(typeof version) // version is not visible here
Output:
FullStackNotes 1.0 active
undefined
undefined
Inner scopes can read variables from outer scopes, but not the other way around. When two variables have the same name, the inner one "shadows" the outer one.
Why var is confusing
var ignores block scope; it is only function-scoped. That leads to classic bugs:
if (true) {
var leaked = "I escaped the block"
let contained = "I stay inside"
}
console.log(leaked)
console.log(typeof contained)
Output:
I escaped the block
undefined
Hoisting
Before running your code, JavaScript scans for declarations. Function declarations are fully "hoisted", so you can call them before they appear in the file. Variables declared with let and const exist but cannot be used before their line (this period is called the temporal dead zone):
console.log(multiply(3, 4)) // works: declarations are hoisted
function multiply(a, b) {
return a * b
}
try {
console.log(divide(10, 2))
} catch (error) {
console.log(error.name + ": " + error.message)
}
const divide = (a, b) => a / b
Output:
12
ReferenceError: Cannot access 'divide' before initialization
In practice: declare things before you use them, and you will rarely need to think about hoisting.
Closures
A closure is a function that remembers the variables from the scope where it was created, even after that outer function has finished running. This sounds abstract, so let's look at an example:
function createCounter() {
let count = 0 // private variable
return function () {
count++
return count
}
}
const counterA = createCounter()
const counterB = createCounter()
console.log(counterA())
console.log(counterA())
console.log(counterA())
console.log(counterB())
Output:
1
2
3
1
createCounter has already returned, but the inner function still has access to count. Each call to createCounter creates a new count, which is why counterB starts from 1. Nothing outside can touch count directly: closures give you private state.
Closures are also handy for creating specialized functions, sometimes called "function factories":
function makeDiscount(percent) {
return (price) => price - (price * percent) / 100
}
const tenOff = makeDiscount(10)
const halfPrice = makeDiscount(50)
console.log(tenOff(200000))
console.log(halfPrice(200000))
Output:
180000
100000
The classic loop closure bug
for (var i = 1; i <= 3; i++) {
setTimeout(() => console.log("var:", i), 0)
}
for (let j = 1; j <= 3; j++) {
setTimeout(() => console.log("let:", j), 0)
}
Output:
var: 4
var: 4
var: 4
let: 1
let: 2
let: 3
With var there is only one i shared by all callbacks, and by the time they run the loop has finished with i = 4. With let each iteration gets its own j, so each closure remembers a different value. One more reason to prefer let.
Tips and common mistakes
- Forgetting
return. A function with braces that does not return givesundefined, a very common bug with arrow functions:(n) => { n * 2 }returns nothing. - Calling vs passing.
repeat(3, action)passes the function;repeat(3, action())calls it immediately and passes its result. - Keep functions small. A function should do one thing. If you need the word "and" to describe it, consider splitting it.
- Name things clearly. Use verbs for functions:
calculateTotal,formatDate,isValidEmail. - Avoid changing global variables inside functions; return a value instead. It makes code easier to test.
Exercise
Write a function createBankAccount(initialBalance) that uses a closure to keep the balance private. It should return an object with three functions: deposit(amount), withdraw(amount) (refuse if the balance is not enough) and getBalance(). Test it by depositing 50000, withdrawing 20000, and then trying to withdraw 1000000.
Summary
- Functions can be declared with
function, stored as expressions, or written as arrow functions. - Default parameters handle missing arguments; functions without
returngiveundefined. - Functions are values, so you can pass them as callbacks and return them from other functions.
- Scope is global, function or block;
let/constrespect blocks,vardoes not. - A closure remembers its outer variables, which is great for private state and function factories.
In Part 5 we move to arrays and the powerful methods map, filter, reduce, find and sort, where callbacks really shine.
Related articles
PostgreSQL Zero to Advanced #1: Introduction & Setup
Start the series by understanding what a relational database is, installing PostgreSQL locally, and getting comfortable with psql commands like \l, \c and \dt.
PHP Zero to Advanced #1: Introduction & Setup
Start your PHP journey: what PHP is, how to install PHP 8.2+, run the built-in development server, and write your very first script with echo.
Ruby on Rails Zero to Advanced #1: Ruby Basics
Start the Rails journey with the language underneath it: irb, variables, strings, puts vs p, and symbols explained with runnable examples.