Advanced Working with Functions
Recursion and Stack
Recursion is when a function calls itself.
function countDown(n) {
if (n <= 0) {
console.log("Done!");
return;
}
console.log(n);
countDown(n - 1);
}
countDown(3);
// β 3, 2, 1, "Done!"
Recursive Factorial
function factorial(n) {
if (n <= 1) return 1; // Base case
return n * factorial(n - 1); // Recursive call
}
console.log(factorial(5)); // β 120 (5 Γ 4 Γ 3 Γ 2 Γ 1)
Recursive vs Iterative
// Iterative
function powIter(x, n) {
let result = 1;
for (let i = 0; i < n; i++) result *= x;
return result;
}
// Recursive
function powRecur(x, n) {
if (n === 0) return 1;
return x * powRecur(x, n - 1);
}
console.log(powIter(2, 3)); // β 8
console.log(powRecur(2, 3)); // β 8
Execution Stack
Each recursive call is pushed onto the call stack:
pow(2, 3)
β 2 * pow(2, 2)
β 2 * 2 * pow(2, 1)
β 2 * 2 * 2 * pow(2, 0)
β 2 * 2 * 2 * 1
β 2 * 2 * 2
β 2 * 4
β 8
Deep Recursion
// Recursive tree traversal
let company = {
name: "Big Corp",
departments: [
{ name: "Engineering", employees: 50 },
{
name: "Sales",
employees: 30,
sub: [
{ name: "Domestic", employees: 20 },
{ name: "International", employees: 10 }
]
}
]
};
function countEmployees(dept) {
let total = dept.employees || 0;
if (dept.sub) {
for (let sub of dept.sub) {
total += countEmployees(sub);
}
}
return total;
}
console.log(countEmployees(company)); // β 110
Rest Parameters and Spread Syntax
Rest Parameters ...
Collect remaining arguments into an array:
function sum(...nums) {
let total = 0;
for (let n of nums) total += n;
return total;
}
console.log(sum(1, 2, 3, 4, 5)); // β 15
Rest parameters must be the last parameter:
function show(name, age, ...hobbies) {
console.log(name); // β "Alice"
console.log(age); // β 30
console.log(hobbies); // β ["reading", "coding", "gaming"]
}
show("Alice", 30, "reading", "coding", "gaming");
Spread Syntax ...
Spread expands an iterable into individual elements:
// Arrays
let arr1 = [1, 2, 3];
let arr2 = [4, 5, 6];
let combined = [...arr1, ...arr2]; // β [1, 2, 3, 4, 5, 6]
// Copy array
let copy = [...arr1];
// String to characters
let chars = [..."hello"]; // β ["h", "e", "l", "l", "o"]
// Function arguments
let numbers = [3, 8, 1, 6, 2];
console.log(Math.max(...numbers)); // β 8
// Objects (ES2018)
let obj1 = { a: 1, b: 2 };
let obj2 = { c: 3, ...obj1 }; // β { c: 3, a: 1, b: 2 }
// Merge objects (later properties overwrite earlier)
let merged = { ...obj1, ...obj2 }; // β { a: 1, b: 2, c: 3 }
Variable Scope, Closure
Lexical Environment
Every function, block, and script has an associated Lexical Environment that stores local variables.
let x = 10; // Global
function outer() {
let y = 20; // Outer
function inner() {
let z = 30; // Inner
console.log(x + y + z); // Can access all three!
}
inner();
}
outer(); // β 60
Closure
A closure is a function that βremembersβ its lexical scope even when called outside of it.
function createCounter() {
let count = 0; // Private variable
return function() {
count++;
return count;
};
}
let counter = createCounter();
console.log(counter()); // β 1
console.log(counter()); // β 2
console.log(counter()); // β 3
// count is not accessible from outside
console.log(counter.count); // β undefined
Practical Closures
// Closure for private data
function createBankAccount(initialBalance) {
let balance = initialBalance;
return {
deposit(amount) {
balance += amount;
return balance;
},
withdraw(amount) {
if (amount > balance) return "Insufficient funds";
balance -= amount;
return balance;
},
getBalance() {
return balance;
}
};
}
let account = createBankAccount(1000);
console.log(account.deposit(500)); // β 1500
console.log(account.withdraw(200)); // β 1300
console.log(account.getBalance()); // β 1300
// console.log(account.balance); // β undefined β private!
Closure in Loops (the classic problem)
// β Classic problem
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 1000);
}
// β 3, 3, 3 (all reference the same `var i`)
// β
Fix with let (block scope)
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 1000);
}
// β 0, 1, 2
// β
Fix with closure (pre-ES6)
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(() => console.log(j), 1000);
})(i);
}
// β 0, 1, 2
The Old βvarβ
Before ES6, var was the only way to declare variables. Itβs still used in older code.
var has no block scope
if (true) {
var x = 10;
let y = 20;
}
console.log(x); // β 10 (var ignores blocks)
// console.log(y); // ReferenceError!
var hoisting
console.log(z); // β undefined (not an error!)
var z = 5;
// Same as:
// var z;
// console.log(z);
// z = 5;
var in loops
for (var i = 0; i < 5; i++) {
// i is visible outside the loop!
}
console.log(i); // β 5
IIFE pattern with var
Before modules, IIFEs were used to create private scopes:
(function() {
var privateVar = "secret";
// Can't access from outside
})();
Modern rule: Use
letandconst. Never usevarin new code.
Global Object
In browsers, the global object is window. In Node.js, itβs global. The modern standard is globalThis.
// Browser
console.log(window); // Global object
console.log(globalThis); // Works everywhere
// Variables declared with var become global properties
var x = 10;
console.log(window.x); // β 10 (var only)
// let/const do NOT become global properties
let y = 20;
console.log(window.y); // β undefined
// Global functions
window.alert("Hello!");
console.log(globalThis.parseInt === parseInt); // β true
Function Object, NFE
Functions are objects β they have properties and methods:
function greet(name) {
return "Hello, " + name;
}
// Functions have properties
console.log(greet.name); // β "greet"
console.log(greet.length); // β 1 (number of parameters)
// You can add custom properties
greet.counter = 0;
greet.counter++;
console.log(greet.counter); // β 1
Named Function Expression (NFE)
// Anonymous function expression
let sayHi = function(name) {
if (name) return "Hi, " + name;
};
// Named function expression β has a name for internal reference
let sayHi = function greeting(name) {
if (name) return "Hi, " + name;
// return greeting("Guest"); // Can reference itself
};
console.log(sayHi.name); // β "greeting"
The βnew Functionβ Syntax
Create a function from a string at runtime:
let sum = new Function("a", "b", "return a + b");
console.log(sum(2, 3)); // β 5
// No parameters
let sayHi = new Function('console.log("Hello")');
sayHi(); // β "Hello"
Security note:
new Functionis rarely used and can be dangerous if the string comes from user input (code injection risk).
Scheduling: setTimeout and setInterval
setTimeout
Run a function once after a delay:
function greet() {
console.log("Hello!");
}
// Run after 2 seconds
setTimeout(greet, 2000);
// With arguments
setTimeout((name) => console.log("Hello, " + name), 1000, "Alice");
// Cancel a timeout
let timeoutId = setTimeout(() => console.log("Never runs"), 3000);
clearTimeout(timeoutId);
setInterval
Run a function repeatedly at intervals:
let count = 0;
let intervalId = setInterval(() => {
count++;
console.log("Tick " + count);
if (count >= 5) clearInterval(intervalId);
}, 1000);
// β "Tick 1", "Tick 2", ... "Tick 5"
Recursive setTimeout vs setInterval
// setInterval β fixed interval, doesn't wait for execution
let id = setInterval(() => {
console.log("Running...");
}, 1000);
// Recursive setTimeout β waits for execution to finish
function recursiveTimeout() {
console.log("Running...");
setTimeout(recursiveTimeout, 1000);
}
// Recursive setTimeout guarantees the delay between completions
Zero Delay setTimeout
setTimeout(() => console.log("World"), 0);
console.log("Hello");
// β "Hello" then "World"
// Even with 0ms delay, it runs after current code finishes
// (scheduled on the macrotask queue)
<div id='timer'></div> let count = 0;
let el = document.getElementById('timer');
let intervalId = setInterval(() => {
count++;
el.textContent = 'Count: ' + count;
if (count >= 10) {
clearInterval(intervalId);
el.textContent = 'Done!';
}
}, 500); Decorators and Forwarding, call/apply
Decorator β a function that wraps another function
function slow(x) {
// Heavy computation
let result = 0;
for (let i = 0; i < x * 1e7; i++) result += i;
return result;
}
// Caching decorator
function caching(fn) {
let cache = new Map();
return function(x) {
if (cache.has(x)) {
console.log("From cache: " + x);
return cache.get(x);
}
let result = fn(x);
cache.set(x, result);
return result;
};
}
let cachedSlow = caching(slow);
console.log(cachedSlow(5)); // Computed
console.log(cachedSlow(5)); // From cache
Function.call
Call a function with a specific this:
function greet() {
console.log("Hello, " + this.name);
}
let user1 = { name: "Alice" };
let user2 = { name: "Bob" };
greet.call(user1); // β "Hello, Alice"
greet.call(user2); // β "Hello, Bob"
// With arguments
function introduce(greeting, punctuation) {
console.log(greeting + ", " + this.name + punctuation);
}
introduce.call(user1, "Hi", "!"); // β "Hi, Alice!"
Function.apply
Same as call but arguments are passed as an array:
introduce.apply(user1, ["Hello", "."]); // β "Hello, Alice."
// Useful for spreading arrays
let numbers = [3, 5, 1, 8, 2];
console.log(Math.max.apply(null, numbers)); // β 8 (Math.max doesn't use this)
Borrowing methods
let obj = {
0: "hello",
1: "world",
length: 2
};
// Borrow Array.prototype.join
let result = Array.prototype.join.call(obj, ", ");
console.log(result); // β "hello, world"
Function Binding
When passing an object method as a callback, this is lost:
let user = {
name: "Alice",
greet() {
console.log("Hello, " + this.name);
}
};
// Direct call works
user.greet(); // β "Hello, Alice"
// Passed as callback β this is lost
setTimeout(user.greet, 1000); // β "Hello, undefined"
Solution 1: Wrapper
setTimeout(() => user.greet(), 1000); // β "Hello, Alice"
Solution 2: Function.bind
let boundGreet = user.greet.bind(user);
boundGreet(); // β "Hello, Alice"
setTimeout(boundGreet, 1000); // β "Hello, Alice"
// bind with arguments (partial application)
function multiply(a, b) {
return a * b;
}
let double = multiply.bind(null, 2); // Pre-fill first argument
console.log(double(5)); // β 10
Partial application
function log(level, message) {
console.log(`[${level}] ${message}`);
}
let info = log.bind(null, "INFO");
let error = log.bind(null, "ERROR");
info("System started"); // β "[INFO] System started"
error("Something broke"); // β "[ERROR] Something broke"
Arrow Functions Revisited
Arrow functions are not just shorter syntax β they have fundamental differences:
No βthisβ
let user = {
name: "Alice",
greet() {
// Regular function β has own this
setTimeout(function() {
console.log("Hello, " + this.name); // this is window/undefined
}, 100);
},
greetArrow() {
// Arrow β inherits this from surrounding scope
setTimeout(() => {
console.log("Hello, " + this.name); // this is user
}, 100);
}
};
user.greet(); // β "Hello, undefined"
user.greetArrow(); // β "Hello, Alice"
No βargumentsβ
function regular() {
console.log(arguments); // β [1, 2, 3]
}
let arrow = () => {
console.log(arguments); // ReferenceError (or inherits from parent)
};
regular(1, 2, 3);
Canβt be used with βnewβ
let Foo = () => {};
// new Foo(); // TypeError: Foo is not a constructor
When to use arrows vs regular
| Scenario | Use |
|---|---|
| Short callbacks, array methods | Arrow |
Object methods that need this | Regular |
| Constructor functions | Regular |
Need arguments object | Regular |
Event handlers (need this = element) | Regular |
let numbers = [1, 2, 3, 4, 5, 6];
// Arrow functions shine in callbacks
let evens = numbers.filter(n => n % 2 === 0);
let squares = numbers.map(n => n * n);
let sum = numbers.reduce((acc, n) => acc + n, 0);
document.write('Evens: ' + evens.join(', ') + '<br>');
document.write('Squares: ' + squares.join(', ') + '<br>');
document.write('Sum: ' + sum + '<br>');
// Arrow with this inheritance (demonstration)
let timer = {
seconds: 0,
start() {
setInterval(() => {
this.seconds++;
}, 1000);
},
getTime() {
return this.seconds;
}
};
timer.start();