Functions are the building blocks of any program. This comprehensive guide explores functions, scope, closures, and execution context across multiple programming languages.
Understanding Functions
What is a Function?
A function is a reusable block of code that performs a specific task. It can accept inputs (parameters), process them, and return an output.
# Python function
def greet(name):
"""Greet a person by name."""
return f"Hello, {name}!"
# Calling the function
message = greet("Alice")
print(message) # Output: Hello, Alice!
// JavaScript function
function greet(name) {
return `Hello, ${name}!`;
}
// Calling the function
const message = greet("Alice");
console.log(message); // Output: Hello, Alice!
Function Declaration vs Expression
// Function declaration (hoisted)
function greet(name) {
return `Hello, ${name}!`;
}
// Function expression (not hoisted)
const greet = function(name) {
return `Hello, ${name}!`;
};
// Arrow function (ES6+)
const greet = (name) => `Hello, ${name}!`;
// Arrow function with body
const greet = (name) => {
const message = `Hello, ${name}!`;
return message;
};
Types of Functions
Pure Functions
# Pure function - same input always produces same output
def add(a, b):
return a + b
# No side effects, no external state
result = add(2, 3) # Always returns 5
Impure Functions
# Impure function - has side effects
counter = 0
def increment():
global counter
counter += 1
return counter
# Or modifies external state
def add_to_list(items, new_item):
items.append(new_item) # Modifies the original list
return items
Higher-Order Functions
# Functions that accept other functions
def apply_operation(func, value):
return func(value)
def square(x):
return x ** 2
def cube(x):
return x ** 3
# Using higher-order function
print(apply_operation(square, 5)) # 25
print(apply_operation(cube, 5)) # 125
# Built-in higher-order functions
numbers = [1, 2, 3, 4, 5]
# map - transform each element
squared = list(map(lambda x: x ** 2, numbers))
print(squared) # [1, 4, 9, 16, 25]
# filter - select elements
evens = list(filter(lambda x: x % 2 == 0, numbers))
print(evens) # [2, 4]
# reduce - accumulate elements
from functools import reduce
total = reduce(lambda acc, x: acc + x, numbers)
print(total) # 15
Scope
Global vs Local Scope
# Global scope
global_var = "I am global"
def function_with_scope():
# Local scope
local_var = "I am local"
print(global_var) # Accessible
print(local_var) # Accessible
function_with_scope()
# print(local_var) # NameError: name 'local_var' is not defined
print(global_var) # Works
// JavaScript scope
var globalVar = "I am global"; // Function-scoped (old way)
let blockVar = "I am block-scoped"; // Block-scoped (modern)
function functionWithScope() {
var functionVar = "I am function-scoped";
let blockVar = "I am block-scoped";
console.log(globalVar); // Accessible
console.log(functionVar); // Accessible
console.log(blockVar); // Accessible
if (true) {
var ifVar = "I escape the if block";
let blockScoped = "I don't escape";
}
console.log(ifVar); // Accessible! (var is function-scoped)
// console.log(blockScoped); // ReferenceError
}
functionWithScope();
console.log(globalVar); // Works
// console.log(functionVar); // ReferenceError
Lexical vs Dynamic Scope
# Python uses LEGB rule:
# L - Local
# E - Enclosing (for nested functions)
# G - Global
# B - Built-in
x = "global"
def outer():
x = "enclosing"
def inner():
x = "local"
print(f"Inner: {x}") # local
inner()
print(f"Outer: {x}") # enclosing
outer()
print(f"Global: {x}") # global
Closures
What is a Closure?
A closure is a function that remembers variables from its outer scope even after the outer function has returned.
def counter():
count = 0
def increment():
nonlocal count
count += 1
return count
return increment
# Create counter instances
counter1 = counter()
counter2 = counter()
print(counter1()) # 1
print(counter1()) # 2
print(counter2()) # 1 (separate closure)
// JavaScript closure
function counter() {
let count = 0;
return function increment() {
count++;
return count;
};
}
const counter1 = counter();
const counter2 = counter();
console.log(counter1()); // 1
console.log(counter1()); // 2
console.log(counter2()); // 1 (separate closure)
// Practical example: private variables
function createPerson(name) {
let _name = name; // Private
return {
getName() {
return _name;
},
setName(newName) {
_name = newName;
}
};
}
const person = createPerson("Alice");
console.log(person.getName()); // Alice
person.setName("Bob");
console.log(person.getName()); // Bob
// person._name would be undefined
Common Closure Patterns
// Factory function
function multiply(factor) {
return function(number) {
return number * factor;
};
}
const double = multiply(2);
const triple = multiply(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
// Event handlers
function createClickHandler(message) {
return function(event) {
console.log(message);
};
}
const button = document.createElement('button');
button.addEventListener('click', createClickHandler('Button clicked!'));
// Memoization (caching)
function memoize(fn) {
const cache = {};
return function(...args) {
const key = JSON.stringify(args);
if (key in cache) {
return cache[key];
}
const result = fn.apply(this, args);
cache[key] = result;
return result;
};
}
const memoizedFibonacci = memoize(function(n) {
if (n <= 1) return n;
return memoizedFibonacci(n - 1) + memoizedFibonacci(n - 2);
});
Execution Context
JavaScript Execution Context
// Execution context has three main components:
// 1. Variable Environment
// 2. Scope Chain
// 3. this binding
// Global context
console.log(this); // Window (in browser) or global (in Node)
// Function context
function fn() {
console.log(this); // Depends on how function is called
}
// Different this bindings
const obj = {
name: 'Alice',
greet() {
console.log(this.name);
}
};
obj.greet(); // 'Alice' - this is obj
const greetFn = obj.greet;
greetFn(); // undefined - this is window/global
// Arrow functions and this
const obj2 = {
name: 'Bob',
greet() {
const inner = () => {
console.log(this.name);
};
inner();
}
};
obj2.greet(); // 'Bob' - arrow inherits this from parent
Call, Apply, and Bind
function greet(greeting, punctuation) {
console.log(`${greeting}, ${this.name}${punctuation}`);
}
const person = { name: 'Alice' };
// call - invoke with specified this
greet.call(person, 'Hello', '!'); // Hello, Alice!
// apply - like call, but takes array of arguments
greet.apply(person, ['Hi', '?']); // Hi, Alice?
// bind - create new function with bound this
const boundGreet = greet.bind(person);
boundGreet('Hey', '.'); // Hey, Alice.
// Partial application with bind
const greetAlice = greet.bind(person, 'Hello');
greetAlice('!'); // Hello, Alice!
Best Practices
Function Design
# Good: Single responsibility
def calculate_area(radius):
"""Calculate circle area."""
return 3.14159 * radius ** 2
# Good: Clear parameters and return type
from typing import List
def find_duplicates(items: List[int]) -> List[int]:
"""Find duplicate values in a list."""
seen = set()
duplicates = []
for item in items:
if item in seen:
duplicates.append(item)
else:
seen.add(item)
return duplicates
# Good: Use default arguments wisely
def create_user(name, role='user', active=True):
return {
'name': name,
'role': role,
'active': active
}
# Avoid: Mutable default arguments
# BAD - list is shared across calls
def add_item(item, items=[]):
items.append(item)
return items
# GOOD - use None and create inside
def add_item_good(item, items=None):
if items is None:
items = []
items.append(item)
return items
// Good: Use arrow functions for callbacks
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(n => n * 2);
// Good: Use destructuring for parameters
function greet({ name, age = 'unknown' }) {
return `Hello, ${name}! You are ${age}.`;
}
greet({ name: 'Alice', age: 30 });
// Good: Rest parameters
function sum(...numbers) {
return numbers.reduce((a, b) => a + b, 0);
}
// Good: Async functions
async function fetchData(url) {
try {
const response = await fetch(url);
return await response.json();
} catch (error) {
console.error('Failed to fetch:', error);
throw error;
}
}
Naming Conventions
# Functions: snake_case, descriptive verbs
def calculate_total():
pass
def fetch_user_by_id():
pass
def validate_email_format():
pass
// Functions: camelCase, descriptive verbs
function calculateTotal() {}
function fetchUserById() {}
function validateEmailFormat() {}
// For predicates, use is/are/has/can
function isValid() {}
function hasPermission() {}
function canAccess() {}
Common Pitfalls
Python
# Pitfall 1: Modifying global variable without declaration
counter = 0
def increment():
counter += 1 # UnboundLocalError
return counter
# Fix: Use global keyword
def increment_fixed():
global counter
counter += 1
return counter
# Pitfall 2: Late binding in closures
functions = [lambda x: x + i for i in range(3)]
print([f(0) for f in functions]) # [2, 2, 2] - all use i=2
# Fix: Use default argument
functions_fixed = [lambda x, i=i: x + i for i in range(3)]
print([f(0) for f in functions_fixed]) # [0, 1, 2]
JavaScript
// Pitfall 1: Understanding hoisting with var
console.log(hoistedVar); // undefined (not ReferenceError)
var hoistedVar = 'hello';
// let and const are not hoisted
// console.log(hoistedLet); // ReferenceError
let hoistedLet = 'world';
// Pitfall 2: Loop with closure
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Outputs: 3, 3, 3
// Fix: Use let (block-scoped)
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log(j), 100);
}
// Outputs: 0, 1, 2
// Pitfall 3: this in callbacks
const person = {
name: 'Alice',
greet() {
setTimeout(function() {
console.log(this.name); // undefined
}, 100);
}
};
// Fix: Use arrow function or bind
const personFixed = {
name: 'Alice',
greet() {
setTimeout(() => {
console.log(this.name); // Alice
}, 100);
}
};
Comments