4.3: Functions & Scope
Understand JavaScript functions and learn how to create reusable blocks of code. Master function declarations, expressions, arrow functions, parameters, return values, and the concept of scope.
1. Function Basics
Function Declaration
JavaScript:
function greet(name) {
return `Hello, ${name}!`;
}
console.log(greet("Alice")); // Hello, Alice!
Python comparison:
def greet(name):
return f"Hello, {name}!"
print(greet("Alice")) # Hello, Alice!
Function Expression
Assigning a function to a variable:
const greet = function(name) {
return `Hello, ${name}!`;
};
console.log(greet("Bob")); // Hello, Bob!
Key difference from declaration:
// Declaration: Hoisted (can call before definition)
sayHi(); // Works!
function sayHi() {
console.log("Hi!");
}
// Expression: NOT hoisted
// sayHello(); // Error: Cannot access before initialization
const sayHello = function() {
console.log("Hello!");
};
Arrow Functions (ES6+)
Modern, concise syntax:
// Traditional function
const add = function(a, b) {
return a + b;
};
// Arrow function
const add = (a, b) => {
return a + b;
};
// Arrow function (implicit return)
const add = (a, b) => a + b;
// Single parameter (no parentheses needed)
const square = x => x * x;
// No parameters
const greet = () => "Hello!";
Python comparison:
# Python lambda (limited to single expression)
add = lambda a, b: a + b
square = lambda x: x * x
# Full function
def add(a, b):
return a + b
Arrow function rules:
- Parentheses optional for single parameter
- Curly braces optional for single expression
returnimplicit for single expression- No own
thisbinding (more on this later)
2. Parameters and Arguments
Default Parameters
JavaScript:
function greet(name = "Guest", greeting = "Hello") {
return `${greeting}, ${name}!`;
}
console.log(greet()); // Hello, Guest!
console.log(greet("Alice")); // Hello, Alice!
console.log(greet("Bob", "Hi")); // Hi, Bob!
Python:
def greet(name="Guest", greeting="Hello"):
return f"{greeting}, {name}!"
Rest Parameters
Collect remaining arguments into an array:
function sum(...numbers) {
return numbers.reduce((total, num) => total + num, 0);
}
console.log(sum(1, 2, 3)); // 6
console.log(sum(1, 2, 3, 4, 5)); // 15
Python comparison:
def sum_all(*numbers):
return sum(numbers)
print(sum_all(1, 2, 3)) # 6
Rest must be last parameter:
function logInfo(action, ...items) {
console.log(`${action}:`, items);
}
logInfo("Deleting", "file1.txt", "file2.txt");
// Deleting: ["file1.txt", "file2.txt"]
Spread Operator
Expand an array into individual arguments:
const numbers = [1, 2, 3];
// Spread in function call
console.log(Math.max(...numbers)); // 3
// Without spread (passes array as single argument)
console.log(Math.max(numbers)); // NaN
Python comparison:
numbers = [1, 2, 3]
max(*numbers) # 3 (unpacking)
Array/object spreading:
// Arrays
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = [...arr1, ...arr2]; // [1, 2, 3, 4, 5, 6]
// Objects
const user = { name: "Alice", age: 30 };
const updated = { ...user, age: 31 }; // { name: "Alice", age: 31 }
3. Scope
Global Scope
Variables accessible everywhere:
const globalVar = "I'm global";
function test() {
console.log(globalVar); // Accessible
}
test(); // I'm global
console.log(globalVar); // I'm global
Function Scope
Variables declared in function are local to that function:
function test() {
const localVar = "I'm local";
console.log(localVar); // Accessible here
}
test(); // I'm local
// console.log(localVar); // Error: localVar is not defined
Python has same behavior:
def test():
local_var = "I'm local"
print(local_var)
test()
# print(local_var) # Error: not defined
Block Scope
let and const are block-scoped:
if (true) {
let blockVar = "Block scoped";
const alsoBlock = "Also block scoped";
var functionScoped = "Function scoped"; // Leaks out!
}
// console.log(blockVar); // Error
// console.log(alsoBlock); // Error
console.log(functionScoped); // Works (var ignores blocks)
Python doesn't have block scope:
if True:
block_var = "I'm accessible outside"
print(block_var) # Works! Python has function scope only
Lexical Scope (Static Scope)
Functions access variables from their definition location, not call location:
const name = "Global";
function outer() {
const name = "Outer";
function inner() {
console.log(name); // "Outer" (from definition scope)
}
return inner;
}
const name = "New Global";
const fn = outer();
fn(); // "Outer" (not "New Global")
4. Closures
A closure is a function that remembers variables from its outer scope.
Basic Closure
function makeCounter() {
let count = 0; // Private variable
return function() {
count++;
return count;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
Each call to makeCounter() creates a new closure:
const counter1 = makeCounter();
const counter2 = makeCounter();
console.log(counter1()); // 1
console.log(counter1()); // 2
console.log(counter2()); // 1 (separate counter)
Python comparison:
def make_counter():
count = 0
def counter():
nonlocal count # Need nonlocal to modify outer variable
count += 1
return count
return counter
counter = make_counter()
print(counter()) # 1
print(counter()) # 2
Practical Closure: Private Variables
function createBankAccount(initialBalance) {
let balance = initialBalance; // Private
return {
deposit(amount) {
balance += amount;
return balance;
},
withdraw(amount) {
if (amount > balance) {
return "Insufficient funds";
}
balance -= amount;
return balance;
},
getBalance() {
return balance;
}
};
}
const account = createBankAccount(100);
console.log(account.deposit(50)); // 150
console.log(account.withdraw(30)); // 120
console.log(account.getBalance()); // 120
// console.log(account.balance); // undefined (private!)
Closure in Loops (Classic Problem)
Problem:
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // What prints?
}, 1000);
}
// Prints: 3, 3, 3 (var has function scope, shared i)
Solution 1: Use let (block scope):
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // Prints: 0, 1, 2
}, 1000);
}
Solution 2: IIFE closure:
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(function() {
console.log(j); // Prints: 0, 1, 2
}, 1000);
})(i);
}
5. Higher-Order Functions
Functions that accept or return other functions.
Functions as Arguments
function processArray(arr, callback) {
const result = [];
for (let item of arr) {
result.push(callback(item));
}
return result;
}
const numbers = [1, 2, 3, 4, 5];
const squared = processArray(numbers, x => x * x);
console.log(squared); // [1, 4, 9, 16, 25]
Built-in Higher-Order Functions
Array.map():
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(x => x * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
Array.filter():
const numbers = [1, 2, 3, 4, 5];
const evens = numbers.filter(x => x % 2 === 0);
console.log(evens); // [2, 4]
Array.reduce():
const numbers = [1, 2, 3, 4, 5];
const sum = numbers.reduce((total, num) => total + num, 0);
console.log(sum); // 15
Python comparison:
numbers = [1, 2, 3, 4, 5]
doubled = list(map(lambda x: x * 2, numbers))
evens = list(filter(lambda x: x % 2 == 0, numbers))
from functools import reduce
sum_all = reduce(lambda a, b: a + b, numbers, 0)
# Or list comprehensions (more Pythonic)
doubled = [x * 2 for x in numbers]
evens = [x for x in numbers if x % 2 == 0]
Functions Returning Functions
function multiplyBy(factor) {
return function(number) {
return number * factor;
};
}
const double = multiplyBy(2);
const triple = multiplyBy(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
Currying:
// Traditional function
function add(a, b, c) {
return a + b + c;
}
// Curried function
function addCurried(a) {
return function(b) {
return function(c) {
return a + b + c;
};
};
}
console.log(addCurried(1)(2)(3)); // 6
// Arrow function currying
const addArrow = a => b => c => a + b + c;
console.log(addArrow(1)(2)(3)); // 6
6. The this Keyword
this in Methods
const user = {
name: "Alice",
greet() {
console.log(`Hello, I'm ${this.name}`);
}
};
user.greet(); // Hello, I'm Alice
Python comparison:
class User:
def __init__(self, name):
self.name = name
def greet(self):
print(f"Hello, I'm {self.name}")
user = User("Alice")
user.greet() # Hello, I'm Alice
this Binding Problems
Problem: Lost context:
const user = {
name: "Alice",
greet() {
console.log(`Hello, I'm ${this.name}`);
}
};
const greetFn = user.greet;
greetFn(); // Hello, I'm undefined (this = undefined/window)
Solution 1: Arrow function (no own this):
const user = {
name: "Alice",
greet: () => {
console.log(`Hello, I'm ${this.name}`); // this from outer scope
}
};
Solution 2: bind():
const user = {
name: "Alice",
greet() {
console.log(`Hello, I'm ${this.name}`);
}
};
const greetFn = user.greet.bind(user);
greetFn(); // Hello, I'm Alice
Solution 3: call() or apply():
const user = {
name: "Alice",
greet() {
console.log(`Hello, I'm ${this.name}`);
}
};
const otherUser = { name: "Bob" };
user.greet.call(otherUser); // Hello, I'm Bob
user.greet.apply(otherUser); // Hello, I'm Bob
Arrow Functions and this
Arrow functions don't have their own this - they inherit from parent scope:
const user = {
name: "Alice",
friends: ["Bob", "Charlie"],
printFriends() {
this.friends.forEach(function(friend) {
// console.log(`${this.name} knows ${friend}`); // Error: this is undefined
});
}
};
// Fix with arrow function
const user2 = {
name: "Alice",
friends: ["Bob", "Charlie"],
printFriends() {
this.friends.forEach(friend => {
console.log(`${this.name} knows ${friend}`); // Works!
});
}
};
user2.printFriends();
// Alice knows Bob
// Alice knows Charlie
7. IIFE (Immediately Invoked Function Expression)
Execute a function immediately after definition:
(function() {
console.log("IIFE executed!");
})();
// With arrow function
(() => {
console.log("Arrow IIFE!");
})();
Use case: Private scope (before modules):
const counter = (function() {
let count = 0; // Private
return {
increment() { return ++count; },
decrement() { return --count; },
getCount() { return count; }
};
})();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getCount()); // 2
8. Practical Examples
Example 1: Debounce Function
Delays execution until after user stops triggering:
function debounce(func, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => func(...args), delay);
};
}
// Usage: search input
const search = debounce((query) => {
console.log("Searching for:", query);
}, 500);
// search("a"); // Cancelled
// search("ap"); // Cancelled
// search("app"); // Runs after 500ms
Example 2: Memoization
Cache function results:
function memoize(fn) {
const cache = {};
return function(...args) {
const key = JSON.stringify(args);
if (key in cache) {
console.log("From cache");
return cache[key];
}
const result = fn(...args);
cache[key] = result;
return result;
};
}
const slowSquare = memoize(x => {
console.log("Computing...");
return x * x;
});
console.log(slowSquare(4)); // Computing... 16
console.log(slowSquare(4)); // From cache 16
Example 3: Function Composition
Combine multiple functions:
const compose = (...fns) => x =>
fns.reduceRight((acc, fn) => fn(acc), x);
const add5 = x => x + 5;
const multiply2 = x => x * 2;
const subtract3 = x => x - 3;
const calculate = compose(subtract3, multiply2, add5);
console.log(calculate(10)); // (10 + 5) * 2 - 3 = 27
9. Practical Exercises
Exercise 4.3.1: Function Types
Write the same function using:
- Function declaration
- Function expression
- Arrow function
- IIFE
Exercise 4.3.2: Closure Practice
Create a function createCounter() that:
- Returns an object with methods: increment, decrement, reset, getValue
- Keeps count private
- Reset returns count to initial value
Exercise 4.3.3: Higher-Order Functions
Use array methods to:
- Filter array of numbers for evens
- Map to square each number
- Reduce to sum all values
- Chain all operations
Exercise 4.3.4: Currying
Write a curried function for calculating volume:
const volume = length => width => height => length * width * height;
10. Knowledge Check
Question 1: What's the difference between function declaration and expression?
Show answer
Declarations are hoisted (can be called before definition). Expressions are not hoisted and must be defined before use.Question 2: What is a closure?
Show answer
A closure is a function that remembers variables from its outer/enclosing scope, even after the outer function has returned.Question 3: Why use arrow functions?
Show answer
Concise syntax, implicit return for single expressions, and lexical `this` binding (inherit from parent scope).Question 4: What does the spread operator ... do?
Show answer
Expands an iterable (array, string) into individual elements. Used in function calls, array/object literals, and destructuring.Question 5: What is function currying?
Show answer
Transforming a function that takes multiple arguments into a sequence of functions that each take a single argument.11. Key Takeaways
- Three ways to define functions: declaration (hoisted), expression, arrow (concise, lexical this)
- Use default parameters for optional arguments:
function(x = 0) - Rest parameters
...argscollect remaining arguments into array - Spread operator
...arrexpands array into individual elements - Scope: global → function → block (let/const only)
- Closures: functions remember outer scope variables
- Higher-order functions: accept or return other functions
- Arrow functions don't have own
this, inherit from parent thisbinding: method context, use bind/call/apply to set- IIFE:
(function() {})()for immediate execution
12. Further Resources
Documentation:
Next Steps
Excellent! You now understand JavaScript functions, scope, and closures.
In Lesson 4.4: Arrays & Objects, you'll learn to work with JavaScript's most important data structures, including array methods and object manipulation.
Next: Lesson 4.4 - Arrays & Objects →