A function is a named, reusable block of code that performs a specific task. Instead of writing the same instructions over and over, you write them once inside a function and call the function whenever you need it.
Real-world analogy: A function is like a recipe card. The recipe is written once. You can follow it as many times as you want, using different ingredients (arguments) each time, and it always produces the same kind of result.
Functions solve three fundamental problems in programming:
| Problem | How Functions Help |
|---|---|
| Repetition | Write once, call many times |
| Organisation | Group related code under a meaningful name |
| Abstraction | Use complex logic without needing to understand the internals every time |
This is the most traditional way to define a function.
function greet(name) {
return "Hello, " + name + "!";
}
console.log(greet("Tunde")); // Output: Hello, Tunde!
console.log(greet("Sara")); // Output: Hello, Sara!Anatomy of a function declaration:
function greet (name) {
│ │ │ │
keyword name parameter(s) body begins| Part | Description |
|---|---|
function | Keyword that tells JavaScript a function is being defined |
greet | The name of the function · used to call it later |
(name) | Parameters · placeholder variables for values passed in |
{ ... } | The body · the instructions that run when the function is called |
return | Sends a value back to whoever called the function |
💡 Parameters vs Arguments · what's the difference?
- Parameters are the placeholder names listed in the function definition:
function greet(name)·nameis a parameter.- Arguments are the actual values passed in when you call the function:
greet("Tunde")·"Tunde"is an argument.Think of a parameter as a blank form field, and an argument as the value you write in it.
Key feature · hoisting:
Function declarations are hoisted, which means JavaScript moves them to the top of the current scope before running any code. You can call a declared function before it appears in the file:
console.log(add(3, 4)); // Output: 7 ✅ — works even though the function is below
function add(a, b) {
return a + b;
}A function expression defines a function and assigns it to a variable.
const multiply = function(a, b) {
return a * b;
};
console.log(multiply(4, 5)); // Output: 20The function on the right side of = has no name · it is an anonymous function. The variable multiply holds a reference to it.
⚠️ Function expressions are NOT hoisted:
console.log(multiply(4, 5)); // ❌ ReferenceError: Cannot access 'multiply' before initialization
const multiply = function(a, b) {
return a * b;
};The variable multiply exists, but its value (the function) hasn't been assigned yet at the point of the call.
Named function expression:
You can give the function inside an expression a name, which is only visible inside the function itself (useful for recursion):
const factorial = function fact(n) {
if (n <= 1) return 1;
return n * fact(n - 1); // 'fact' is accessible here
};
console.log(factorial(5)); // Output: 120
// console.log(fact(5)); // ❌ ReferenceError — 'fact' not visible outsideArrow functions are a shorter syntax introduced in ES2015 (ES6). They are especially popular in modern JavaScript.
Basic syntax:
const square = (n) => {
return n * n;
};
console.log(square(6)); // Output: 36Shorthand rules:
| Situation | Shorthand |
|---|---|
| Single parameter | Parentheses optional: n => n * n |
| Single-line body with a return | Curly braces and return optional (implicit return) |
| No parameters | Empty parentheses required: () => "Hello" |
// Full syntax:
const square = (n) => { return n * n; };
// Concise syntax (equivalent):
const square = n => n * n;
console.log(square(7)); // Output: 49Multi-parameter arrow function:
const add = (a, b) => a + b;
console.log(add(10, 3)); // Output: 13⚠️ Critical difference: Arrow functions and
thisArrow functions do NOT have their ownthisbinding · they inheritthisfrom the surrounding context. This is explored in depth in Chapter 4. For now, just know that arrow functions and regular functions behave differently when used inside objects or event handlers.
If a caller doesn't pass a value for a parameter, you can provide a fallback:
function greet(name = "Guest") {
return "Welcome, " + name + "!";
}
console.log(greet("Amara")); // Output: Welcome, Amara!
console.log(greet()); // Output: Welcome, Guest!How defaults work:
Default values are used when the argument is undefined · either because it was not passed at all, or was explicitly passed as undefined. Passing null does NOT trigger the default:
function show(value = "default") {
console.log(value);
}
show(); // Output: default ← argument is missing (undefined)
show(undefined); // Output: default ← explicitly undefined
show(null); // Output: null ← null is a real value, not undefined
show("hello"); // Output: helloRest parameters allow a function to accept any number of arguments, collected into an array.
function sum(...numbers) {
let total = 0;
for (const n of numbers) {
total += n;
}
return total;
}
console.log(sum(1, 2)); // Output: 3
console.log(sum(1, 2, 3, 4, 5)); // Output: 15
console.log(sum()); // Output: 0The ... before numbers says "collect all remaining arguments into an array named numbers."
⚠️ Rules for rest parameters:
- The rest parameter must be the last parameter:
function f(a, b, ...rest)✅- There can only be one rest parameter per function
function f(...rest, a)❌ SyntaxError
Combining regular and rest parameters:
function introduce(greeting, ...names) {
names.forEach(name => console.log(greeting + ", " + name + "!"));
}
introduce("Hello", "Alice", "Bob", "Charlie");
// Output:
// Hello, Alice!
// Hello, Bob!
// Hello, Charlie!Before rest parameters existed, all regular functions had access to an arguments object · an array-like object containing all passed arguments.
function logAll() {
console.log(arguments.length); // How many arguments were passed
for (let i = 0; i < arguments.length; i++) {
console.log(arguments[i]);
}
}
logAll("a", "b", "c");
// Output:
// 3
// a
// b
// c⚠️
argumentsis NOT available in arrow functions. Arrow functions have noargumentsobject · use rest parameters instead.
| Feature | arguments object | Rest parameters (...args) |
|---|---|---|
| Type | Array-like object | Real array |
| Arrow function support | ❌ No | ✅ Yes |
Array methods (.map, .filter) | ❌ Not directly | ✅ Yes |
| Modern best practice | Avoid | Preferred |
arguments Object (Classic Functions Only). Open your editor, type the examples above by hand, modify them, and observe what changes.In JavaScript, functions are first-class citizens · they are treated like any other value. This means you can:
// 1. Assign a function to a variable
const sayHi = function() { return "Hi!"; };
// 2. Pass a function as an argument to another function
function callIt(fn) {
return fn();
}
console.log(callIt(sayHi)); // Output: Hi!
// 3. Return a function from another function
function makeMultiplier(x) {
return function(y) {
return x * y;
};
}
const triple = makeMultiplier(3);
console.log(triple(5)); // Output: 15
console.log(triple(10)); // Output: 30
// 4. Store functions in arrays and objects
const operations = [
(a, b) => a + b,
(a, b) => a - b,
(a, b) => a * b,
];
console.log(operations[0](10, 4)); // Output: 14
console.log(operations[2](10, 4)); // Output: 40This is the foundation of functional programming and makes concepts like callbacks and closures possible.
Every function returns a value. If you don't write a return statement, the function returns undefined automatically.
function noReturn() {
let x = 5; // Does something but returns nothing
}
const result = noReturn();
console.log(result); // Output: undefinedreturn also stops the function immediately:
function checkAge(age) {
if (age < 0) {
return "Invalid age"; // Exits here — nothing below runs
}
if (age < 18) {
return "Minor";
}
return "Adult";
}
console.log(checkAge(-1)); // Output: Invalid age
console.log(checkAge(15)); // Output: Minor
console.log(checkAge(25)); // Output: Adult🤔 Thinking question: What is the return value of
console.log("Hello")? Tryconst x = console.log("Hello"); console.log(x);· what do you get?
# CHAPTER 2 · ADVANCED FUNCTION PATTERNS
undefined. Open your editor, type the examples above by hand, modify them, and observe what changes.Once you understand the basics of defining and calling functions, JavaScript opens up more sophisticated techniques. These patterns appear constantly in professional code, frameworks like React, and libraries like Lodash.
A higher-order function is a function that either:
- Takes one or more functions as arguments, OR
- Returns a function as its result (or both)
You already know addEventListener · it's a higher-order function. It accepts a function (the callback) as its second argument.
Micro-demo · accepting a function:
function applyTwice(fn, value) {
return fn(fn(value)); // Call fn on value, then call fn on the result
}
const double = n => n * 2;
console.log(applyTwice(double, 3)); // Output: 12 (3×2=6, 6×2=12)
console.log(applyTwice(double, 5)); // Output: 20 (5×2=10, 10×2=20)Micro-demo · returning a function (function factory):
function makeGreeter(greeting) {
return function(name) {
return greeting + ", " + name + "!";
};
}
const sayGoodMorning = makeGreeter("Good morning");
const sayGoodNight = makeGreeter("Good night");
console.log(sayGoodMorning("Tunde")); // Output: Good morning, Tunde!
console.log(sayGoodNight("Sara")); // Output: Good night, Sara!makeGreeter is a function factory · a function that produces customised functions.
A pure function is a function that:
- Given the same input, always returns the same output
- Has no side effects (does not change anything outside itself)
// Pure function — predictable, no external impact
function add(a, b) {
return a + b;
}
// Impure function — depends on external variable; changes it
let total = 0;
function addToTotal(n) {
total += n; // Side effect: changes a variable outside the function
return total;
}
console.log(add(2, 3)); // Always: 5
console.log(add(2, 3)); // Always: 5
addToTotal(5); // total is now 5
addToTotal(5); // total is now 10 — same call, different result!💡 Why care about pure functions?
- They are easier to test (no hidden state)
- They are predictable (same input = same output always)
- They are safe to reuse without worrying about side effects
Professional JavaScript uses pure functions wherever possible.
A recursive function is one that calls itself. It must have a base case that stops the recursion; otherwise, it runs forever and causes a stack overflow.
function countdown(n) {
if (n <= 0) { // Base case: stop here
console.log("Done!");
return;
}
console.log(n);
countdown(n - 1); // Recursive call: same function with a smaller problem
}
countdown(5);
// Output:
// 5
// 4
// 3
// 2
// 1
// Done!Classic example · factorial:
function factorial(n) {
if (n <= 1) return 1; // Base case
return n * factorial(n - 1); // Recursive case
}
console.log(factorial(5)); // Output: 120 (5×4×3×2×1)Recursive breakdown for factorial(4):
factorial(4)
→ 4 * factorial(3)
→ 3 * factorial(2)
→ 2 * factorial(1)
→ 1 (base case)
→ 2 * 1 = 2
→ 3 * 2 = 6
→ 4 * 6 = 24Function composition means combining two or more functions so the output of one becomes the input of the next · like a pipeline.
const double = n => n * 2;
const addTen = n => n + 10;
const stringify = n => "Result: " + n;
// Manual composition:
const result = stringify(addTen(double(5)));
console.log(result); // Output: Result: 20 (5→10→20→"Result: 20")
// Reusable compose function:
function compose(...fns) {
return function(value) {
return fns.reduceRight((acc, fn) => fn(acc), value);
// Applies functions right to left: stringify(addTen(double(value)))
};
}
const transform = compose(stringify, addTen, double);
console.log(transform(5)); // Output: Result: 20
console.log(transform(10)); // Output: Result: 30💡 Real-world analogy: Function composition is like an assembly line. Each station (function) does one job and passes the result to the next. This produces clean, modular code where each function has a single responsibility.
Memoisation is an optimisation technique where a function caches its results so repeated calls with the same arguments return instantly from the cache instead of recomputing.
function memoize(fn) {
const cache = {};
return function(n) {
if (cache[n] !== undefined) {
console.log("From cache: " + n);
return cache[n]; // Return cached result
}
const result = fn(n);
cache[n] = result; // Store in cache
return result;
};
}
function slowSquare(n) {
// Simulate slow computation
return n * n;
}
const fastSquare = memoize(slowSquare);
console.log(fastSquare(5)); // Computed: 25
console.log(fastSquare(5)); // From cache: 25 (instant)
console.log(fastSquare(6)); // Computed: 36
console.log(fastSquare(6)); // From cache: 36 (instant)🤔 Thinking question: In the
memoizefunction, why doescachepersist between calls even thoughmemoizehas finished running? (Hint: this is a closure · Chapter 9 explains exactly why.)
# CHAPTER 3 · CALLBACKS
A callback is a function passed as an argument to another function, to be called (executed) later · either immediately or after an asynchronous operation completes.
Real-world analogy: You call a restaurant to order food. Instead of waiting silently on the phone until the food is ready (blocking), you give the restaurant your phone number (the callback). They call you back when the food is ready. You can do other things in the meantime.
function processOrder(item, callback) {
console.log("Processing order for: " + item);
callback(item); // Call the function that was passed in
}
function deliver(item) {
console.log(item + " has been delivered!");
}
processOrder("Pizza", deliver);
// Output:
// Processing order for: Pizza
// Pizza has been delivered!A synchronous callback is called immediately · the outer function doesn't move to the next line until the callback has finished.
function greetUser(name, formatter) {
const formatted = formatter(name); // Call the callback right now
console.log("Hello, " + formatted);
}
greetUser("tunde", name => name.toUpperCase());
// Output: Hello, TUNDE
greetUser("sara", name => name[0].toUpperCase() + name.slice(1));
// Output: Hello, SaraBuilt-in array methods like .forEach(), .map(), .filter(), and .sort() all use synchronous callbacks:
const numbers = [1, 2, 3, 4, 5];
// forEach: calls the callback for each element
numbers.forEach(function(n) {
console.log(n * 10);
});
// Output: 10, 20, 30, 40, 50
// map: calls the callback for each element, collects results into a new array
const doubled = numbers.map(n => n * 2);
console.log(doubled); // Output: [2, 4, 6, 8, 10]
// filter: keeps elements for which the callback returns true
const evens = numbers.filter(n => n % 2 === 0);
console.log(evens); // Output: [2, 4]An asynchronous callback is called later · after some time has passed or after an operation completes. The rest of the code continues running in the meantime.
console.log("Start");
setTimeout(function() {
console.log("This runs after 2 seconds");
}, 2000);
console.log("End");
// Output:
// Start
// End
// (2 seconds later) This runs after 2 secondssetTimeout is a built-in function that calls a callback after a delay. Notice that "End" prints before the callback · JavaScript did not wait.
setInterval · repeated callbacks:
let count = 0;
const interval = setInterval(function() {
count++;
console.log("Tick " + count);
if (count === 3) {
clearInterval(interval); // Stop after 3 ticks
console.log("Done");
}
}, 1000);
// Output (one per second):
// Tick 1
// Tick 2
// Tick 3
// DoneWhen multiple asynchronous operations depend on each other, callbacks can nest deeply · this is called callback hell or the pyramid of doom:
// ❌ Callback hell — hard to read and maintain
getUser(userId, function(user) {
getOrders(user.id, function(orders) {
getProductDetails(orders[0].productId, function(product) {
getReviews(product.id, function(reviews) {
console.log(reviews);
// Nested 4 levels deep — and it can get worse
});
});
});
});Solutions:
- Named functions · pull callbacks out of the nesting:
function handleReviews(reviews) {
console.log(reviews);
}
function handleProduct(product) {
getReviews(product.id, handleReviews);
}
function handleOrders(orders) {
getProductDetails(orders[0].productId, handleProduct);
}
function handleUser(user) {
getOrders(user.id, handleOrders);
}
getUser(userId, handleUser);
// Same logic, but flat and readable- Promises (modern approach · each step returns a promise instead of accepting a callback)
- async/await (the cleanest modern syntax · built on top of promises)
💡 Real-world relevance: Callback hell is why Promises and async/await were invented. Every modern JavaScript developer encounters this pattern. Understanding the problem is essential for understanding why the solutions exist.
In Node.js and many libraries, callbacks follow a convention: the first argument is always an error (or null if no error occurred), and subsequent arguments are the results.
function readFile(filename, callback) {
// Simulate reading a file
const error = filename === "missing.txt" ? new Error("File not found") : null;
const data = error ? null : "File contents here";
callback(error, data);
}
readFile("hello.txt", function(err, data) {
if (err) {
console.log("Error:", err.message);
return; // Stop processing
}
console.log("Data:", data);
});
// Output: Data: File contents here
readFile("missing.txt", function(err, data) {
if (err) {
console.log("Error:", err.message);
return;
}
console.log("Data:", data);
});
// Output: Error: File not foundAlways check for errors first before using the data · this prevents crashes when operations fail.
# CHAPTER 4 · THE this KEYWORD
this is a special keyword in JavaScript that refers to the object that is currently executing the code. Its value is not fixed · it changes depending on how and where the function is called.
This makes this one of the most confusing concepts for beginners, but also one of the most powerful tools in JavaScript.
Core rule: this is not determined by where a function is written. It is determined by how the function is called.
this?. Open your editor, type the examples above by hand, modify them, and observe what changes.In a regular function called without any object context, this refers to the global object:
- In a browser:
thisis thewindowobject - In Node.js:
thisis theglobalobject
function showThis() {
console.log(this);
}
showThis(); // Output: Window {...} (in a browser)this in a Regular Function (Non-Strict Mode). Open your editor, type the examples above by hand, modify them, and observe what changes.When a function is called as a method of an object, this refers to that object:
const person = {
name: "Tunde",
greet: function() {
console.log("Hello, my name is " + this.name);
}
};
person.greet(); // Output: Hello, my name is Tundethis.name inside greet refers to person.name because greet is being called as a method of person.
Micro-demo · this changes with how you call the function:
const person = {
name: "Tunde",
greet: function() {
console.log(this.name);
}
};
person.greet(); // Output: Tunde ← called as a method
const fn = person.greet;
fn(); // Output: undefined ← called as a plain functionIn the second call, the function is extracted from the object and called on its own. It's no longer a method call, so this is no longer person.
this in an Object Method. Open your editor, type the examples above by hand, modify them, and observe what changes.When a function is used as a constructor (called with new), this refers to the newly created object:
function Car(make, model) {
this.make = make; // Attach 'make' to the new object
this.model = model; // Attach 'model' to the new object
this.describe = function() {
return this.make + " " + this.model;
};
}
const myCar = new Car("Toyota", "Corolla");
console.log(myCar.describe()); // Output: Toyota Corolla
console.log(myCar.make); // Output: ToyotaWhen new Car("Toyota", "Corolla") runs:
- A blank object is created
thisinsideCarpoints to that new object- Properties are attached to it
- The new object is returned automatically
this in a Constructor Function. Open your editor, type the examples above by hand, modify them, and observe what changes.In a DOM event listener using a regular function, this refers to the element that fired the event:
const btn = document.getElementById("my-button");
btn.addEventListener("click", function() {
console.log(this); // Output: <button id="my-button">...</button>
this.style.background = "red"; // Changes the clicked button's colour
});This is very useful for writing one handler that can work on multiple elements.
this in Event Listeners. Open your editor, type the examples above by hand, modify them, and observe what changes.Arrow functions do not have their own this. They inherit this from the enclosing lexical scope · the this value from the context where the arrow function was defined.
const team = {
name: "Dev Team",
members: ["Alice", "Bob", "Charlie"],
listMembers: function() {
// 'this' here is 'team' (regular function as a method)
this.members.forEach(member => {
// Arrow function inherits 'this' from listMembers → 'this' is still 'team'
console.log(member + " is on " + this.name);
});
}
};
team.listMembers();
// Output:
// Alice is on Dev Team
// Bob is on Dev Team
// Charlie is on Dev TeamWhat would go wrong with a regular function inside forEach:
listMembers: function() {
this.members.forEach(function(member) {
// Regular function — 'this' is now the global object, NOT 'team'
console.log(member + " is on " + this.name); // this.name is undefined!
});
}This was a very common bug before arrow functions. Arrow functions were specifically designed to fix this problem.
this in Arrow Functions. Open your editor, type the examples above by hand, modify them, and observe what changes.| Context | What this refers to |
|---|---|
| Global scope (non-strict) | window (browser) or global (Node.js) |
| Regular function, called directly | window / global (or undefined in strict mode) |
| Method of an object | The object before the dot |
Constructor (called with new) | The newly created object |
| Event listener (regular function) | The element that fired the event |
| Arrow function | Inherits from the surrounding lexical context |
.call() / .apply() | Explicitly set (see Chapter 5 & 6) |
.bind() | Permanently set (see Chapter 7) |
# CHAPTER 5 · FUNCTION.CALL()
this Summary Table. Open your editor, type the examples above by hand, modify them, and observe what changes..call() is a method available on every function. It lets you call the function while explicitly setting what this should be inside it.
function.call(thisArg, arg1, arg2, ...)| Part | Meaning |
|---|---|
thisArg | The value to use as this inside the function |
arg1, arg2, ... | Arguments to pass to the function, listed individually |
.call()?. Open your editor, type the examples above by hand, modify them, and observe what changes.function introduce() {
console.log("Hi, I'm " + this.name + " and I'm " + this.age);
}
const alice = { name: "Alice", age: 30 };
const bob = { name: "Bob", age: 25 };
introduce.call(alice); // Output: Hi, I'm Alice and I'm 30
introduce.call(bob); // Output: Hi, I'm Bob and I'm 25The introduce function uses this.name and this.age. By using .call(), we tell it "when you say this, mean alice" (or bob).
Passing arguments with .call():
function greet(greeting, punctuation) {
console.log(greeting + ", " + this.name + punctuation);
}
const person = { name: "Tunde" };
greet.call(person, "Hello", "!"); // Output: Hello, Tunde!
greet.call(person, "Good day", "."); // Output: Good day, Tunde.Arguments come after the thisArg, separated by commas.
.call() Usage. Open your editor, type the examples above by hand, modify them, and observe what changes.One of the most useful applications of .call() is method borrowing · using a method from one object on a different object, without copying it.
const dog = {
name: "Rex",
speak: function(sound) {
console.log(this.name + " says " + sound);
}
};
const cat = { name: "Whiskers" };
// Borrow dog's speak method for cat:
dog.speak.call(cat, "Meow"); // Output: Whiskers says Meowcat doesn't have its own speak method, but by using .call() we can temporarily use dog.speak with cat as the context.
.call(). Open your editor, type the examples above by hand, modify them, and observe what changes.A classic use of .call() is converting the arguments object to a real array:
function toArray() {
return Array.prototype.slice.call(arguments);
// Borrow Array's slice method and call it on the arguments object
}
console.log(toArray(1, 2, 3)); // Output: [1, 2, 3]💡 Note: In modern code, you'd just use
[...arguments]or rest parameters...args. But you'll see this pattern in older codebases.
.call() with arguments. Open your editor, type the examples above by hand, modify them, and observe what changes..call() is used to run parent constructor logic inside a child constructor:
function Animal(name, sound) {
this.name = name;
this.sound = sound;
}
function Dog(name) {
Animal.call(this, name, "Woof"); // Run Animal's constructor with Dog's 'this'
this.type = "Dog";
}
const d = new Dog("Rex");
console.log(d.name); // Output: Rex
console.log(d.sound); // Output: Woof
console.log(d.type); // Output: DogAnimal.call(this, ...) says: "Run the Animal constructor, but use the Dog instance as this." This copies all of Animal's initialisation into Dog.
# CHAPTER 6 · FUNCTION.APPLY()
.call(). Open your editor, type the examples above by hand, modify them, and observe what changes..apply() works exactly like .call(), with one key difference: instead of passing arguments individually, you pass them as an array.
function.apply(thisArg, [arg1, arg2, ...]).call() | .apply() | |
|---|---|---|
| Arguments | Listed individually | Passed as an array |
| Syntax | fn.call(obj, a, b, c) | fn.apply(obj, [a, b, c]) |
| When to use | When you know the arguments upfront | When arguments are already in an array |
.apply()?. Open your editor, type the examples above by hand, modify them, and observe what changes.function greet(greeting, punctuation) {
console.log(greeting + ", " + this.name + punctuation);
}
const person = { name: "Tunde" };
const args = ["Hello", "!"];
greet.apply(person, args); // Output: Hello, Tunde!The array args is "spread out" as individual arguments when apply calls the function.
.apply() Usage. Open your editor, type the examples above by hand, modify them, and observe what changes.Math.max() normally takes individual numbers: Math.max(1, 2, 3). It doesn't accept an array. .apply() solves this by spreading an array as individual arguments:
const scores = [85, 92, 78, 96, 88];
const highest = Math.max.apply(null, scores);
const lowest = Math.min.apply(null, scores);
console.log(highest); // Output: 96
console.log(lowest); // Output: 78null is used as the first argument because Math.max doesn't use this · we don't care what this is.
💡 Modern equivalent: The spread operator
...does the same thing more cleanly:Math.max(...scores) // Output: 96But knowing
.apply()is essential for reading older code.
.apply(). Open your editor, type the examples above by hand, modify them, and observe what changes.const arrayA = [1, 2, 3];
const arrayB = [4, 5, 6];
Array.prototype.push.apply(arrayA, arrayB);
// Equivalent to: arrayA.push(4, 5, 6)
console.log(arrayA); // Output: [1, 2, 3, 4, 5, 6]Array.prototype.push doesn't accept an array directly, but .apply() spreads arrayB into individual push arguments.
💡 Modern equivalent:
arrayA.push(...arrayB)achieves the same thing using the spread operator.
.apply(). Open your editor, type the examples above by hand, modify them, and observe what changes.C for Comma (
.call()takes comma-separated arguments) A for Array (.apply()takes an array)
fn.call(ctx, a, b, c); // C = Comma
fn.apply(ctx, [a, b, c]); // A = Array# CHAPTER 7 · FUNCTION.BIND()
.call() vs .apply() · Memory Trick. Open your editor, type the examples above by hand, modify them, and observe what changes..bind() creates a new function with this permanently locked to the object you specify. Unlike .call() and .apply() which call the function immediately, .bind() returns a new function that you can call later.
const boundFunction = fn.bind(thisArg, arg1, arg2, ...);.call() | .apply() | .bind() | |
|---|---|---|---|
| Calls the function? | Immediately | Immediately | No · returns a new function |
Sets this? | Yes | Yes | Yes, permanently |
| Arguments | Individually | As array | Can pre-set (partial application) |
.bind()?. Open your editor, type the examples above by hand, modify them, and observe what changes.const person = {
name: "Tunde",
greet: function() {
console.log("Hello, I'm " + this.name);
}
};
const greetTunde = person.greet.bind(person);
// Call it later:
greetTunde(); // Output: Hello, I'm Tunde
greetTunde(); // Output: Hello, I'm TundegreetTunde is a new function. No matter how or when you call it, this will always be person.
.bind() Usage. Open your editor, type the examples above by hand, modify them, and observe what changes.The most common use of .bind() is fixing the "lost this" problem when passing a method as a callback:
const timer = {
seconds: 0,
start: function() {
// ❌ Without bind: 'this' would be undefined or global inside setInterval
setInterval(function() {
this.seconds++; // 'this' is NOT timer here!
console.log(this.seconds);
}, 1000);
}
};Fix with .bind():
const timer = {
seconds: 0,
start: function() {
setInterval(function() {
this.seconds++;
console.log(this.seconds);
}.bind(this), 1000); // Bind 'this' (which is 'timer' at the point of call)
}
};
timer.start();
// Output (each second): 1, 2, 3, 4, ....bind(this) is called while we're still inside timer.start, where this is timer. The bound function always refers back to timer.
💡 Alternative: Arrow functions also fix this problem without
.bind(). This is why arrow functions are preferred for callbacks inside object methods.
this Problem. Open your editor, type the examples above by hand, modify them, and observe what changes..bind() can also pre-fill some of a function's arguments. This is called partial application:
function multiply(a, b) {
return a * b;
}
const double = multiply.bind(null, 2); // Pre-fill 'a' with 2
const triple = multiply.bind(null, 3); // Pre-fill 'a' with 3
console.log(double(5)); // Output: 10 (2 × 5)
console.log(double(10)); // Output: 20 (2 × 10)
console.log(triple(4)); // Output: 12 (3 × 4)null is the thisArg (we don't need this here). The 2 pre-fills the first argument a. When we call double(5), it calls multiply(2, 5).
Real-world use · specialising a general function:
function formatCurrency(currencySymbol, amount) {
return currencySymbol + amount.toFixed(2);
}
const formatUSD = formatCurrency.bind(null, "$");
const formatEuros = formatCurrency.bind(null, "€");
const formatGBP = formatCurrency.bind(null, "£");
console.log(formatUSD(19.99)); // Output: $19.99
console.log(formatEuros(24.5)); // Output: €24.50
console.log(formatGBP(9.95)); // Output: £9.95.bind(). Open your editor, type the examples above by hand, modify them, and observe what changes.function Button(label) {
this.label = label;
this.clickCount = 0;
}
Button.prototype.handleClick = function() {
this.clickCount++;
console.log(this.label + " clicked " + this.clickCount + " times");
};
const saveBtn = new Button("Save");
const deleteBtn = new Button("Delete");
document.getElementById("save-btn")
.addEventListener("click", saveBtn.handleClick.bind(saveBtn));
document.getElementById("delete-btn")
.addEventListener("click", deleteBtn.handleClick.bind(deleteBtn));Without .bind(), this inside handleClick would be the DOM button element (not the Button instance), and this.clickCount would be undefined.
# CHAPTER 8 · IIFE · IMMEDIATELY INVOKED FUNCTION EXPRESSIONS
.bind() in Event Listeners with Context. Open your editor, type the examples above by hand, modify them, and observe what changes.An IIFE (pronounced "iffy") is a function that is defined and immediately called in one expression. It runs once and disappears.
(function() {
console.log("I run immediately!");
})();
// Output: I run immediately!Anatomy of an IIFE:
( function() { } ) ()
│ │ │ │ │
│ └── function body ──┘ │ └── call it now
│ │
└─────── wrapping parens ──┘
(turn the declaration into an expression)The outer parentheses ( ) wrap the function, converting it from a declaration to an expression. The trailing () then immediately calls it.
JavaScript (before ES6) only had function scope · var variables declared inside a function are invisible outside it. But var outside a function was global.
The problem:
var counter = 0; // Global — any script on the page can access and corrupt this
function increment() {
counter++;
}If two scripts on the same page both define var counter, they overwrite each other. This was a major source of bugs in large applications.
The IIFE solution:
(function() {
var counter = 0; // Trapped inside the IIFE — completely private
function increment() {
counter++;
console.log(counter);
}
increment(); // Output: 1
increment(); // Output: 2
})();
// console.log(counter); // ❌ ReferenceError — counter doesn't exist out hereThe IIFE creates a private scope. Variables inside cannot be accessed from outside.
You can pass arguments to an IIFE:
(function(name, version) {
console.log("Loading " + name + " v" + version);
})("MyApp", "1.0.0");
// Output: Loading MyApp v1.0.0A very common pattern is passing window or document as arguments, giving them a local alias for performance and minification:
(function(win, doc) {
// Inside here, 'win' refers to the global window object
// 'doc' refers to the document object
win.myApp = {}; // Attach app to global scope
doc.getElementById("app").innerHTML = "Ready";
})(window, document);(() => {
console.log("Arrow IIFE!");
})();
// Output: Arrow IIFE!An IIFE can return a value, which is immediately captured:
const result = (function() {
const a = 10;
const b = 20;
return a + b;
})();
console.log(result); // Output: 30The module pattern uses an IIFE to create private state and exposes only selected public methods:
const bankAccount = (function() {
// Private — not accessible from outside
let balance = 0;
// Public interface — returned as an object
return {
deposit: function(amount) {
if (amount > 0) balance += amount;
console.log("Deposited £" + amount + ". Balance: £" + balance);
},
withdraw: function(amount) {
if (amount > balance) {
console.log("Insufficient funds.");
return;
}
balance -= amount;
console.log("Withdrew £" + amount + ". Balance: £" + balance);
},
getBalance: function() {
return balance;
}
};
})();
bankAccount.deposit(100); // Output: Deposited £100. Balance: £100
bankAccount.withdraw(30); // Output: Withdrew £30. Balance: £70
console.log(bankAccount.getBalance()); // Output: 70
// console.log(bankAccount.balance); // Output: undefined — 'balance' is private!balance can never be read or changed directly from outside · only through the methods the module chooses to expose. This is encapsulation, one of the core principles of good software design.
💡 Today's alternative: ES6 modules (
import/export) handle scope isolation natively. But IIFEs are still used in scripts that run in browsers without a module system, and understanding them is essential for reading legacy code.
🤔 Thinking question: In the bank account example, why can
depositandwithdrawaccessbalanceeven though the IIFE has already returned? (Hint: this is a closure · Chapter 9 explains the mechanism.)
# CHAPTER 9 · 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 the concept that makes callbacks, the module pattern, memoisation, and many other patterns work. It is arguably the most important concept in JavaScript.
Simple definition: A closure = a function + the variables it captured from its birth environment.
Before closures make sense, you need to understand lexical scoping: in JavaScript, functions can access variables from their outer (enclosing) scopes.
const outerVariable = "I'm outside";
function innerFunction() {
console.log(outerVariable); // Can access the outer variable
}
innerFunction(); // Output: I'm outsideThis works because innerFunction was defined inside the same scope as outerVariable. The function has access to its birth environment.
The magic of closures is that the inner function keeps access to the outer variables even after the outer function has finished running and its variables would normally be garbage collected.
function makeCounter() {
let count = 0; // This variable belongs to makeCounter
return function() { // This inner function is returned — it's a closure
count++;
return count;
};
}
const counter = makeCounter(); // makeCounter finishes running here
console.log(counter()); // Output: 1
console.log(counter()); // Output: 2
console.log(counter()); // Output: 3What is happening:
makeCounter()runs and creates a local variablecount = 0.makeCounter()returns the inner function.makeCounter()finishes · its execution context ends.- BUT:
countis NOT destroyed because the returned function holds a reference to it. - Every time
counter()is called, it accesses and modifies that samecount.
count is enclosed inside the returned function · hence "closure."
If you call makeCounter() twice, you get two completely separate closures with their own separate count variables:
function makeCounter() {
let count = 0;
return function() {
count++;
return count;
};
}
const counter1 = makeCounter();
const counter2 = makeCounter();
console.log(counter1()); // Output: 1
console.log(counter1()); // Output: 2
console.log(counter2()); // Output: 1 ← counter2 has its OWN count, starting at 0
console.log(counter1()); // Output: 3 ← counter1 is still goingcounter1 and counter2 are completely independent. They each have their own enclosed count.
The enclosed value can come from the outer function's parameters:
function makeAdder(x) {
return function(y) {
return x + y; // x is captured from makeAdder's scope
};
}
const add5 = makeAdder(5);
const add10 = makeAdder(10);
console.log(add5(3)); // Output: 8 (5 + 3)
console.log(add5(7)); // Output: 12 (5 + 7)
console.log(add10(3)); // Output: 13 (10 + 3)add5 permanently remembers x = 5. add10 permanently remembers x = 10. This is partial application through closures.
Closures are the mechanism that makes the IIFE module pattern work. They also allow you to create private variables without using an IIFE:
function createPerson(name) {
// 'name' is private — accessible only through the returned methods
let _name = name;
let _callCount = 0;
return {
getName: function() {
_callCount++;
return _name;
},
setName: function(newName) {
if (typeof newName === "string" && newName.trim() !== "") {
_name = newName;
}
},
getCallCount: function() {
return _callCount;
}
};
}
const person = createPerson("Alice");
console.log(person.getName()); // Output: Alice
console.log(person.getName()); // Output: Alice
console.log(person.getCallCount()); // Output: 2
person.setName("Bob");
console.log(person.getName()); // Output: Bob
// console.log(person._name); // Output: undefined — truly private_name and _callCount can never be accessed or modified directly from outside. They are private to the closure.
This is one of the most famous JavaScript gotchas:
// ❌ Bug: All buttons alert "3"
for (var i = 0; i < 3; i++) {
const btn = document.createElement("button");
btn.innerText = "Button " + i;
btn.addEventListener("click", function() {
alert("I am button " + i); // By the time this runs, i is 3 (the loop ended)
});
document.body.appendChild(btn);
}
// Clicking any button → Alert: "I am button 3"Why: var is function-scoped, so all three callbacks share the same i variable. By the time any button is clicked, the loop has finished and i is 3.
Fix 1 · Use let (block-scoped, creates a new i for each iteration):
for (let i = 0; i < 3; i++) { // 'let' creates a new i per iteration
const btn = document.createElement("button");
btn.innerText = "Button " + i;
btn.addEventListener("click", function() {
alert("I am button " + i); // Each closure now has its own 'i'
});
document.body.appendChild(btn);
}
// Clicking Button 0 → Alert: "I am button 0"
// Clicking Button 1 → Alert: "I am button 1"
// Clicking Button 2 → Alert: "I am button 2"Fix 2 · Use an IIFE to create a new scope per iteration (pre-ES6 approach):
for (var i = 0; i < 3; i++) {
(function(j) { // j is a new variable per iteration, capturing current i
const btn = document.createElement("button");
btn.innerText = "Button " + j;
btn.addEventListener("click", function() {
alert("I am button " + j);
});
document.body.appendChild(btn);
})(i);
}⚠️ This bug is extremely common in interviews. Make sure you understand both why it happens and how to fix it.
React hooks are closures:
// useState in React returns a pair where the setter closes over the state
const [count, setCount] = useState(0);
// Every time setCount is called, it updates the closed-over 'count'Event handlers with context:
function setupHandler(userId) {
document.getElementById("profile-btn").addEventListener("click", function() {
fetchUserProfile(userId); // userId is captured — no need to pass it explicitly
});
}
setupHandler(42); // The handler will always fetch user 42setTimeout inside loops:
// Send a message after 1, 2, and 3 seconds
[1, 2, 3].forEach(function(seconds) {
setTimeout(function() {
console.log("Message after " + seconds + " seconds");
// 'seconds' is captured correctly because forEach callback creates a new scope
}, seconds * 1000);
});# CHAPTER 10 · FUNCTION REFERENCE (BUILT-IN METHODS)
JavaScript provides several built-in methods on every function object. Understanding these turns you from a passive function user into someone who can control exactly how functions are called.
Returns the source code of the function as a string:
function add(a, b) {
return a + b;
}
console.log(add.toString());
// Output:
// function add(a, b) {
// return a + b;
// }Used for debugging, code analysis tools, and documentation generators.
Function.prototype.toString(). Open your editor, type the examples above by hand, modify them, and observe what changes.The length property returns the number of expected parameters (not counting rest parameters or parameters with defaults):
function f1(a, b, c) {}
function f2(a, b = 0, c) {}
function f3(a, ...rest) {}
console.log(f1.length); // Output: 3
console.log(f2.length); // Output: 1 (only 'a' counts — 'b' has a default)
console.log(f3.length); // Output: 1 (only 'a' counts — rest doesn't count)This is used by libraries to check how many arguments a function expects and dispatch accordingly.
Function.prototype.length. Open your editor, type the examples above by hand, modify them, and observe what changes.Returns the name of the function:
function greet() {}
const sayHi = function hello() {};
const arrow = () => {};
const obj = { method() {} };
console.log(greet.name); // Output: greet
console.log(sayHi.name); // Output: hello
console.log(arrow.name); // Output: arrow
console.log(obj.method.name); // Output: methodUseful in debugging · error stack traces use the function name to show where a problem occurred. Anonymous functions appear as "" or "anonymous" in stack traces, making debugging harder.
Function.prototype.name. Open your editor, type the examples above by hand, modify them, and observe what changes.Already covered in Chapter 5. Quick reference:
fn.call(thisArg, arg1, arg2, ...);
// Calls fn immediately, setting 'this' to thisArgFunction.prototype.call() · Recap. Open your editor, type the examples above by hand, modify them, and observe what changes.Already covered in Chapter 6. Quick reference:
fn.apply(thisArg, [arg1, arg2, ...]);
// Calls fn immediately, passing args as an arrayFunction.prototype.apply() · Recap. Open your editor, type the examples above by hand, modify them, and observe what changes.Already covered in Chapter 7. Quick reference:
const boundFn = fn.bind(thisArg, arg1, arg2, ...);
// Returns a new function with 'this' and optional args pre-setFunction.prototype.bind() · Recap. Open your editor, type the examples above by hand, modify them, and observe what changes.| Method / Property | Type | Purpose |
|---|---|---|
.call(thisArg, ...args) | Method | Call immediately with explicit this, individual args |
.apply(thisArg, [args]) | Method | Call immediately with explicit this, array of args |
.bind(thisArg, ...args) | Method | Return new function with this and args pre-set |
.toString() | Method | Return function source code as a string |
.length | Property | Number of declared parameters (excluding defaults and rest) |
.name | Property | Name of the function |
# PHASE 2 · APPLIED EXERCISES
Objective: Practice writing the same logic using four different function definition styles.
Scenario: A temperature conversion utility for a weather app.
Warm-up mini-example:
// Celsius to Fahrenheit: F = (C × 9/5) + 32
const toF = c => (c * 9/5) + 32;
console.log(toF(0)); // Output: 32
console.log(toF(100)); // Output: 212Step-by-step instructions:
Write a celsiusToFahrenheit function using:
- A function declaration
- A function expression (anonymous)
- A named function expression
- An arrow function (concise single-line)
Then write a convertTemperatures(temps, converterFn) function that accepts an array of temperatures and a converter function, returning a new array of converted values. Call it with all four converter styles.
Expected output:
convertTemperatures([0, 20, 37, 100], celsiusToFahrenheit);
// Output: [32, 68, 98.6, 212]Self-check questions:
- Which of the four styles is hoisted? Test it by calling the function before its definition.
- In
convertTemperatures, is the converter parameter a callback? How do you know?
Objective: Build a mini utility library of higher-order functions.
Scenario: You're building a data processing utility for a school's grade tracking system.
Step-by-step instructions:
Start with this data:
const students = [
{ name: "Alice", grade: 92 },
{ name: "Bob", grade: 55 },
{ name: "Carol", grade: 78 },
{ name: "David", grade: 61 },
{ name: "Eve", grade: 88 },
];Write the following functions using .map(), .filter(), and .reduce() (all higher-order functions):
getNames(students)→ Returns["Alice", "Bob", "Carol", "David", "Eve"]getPassingStudents(students)→ Returns students with grade ≥ 65getAverageGrade(students)→ Returns the average grade (use.reduce())getLetterGrade(score)→ Returns "A" (≥90), "B" (≥80), "C" (≥70), "D" (≥60), "F" (<60)gradeReport(students)→ Returns a string array:["Alice: A", "Bob: F", ...]
Warm-up mini-example for reduce:
const total = [10, 20, 30].reduce((accumulator, current) => accumulator + current, 0);
console.log(total); // Output: 60Self-check questions:
- Is
.map()a higher-order function? Why? - In
gradeReport, you'll composegetLetterGradeinside a.map()· what does that make the outer function?
Objective: Simulate an asynchronous login flow using callbacks.
Scenario: A login system with three steps: validate credentials, fetch user profile, load user settings. Each step takes a simulated delay using setTimeout.
Step-by-step instructions:
function validateCredentials(username, password, callback) {
setTimeout(function() {
const isValid = username === "admin" && password === "1234";
if (isValid) {
callback(null, { userId: 42, username });
} else {
callback(new Error("Invalid credentials"));
}
}, 500);
}Write similar functions for fetchUserProfile(userId, callback) and loadUserSettings(userId, callback). Chain them together using error-first callbacks. If any step fails, stop and log the error.
Hints:
- Check
if (err) { ... return; }at the start of each callback - The chain should look like: validate → if OK → fetch profile → if OK → load settings → display welcome message
Self-check questions:
- What is the maximum nesting depth if you chain 5 callbacks?
- How would you refactor this using named functions to flatten the nesting?
Objective: Predict the value of this in different contexts before running the code.
Step-by-step instructions:
For each snippet below, write your prediction of what will be logged before you run it:
this. Open your editor, type the examples above by hand, modify them, and observe what changes.// Snippet A
const obj = {
value: 42,
getValue: function() {
return this.value;
}
};
const fn = obj.getValue;
console.log(fn()); // Prediction: ___?
console.log(obj.getValue()); // Prediction: ___?// Snippet B
function Timer() {
this.ticks = 0;
setTimeout(function() {
this.ticks++;
console.log(this.ticks); // Prediction: ___?
}, 100);
}
new Timer();// Snippet C
function Timer() {
this.ticks = 0;
setTimeout(() => {
this.ticks++;
console.log(this.ticks); // Prediction: ___?
}, 100);
}
new Timer();After predicting, run each snippet and compare. If wrong, write an explanation of why.
Objective: Solve three real-world problems using the correct method.
Problem 1 (use .call()): You have a fullName function and two objects. Call it for each without modifying the objects.
function fullName(separator) {
return this.first + separator + this.last;
}
const person1 = { first: "Ada", last: "Lovelace" };
const person2 = { first: "Grace", last: "Hopper" };
// Expected:
// Ada Lovelace
// Grace-HopperProblem 2 (use .apply()): Find the maximum and minimum values from this array using Math.max and Math.min with .apply():
const temperatures = [23, 17, 31, 8, 27, 15, 29];
// Expected: Max: 31, Min: 8Problem 3 (use .bind()): Fix the broken countdown so it logs correctly:
function Counter(start) {
this.count = start;
this.tick = function() {
if (this.count > 0) {
console.log(this.count--);
setTimeout(this.tick, 500); // ❌ 'this' will be lost here
}
};
this.tick();
}
new Counter(5);Objective: Use an IIFE to create a private configuration module.
Scenario: You're building a web app that needs configuration settings (API base URL, default page size, app version) that should be read-only · nothing outside should be able to change them after initialisation.
Step-by-step instructions:
const config = (function() {
const _settings = {
apiBase: "https://api.myapp.com/v1",
pageSize: 20,
version: "2.3.1",
debug: false
};
return {
get: function(key) { /* return the value for key */ },
getAll: function() { /* return a copy, not the original */ },
isDebug: function() { /* return the debug flag */ }
};
})();Fill in the three method bodies. getAll must return a copy of _settings so external code can't modify the original. Hint: use Object.assign({}, _settings) or the spread operator {..._settings}.
Expected:
console.log(config.get("version")); // Output: 2.3.1
console.log(config.isDebug()); // Output: false
const copy = config.getAll();
copy.version = "hack"; // Modifying the copy
console.log(config.get("version")); // Output: 2.3.1 ← original unchangedObjective: Build a reusable rate limiter using closures.
Scenario: A button on a web page that should only be clickable once every 3 seconds (to prevent spam clicks). Use a closure to remember the last time the button was clicked.
Step-by-step instructions:
function createRateLimiter(fn, cooldownMs) {
let lastCalled = 0;
return function() {
const now = Date.now();
if (now - lastCalled < cooldownMs) {
console.log("Too soon! Please wait.");
return;
}
lastCalled = now;
fn();
};
}
function saveData() {
console.log("Data saved at " + new Date().toLocaleTimeString());
}
const limitedSave = createRateLimiter(saveData, 3000);
// Simulating rapid clicks:
limitedSave(); // Output: Data saved at ...
limitedSave(); // Output: Too soon! Please wait.
limitedSave(); // Output: Too soon! Please wait.
// (3 seconds later)
limitedSave(); // Output: Data saved at ...Self-check questions:
- Which variable in
createRateLimiteris closed over by the returned function? - What would happen if
lastCalledwere declared withvarinside the returned function instead of in the outer function? - How does this differ from just using a global
lastCalledvariable?
# PHASE 3 · PROJECT SIMULATION
Scenario: You are a junior developer at a project management startup. Your task is to build a lightweight JavaScript pipeline that processes tasks through several stages · filtering, transforming, prioritising · using functional programming concepts. All internal state must be private (no global variables), and the system must be extensible.
This project deliberately combines all ten chapters:
- Function definitions (Chapter 1)
- Higher-order functions and composition (Chapter 2)
- Callbacks (Chapter 3)
- Closures for private state (Chapter 9)
- IIFE for module encapsulation (Chapter 8)
.call()/.apply()/.bind()where appropriate (Chapters 5 · 7)
Stage 1 · Data and Core Utilities
Setup: The task data and basic utility functions
// Raw task data — simulating a real project's task list
const rawTasks = [
{ id: 1, title: "Write unit tests", category: "dev", hours: 3, done: false },
{ id: 2, title: "Design login screen", category: "design", hours: 5, done: false },
{ id: 3, title: "Fix payment bug", category: "dev", hours: 1, done: true },
{ id: 4, title: "Update documentation", category: "writing", hours: 2, done: false },
{ id: 5, title: "Code review", category: "dev", hours: 2, done: false },
{ id: 6, title: "Client presentation", category: "meeting", hours: 4, done: true },
{ id: 7, title: "Database optimisation", category: "dev", hours: 6, done: false },
{ id: 8, title: "Write blog post", category: "writing", hours: 3, done: false },
];Core utility functions (pure functions only):
// Filter tasks by a predicate function
function filterTasks(tasks, predicate) {
return tasks.filter(predicate);
}
// Transform tasks (map)
function transformTasks(tasks, transformer) {
return tasks.map(transformer);
}
// Reduce tasks to a single value
function reduceTasks(tasks, reducer, initialValue) {
return tasks.reduce(reducer, initialValue);
}
// Sort tasks by a key
function sortTasks(tasks, keyFn, ascending = true) {
return [...tasks].sort((a, b) => {
const valA = keyFn(a);
const valB = keyFn(b);
return ascending ? (valA > valB ? 1 : -1) : (valA < valB ? 1 : -1);
});
}Expected output · test Stage 1:
const pendingTasks = filterTasks(rawTasks, task => !task.done);
console.log(pendingTasks.length); // Output: 6
const totalHours = reduceTasks(rawTasks, (sum, t) => sum + t.hours, 0);
console.log(totalHours); // Output: 26Stage 2 · The Pipeline Builder (Higher-Order Functions + Closures)
Build a reusable pipeline function that chains operations together:
function createPipeline(...steps) {
// Each 'step' is a function that takes tasks and returns modified tasks
return function(tasks) {
return steps.reduce(function(currentTasks, step) {
return step(currentTasks);
}, tasks);
};
}Create specialised step functions using partial application via .bind():
// Step factories — each returns a function for use in the pipeline
const onlyPending = tasks => filterTasks(tasks, t => !t.done);
const onlyDev = tasks => filterTasks(tasks, t => t.category === "dev");
const byHoursAsc = tasks => sortTasks(tasks, t => t.hours, true);
const byHoursDesc = tasks => sortTasks(tasks, t => t.hours, false);
// Generalised filter step factory using closure:
function byCategoryStep(category) {
return function(tasks) {
return filterTasks(tasks, t => t.category === category);
};
}
const addPriorityLabel = tasks => transformTasks(tasks, function(task) {
const priority = task.hours >= 5 ? "HIGH"
: task.hours >= 3 ? "MEDIUM"
: "LOW";
return { ...task, priority };
});Compose pipelines:
const devBacklogPipeline = createPipeline(
onlyPending,
onlyDev,
addPriorityLabel,
byHoursDesc
);
const results = devBacklogPipeline(rawTasks);
results.forEach(t => {
console.log(`[${t.priority}] ${t.title} (${t.hours}h)`);
});
// Expected output:
// [HIGH] Database optimisation (6h)
// [MEDIUM] Write unit tests (3h)
// [LOW] Fix payment bug (1h) ← wait, this is 'done:true' — it should be filtered out
// Actually: only pending dev tasks:
// [HIGH] Database optimisation (6h)
// [MEDIUM] Write unit tests (3h)
// [LOW] Code review (2h)Stage 3 · The Private Task Manager Module (IIFE + Closures)
Encapsulate everything in a module with private state and a public API:
const TaskManager = (function() {
// Private state
let _tasks = [];
let _history = [];
let _nextId = 1;
// Private helpers
function _log(action, taskId) {
_history.push({
action,
taskId,
timestamp: new Date().toISOString()
});
}
// Public API
return {
loadTasks: function(tasks) {
_tasks = tasks.map(t => ({ ...t })); // Store a copy, not the original
_nextId = Math.max(..._tasks.map(t => t.id)) + 1;
_log("LOAD", null);
},
addTask: function(title, category, hours) {
const task = { id: _nextId++, title, category, hours, done: false };
_tasks.push(task);
_log("ADD", task.id);
return task;
},
completeTask: function(id) {
const task = _tasks.find(t => t.id === id);
if (task) {
task.done = true;
_log("COMPLETE", id);
return true;
}
return false;
},
runPipeline: function(pipeline) {
return pipeline([..._tasks]); // Pass a copy to the pipeline
},
getStats: function() {
const total = _tasks.length;
const done = _tasks.filter(t => t.done).length;
const pending = total - done;
const totalHrs = reduceTasks(_tasks, (s, t) => s + t.hours, 0);
const doneHrs = reduceTasks(_tasks.filter(t => t.done), (s, t) => s + t.hours, 0);
return { total, done, pending, totalHrs, doneHrs };
},
getHistory: function() {
return [..._history]; // Return a copy
}
};
})();Using the module:
TaskManager.loadTasks(rawTasks);
// Add a new task
TaskManager.addTask("Performance audit", "dev", 4);
// Complete a task
TaskManager.completeTask(4);
// Run a pipeline through the manager
const devPipeline = createPipeline(onlyPending, onlyDev, addPriorityLabel, byHoursDesc);
const devResults = TaskManager.runPipeline(devPipeline);
console.log("Dev backlog:");
devResults.forEach(t => console.log(` [${t.priority}] ${t.title}`));
// Stats
const stats = TaskManager.getStats();
console.log("\nProject Stats:");
console.log(" Total tasks: " + stats.total);
console.log(" Done: " + stats.done);
console.log(" Pending: " + stats.pending);
console.log(" Total hours: " + stats.totalHrs);
// History
console.log("\nAudit Trail:");
TaskManager.getHistory().forEach(entry => {
console.log(" " + entry.action + (entry.taskId ? " #" + entry.taskId : "") + " at " + entry.timestamp);
});Expected output:
Dev backlog:
[HIGH] Database optimisation
[HIGH] Performance audit
[MEDIUM] Write unit tests
[LOW] Code review
Project Stats:
Total tasks: 9
Done: 3
Pending: 6
Total hours: 30
Audit Trail:
LOAD at 2024-...
ADD #9 at 2024-...
COMPLETE #4 at 2024-...Stage 4 · Advanced Challenge: Debounce Utility (Closures + Timers)
The debounce function is used everywhere in real applications · search boxes, resize handlers, form auto-saves. It ensures a function doesn't fire more than once within a set time window, no matter how many times it's triggered.
function debounce(fn, delayMs) {
let timeoutId = null; // Closed-over variable — persists between calls
return function(...args) {
// If the timer is already running, cancel it and restart
clearTimeout(timeoutId);
// Start a new timer
timeoutId = setTimeout(() => {
fn.apply(this, args); // Use .apply() to preserve 'this' and pass args
timeoutId = null;
}, delayMs);
};
}Usage · search box that only queries after typing stops:
function searchTasks(query) {
const results = TaskManager.runPipeline(
createPipeline(tasks => tasks.filter(t => t.title.toLowerCase().includes(query)))
);
console.log("Search '" + query + "':", results.map(t => t.title));
}
const debouncedSearch = debounce(searchTasks, 300);
// Simulate fast keystrokes:
debouncedSearch("w");
debouncedSearch("wr");
debouncedSearch("wri");
debouncedSearch("writ");
// Only "writ" search will actually run (the others were cancelled)
// Output: Search 'writ': ["Write unit tests", "Write blog post"]Why this uses closures: timeoutId is captured by the returned function. Every call to debouncedSearch reads and updates the same timeoutId. Without a closure, this would require a global variable.
Reflection Questions:
- In
TaskManager, why doesrunPipelinepass[..._tasks](a copy) to the pipeline instead of_tasksdirectly? - In
debounce, the function usesfn.apply(this, args). What would happen if you just usedfn(...args)instead? When would they differ? - The
_historyarray inTaskManagercan grow indefinitely. In a real application, how would you handle this? (Think: max length, persisting tolocalStorage, sending to a server.) - How would you modify
createPipelineto support async pipeline steps (steps that return a Promise)? - The
devBacklogPipelineis created outsideTaskManager. What would be the advantages and disadvantages of moving pipeline creation inside the module?
# QUIZ & COMPLETION CHECKLIST
Test your understanding. Write your answers before checking.
Q1: What is the difference between a function declaration and a function expression in terms of hoisting?
Q2: Write a concise arrow function that takes a number n and returns its cube (n³).
Q3: What does a rest parameter (...args) return · an array or an object?
Q4: In the following code, what is logged and why?
const obj = { n: 5 };
function double() { return this.n * 2; }
console.log(double.call(obj)); // ?Q5: What is the key difference between .call() and .apply()?
Q6: What does .bind() return · a value or a function?
Q7: What does event.preventDefault() do and why is it needed in form validation?
Q8: What is an IIFE and give one reason to use it?
Q9: Given the following code, what are the two outputs and why are they different?
function makeCounter() {
let count = 0;
return () => ++count;
}
const a = makeCounter();
const b = makeCounter();
a(); a();
console.log(a()); // ?
console.log(b()); // ?Q10: In the loop closure bug, why does using let instead of var fix the problem?
A1: Function declarations are hoisted · you can call them before they appear. Function expressions are not · accessing them before assignment throws a ReferenceError (for let/const) or returns undefined then throws (for var).
A2: const cube = n => n * 3; (or n n * n)
A3: A real array, with all standard array methods available.
A4: 10. .call(obj) sets this to obj, so this.n is 5. 5 * 2 = 10.
A5: .call() takes arguments individually: fn.call(ctx, a, b, c). .apply() takes them as an array: fn.apply(ctx, [a, b, c]).
A6: A new function · with this and any pre-filled arguments permanently set.
A7: It stops the browser's default form submission behaviour (page reload), allowing JavaScript to handle validation and submission instead.
A8: A function defined and immediately executed: (function() { ... })(). Use cases: creating a private scope to avoid polluting global variables, or running initialisation code that shouldn't be repeatable.
A9: a() logs 3 (called three times: 1, 2, 3). b() logs 1 (independent closure, count starts fresh at 0). Each call to makeCounter() creates a separate count variable.
A10: var is function-scoped · all loop iterations share one var i variable. By the time callbacks run, the loop has ended and i is the final value. let is block-scoped · each iteration creates a brand-new i binding. Each callback closes over its own separate i.
| Mistake | Why It Happens | Fix |
|---|---|---|
| Calling a function expression before assignment | Not hoisted | Move the expression up, or use a declaration |
addEventListener("click", fn()) | Calls fn immediately; passes its return value | Use fn without parentheses |
this is wrong inside a callback | Plain function call loses object context | Use .bind() or an arrow function |
const for a counter variable | const can't be reassigned | Use let |
Loop closures all share one var | var is function-scoped, not block-scoped | Use let |
| Can't remove an anonymous listener | No reference to pass to removeEventListener | Use a named function |
Default values trigger for null | They only trigger for undefined, not null | Check explicitly if needed |
| Arrow functions in object methods | No own this · inherits from outer scope | Use regular function for methods |
JavaScript functions are first-class citizens that can be defined in multiple ways, passed as callbacks, composed into pipelines, scoped with IIFEs, bound to specific contexts with
call/apply/bind, and made to "remember" private variables through closures · making them the single most powerful tool in the entire language.
Tutorial generated by AI_TUTORIAL_GENERATOR · Source curriculum: W3Schools JavaScript Functions (11 pages)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>My Project</title> </head> <body> <!-- Your code h
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>My Project</title>
</head>
<body>
<!-- Your code here -->
</body>
</html>