You need Node installed, and to know how to run a
.js file. If node --version in a terminal
prints a version number, you are ready. If it does not — or if you
have never opened a terminal — do
Getting Started with
JavaScript first; it installs Node and runs your first program in about
fifteen minutes. Nothing else is needed.
Function Declarations
A function is a reusable block of code that performs a specific task. In JavaScript, the most traditional way to define a function is with a function declaration:
// Function declaration
function greet(name) {
console.log(`Hello, ${name}!`);
}
// Calling the function
greet("Alice"); // "Hello, Alice!"
greet("Bob"); // "Hello, Bob!"
// A function that performs a calculation
function add(a, b) {
return a + b;
}
const result = add(3, 5);
console.log(result); // 8
Function declarations are hoisted, which means you can call them before they appear in your code:
// This works — declarations are hoisted
sayHello(); // "Hello!"
function sayHello() {
console.log("Hello!");
}
Function Expressions
A function expression assigns a function to a variable. The function itself can be anonymous (no name) or named:
// Anonymous function expression
const greet = function(name) {
console.log(`Hello, ${name}!`);
};
greet("Alice"); // "Hello, Alice!"
// Named function expression (useful for debugging/recursion)
const factorial = function fact(n) {
if (n <= 1) return 1;
return n * fact(n - 1);
};
console.log(factorial(5)); // 120
Unlike declarations, function expressions are not hoisted. You must define them before you use them:
// This will NOT work — expressions are not hoisted
// sayHi(); // TypeError: sayHi is not a function
const sayHi = function() {
console.log("Hi!");
};
sayHi(); // "Hi!" — works after the definition
Arrow Functions
Arrow functions (introduced in ES6) provide a shorter syntax for writing functions. They are especially popular for callbacks and short operations:
// Standard function expression
const add = function(a, b) {
return a + b;
};
// Arrow function equivalent
const addArrow = (a, b) => {
return a + b;
};
// Concise body — if there's just one expression, you can omit {} and return
const addShort = (a, b) => a + b;
// Single parameter — parentheses are optional
const double = n => n * 2;
// No parameters — empty parentheses required
const getRandom = () => Math.random();
// Examples in action
console.log(addShort(3, 5)); // 8
console.log(double(4)); // 8
console.log(getRandom()); // 0.7234... (random)
Arrow functions are not just shorter syntax — they behave differently
with this. Arrow functions do not have their own this
binding; they inherit this from the surrounding scope. This
makes them ideal for callbacks but unsuitable for object methods that
need to reference the object via this. Regular functions
get their own this based on how they are called.
// Arrow functions and this
const counter = {
count: 0,
// Regular function — 'this' refers to counter
increment: function() {
this.count++;
console.log(this.count);
},
// Arrow function — 'this' is inherited (NOT counter!)
// incrementArrow: () => {
// this.count++; // 'this' is the outer scope, not counter
// }
};
counter.increment(); // 1
counter.increment(); // 2
// Where arrow functions shine: callbacks
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(n => n * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
Parameters and Default Values
Functions can accept parameters (inputs). JavaScript is flexible — you can call a function with fewer or more arguments than defined:
function greet(name, greeting) {
console.log(`${greeting}, ${name}!`);
}
greet("Alice", "Hello"); // "Hello, Alice!"
greet("Bob"); // "undefined, Bob!" — missing args become undefined
// Default parameters (ES6)
function greetDefault(name, greeting = "Hello") {
console.log(`${greeting}, ${name}!`);
}
greetDefault("Alice", "Hi"); // "Hi, Alice!"
greetDefault("Bob"); // "Hello, Bob!" — uses default
// Default values can be expressions
function createUser(name, role = "viewer", joinDate = new Date()) {
return { name, role, joinDate };
}
console.log(createUser("Alice"));
// { name: "Alice", role: "viewer", joinDate: [current date] }
console.log(createUser("Bob", "admin"));
// { name: "Bob", role: "admin", joinDate: [current date] }
Rest Parameters
// Collect remaining arguments into an array
function sum(...numbers) {
let total = 0;
for (const num of numbers) {
total += num;
}
return total;
}
console.log(sum(1, 2, 3)); // 6
console.log(sum(10, 20, 30, 40)); // 100
// Rest must be the last parameter
function logAll(first, ...rest) {
console.log(`First: ${first}`);
console.log(`Rest: ${rest}`);
}
logAll("a", "b", "c", "d");
// "First: a"
// "Rest: b,c,d"
Return Values
Functions send data back using return. A function without a
return statement (or with an empty return) returns
undefined.
// Returning a value
function multiply(a, b) {
return a * b;
}
const product = multiply(4, 5);
console.log(product); // 20
// Early return (guard clause pattern)
function divide(a, b) {
if (b === 0) {
return "Cannot divide by zero";
}
return a / b;
}
console.log(divide(10, 2)); // 5
console.log(divide(10, 0)); // "Cannot divide by zero"
// Returning objects (wrap in parentheses with arrow functions)
const makeUser = (name, age) => ({ name, age });
console.log(makeUser("Alice", 25)); // { name: "Alice", age: 25 }
// Functions without return give undefined
function doSomething() {
console.log("Working...");
// no return statement
}
const result = doSomething(); // prints "Working..."
console.log(result); // undefined
Scope
Scope determines where variables are accessible. JavaScript has three types of scope:
Global Scope
// Variables declared outside any function or block are global
const appName = "MyApp";
let userCount = 0;
function showApp() {
// Global variables are accessible everywhere
console.log(appName); // "MyApp"
userCount++;
}
showApp();
console.log(userCount); // 1
Function (Local) Scope
function calculateTax(amount) {
const taxRate = 0.2; // local to this function
const tax = amount * taxRate;
return tax;
}
console.log(calculateTax(100)); // 20
// console.log(taxRate); // ReferenceError: taxRate is not defined
// console.log(tax); // ReferenceError: tax is not defined
Block Scope
// let and const are block-scoped (limited to { })
if (true) {
const blockConst = "I'm block-scoped";
let blockLet = "Me too";
console.log(blockConst); // works
console.log(blockLet); // works
}
// console.log(blockConst); // ReferenceError
// console.log(blockLet); // ReferenceError
// for loop scope
for (let i = 0; i < 3; i++) {
// i only exists inside this loop
}
// console.log(i); // ReferenceError
Variables declared with var are function-scoped, not
block-scoped. This means a var inside an if,
for, or while block leaks out to the surrounding
function. It also gets "hoisted" to the top of the function (the declaration
moves up, but the assignment stays where it is), which can cause variables
to be undefined unexpectedly.
// var hoisting demonstration
console.log(x); // undefined (not an error — var is hoisted)
var x = 5;
console.log(x); // 5
// What JavaScript actually sees:
// var x; // declaration hoisted to top
// console.log(x); // undefined
// x = 5; // assignment stays here
// console.log(x); // 5
// let/const do NOT allow this
// console.log(y); // ReferenceError: Cannot access 'y' before initialization
// let y = 5;
Closures
A closure is a function that remembers the variables from the scope where it was created, even after that scope has finished executing. This is one of JavaScript's most powerful features.
// Basic closure
function createGreeter(greeting) {
// The inner function "closes over" the greeting variable
return function(name) {
console.log(`${greeting}, ${name}!`);
};
}
const sayHello = createGreeter("Hello");
const sayHi = createGreeter("Hi");
sayHello("Alice"); // "Hello, Alice!"
sayHi("Bob"); // "Hi, Bob!"
// greeting is "remembered" even though createGreeter has returned
Practical Closure: Counter
function createCounter(start = 0) {
let count = start;
return {
increment: () => ++count,
decrement: () => --count,
getCount: () => count,
reset: () => { count = start; }
};
}
const counter = createCounter(10);
console.log(counter.getCount()); // 10
counter.increment();
counter.increment();
console.log(counter.getCount()); // 12
counter.decrement();
console.log(counter.getCount()); // 11
counter.reset();
console.log(counter.getCount()); // 10
// count is private — there's no way to access it directly
// console.log(counter.count); // undefined
Closure Gotcha: Loops
// Common mistake with var in loops
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // prints 3, 3, 3 (not 0, 1, 2)
}, 100);
}
// var is function-scoped, so all callbacks share the same i
// Fix: use let (block-scoped — each iteration gets its own i)
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // prints 0, 1, 2
}, 100);
}
Callback Functions
A callback is a function passed as an argument to another function. Callbacks are fundamental to JavaScript — they're used for event handling, array methods, timers, and asynchronous operations.
// Passing a function as an argument
function doMath(a, b, operation) {
return operation(a, b);
}
const sum = doMath(5, 3, (a, b) => a + b);
const product = doMath(5, 3, (a, b) => a * b);
console.log(sum); // 8
console.log(product); // 15
Callbacks with Array Methods
const numbers = [1, 2, 3, 4, 5];
// forEach — run a function on each element
numbers.forEach(num => console.log(num * 2));
// 2, 4, 6, 8, 10
// map — transform each element (returns new array)
const doubled = numbers.map(num => num * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
// filter — keep elements that pass a test
const evens = numbers.filter(num => num % 2 === 0);
console.log(evens); // [2, 4]
// find — get the first element that passes a test
const firstBig = numbers.find(num => num > 3);
console.log(firstBig); // 4
Callbacks with Timers
// setTimeout — run once after a delay (milliseconds)
setTimeout(() => {
console.log("This runs after 2 seconds");
}, 2000);
// setInterval — run repeatedly at an interval
let count = 0;
const intervalId = setInterval(() => {
count++;
console.log(`Tick ${count}`);
if (count >= 5) {
clearInterval(intervalId); // stop after 5 ticks
}
}, 1000);
Callbacks with Event Listeners
// In a browser environment:
// document.getElementById("myButton").addEventListener("click", function(event) {
// console.log("Button clicked!");
// console.log(event.target); // the element that was clicked
// });
// Arrow function version:
// document.getElementById("myButton").addEventListener("click", (event) => {
// console.log("Button clicked!");
// });
Now Do It Yourself: Five Steps
JavaScript has two ways to write a function and they are not interchangeable. You will write both, give one a default, and meet the two errors that account for most beginner debugging. Every output below is exactly what Node printed.
Go: open a terminal, run cd, and create funcs.js.
Do: type these four lines and run node funcs.js.
function greet(name) {
return "Hello, " + name + "!";
}
console.log(greet("Alice"));
You should see: Hello, Alice!. Declaring a function does nothing on its own; the last line is what runs it.
If not: if nothing prints, you have the declaration and no call. If you get undefined, read step 2 — that is a different problem and a very common one.
Go: same file, replace the contents.
Do: type these four lines and run it. This one is meant to look wrong.
function add(a, b) {
a + b;
}
console.log(add(2, 3));
You should see: undefined. The addition happened and the answer was thrown away, because nothing returned it. Every JavaScript function returns undefined unless you say otherwise.
If not: if you see 5, you already added return. Take it out and look at undefined once on purpose — recognising it instantly is worth real time later, because it almost always means a function forgot to return.
Go: same file, replace the contents.
Do: type these three lines and run it. Those are backticks.
const shout = (name) => `HELLO, ${name.toUpperCase()}!`;
console.log(shout("bob"));
console.log(typeof shout);
You should see: HELLO, BOB! then function. An arrow function with no braces returns its single expression automatically — there is no return to forget. Add braces and you must write return again.
If not: the difference that bites later is this: an arrow function does not get its own, which is exactly why arrows are the right choice inside callbacks and the wrong choice for an object's method. For now, prefer arrows for short one-expression helpers.
Go: same file, replace the contents.
Do: type these five lines and run it.
function add(a, b = 10) {
return a + b;
}
console.log(add(5));
console.log(add(5, 20));
You should see: 15 then 25. The default applies when the argument is left out, and is overridden when it is supplied — which is how you add an option without breaking every existing call.
If not: note that the default fires for undefined but not for null: add(5, null) gives 5, because null counts as a value you supplied. That distinction catches people out when a value arrives from JSON.
Go: same file, replace the contents.
Do: type these four lines and run it. This is meant to fail.
function g(n) {
return n.toUpperCase();
}
console.log(g());
You should see: TypeError: Cannot read properties of undefined (reading 'toUpperCase'). Read it backwards: something was undefined, and you asked that nothing for toUpperCase. The missing argument became undefined.
If not: 🔴 Unlike Python, JavaScript does not complain about a missing argument. It hands you undefined and lets you continue until something else breaks — often far away from the real cause. That is why this specific message is worth memorising: it nearly always means a value never arrived.
Without scrolling up: your function runs correctly but the caller keeps getting
undefined. Name the two possible causes. Answer: the function has no
return, or you passed no argument and it is returning
undefined from a value that never arrived. Both produce the same symptom
in different places.
Now do it without the page: write area(width, height)
as an arrow function with height defaulting to 1, and print
area(5) and area(5, 3). Steps 3 and 4 joined up.
Summary
- Function declarations are hoisted — can be called before they appear in code
- Function expressions assign functions to variables — not hoisted
- Arrow functions provide concise syntax and inherit
thisfrom their scope - Default parameters let you set fallback values:
(name = "World") - Rest parameters collect extra arguments:
(...args) - Functions return
undefinedunless you explicitlyreturna value - Scope: global, function (local), and block (
let/constinside{ }) - Closures remember variables from their creation scope — essential for data privacy and factory patterns
- Callbacks are functions passed to other functions — used everywhere in JavaScript
You now understand how to write, organize, and compose functions in JavaScript. Next up: arrays and objects — the two most important data structures you will work with every day.