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
  • return implicit for single expression
  • No own this binding (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:

  1. Function declaration
  2. Function expression
  3. Arrow function
  4. IIFE

Exercise 4.3.2: Closure Practice

Create a function createCounter() that:

  1. Returns an object with methods: increment, decrement, reset, getValue
  2. Keeps count private
  3. Reset returns count to initial value

Exercise 4.3.3: Higher-Order Functions

Use array methods to:

  1. Filter array of numbers for evens
  2. Map to square each number
  3. Reduce to sum all values
  4. 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 ...args collect remaining arguments into array
  • Spread operator ...arr expands 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
  • this binding: 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 →