Functions Advanced - Exercise 2

A closure is a function that remembers and can access variables from its outer scope even after the outer function has finished executing. Closures are created every time a function is created and allow functions to maintain a reference to their lexical environment.

javascript

function outer() {
  let count = 0;
  
  function inner() {
    count++;
    return count;
  }
  
  return inner;
}

const counter = outer();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

Closures enable data privacy by hiding variables inside a function scope where they cannot be accessed directly from outside. Only the inner functions returned by the outer function can access and modify these private variables.

javascript

function createBankAccount(initialBalance) {
  let balance = initialBalance;
  
  return {
    deposit: function(amount) {
      balance += amount;
      return balance;
    },
    withdraw: function(amount) {
      if (amount <= balance) {
        balance -= amount;
        return balance;
      }
      return 'Insufficient funds';
    },
    getBalance: function() {
      return balance;
    }
  };
}

const account = createBankAccount(100);
console.log(account.getBalance()); // 100
console.log(account.deposit(50)); // 150
console.log(account.balance); // undefined (private)

Lexical scope means that the scope of a variable is determined by where it is written in the source code, not where it is called. Inner functions have access to variables defined in their outer functions based on their position in the code.

javascript

const globalVar = 'global';

function outer() {
  const outerVar = 'outer';
  
  function inner() {
    const innerVar = 'inner';
    console.log(globalVar); // 'global'
    console.log(outerVar); // 'outer'
    console.log(innerVar); // 'inner'
  }
  
  inner();
}

outer();

In regular functions, the value of this is determined by how the function is called, not where it is defined. When called as a method, this refers to the object. When called alone, this refers to the global object or undefined in strict mode.

javascript

const person = {
  name: 'Alice',
  greet: function() {
    console.log('Hello, ' + this.name);
  }
};

person.greet(); // 'Hello, Alice'

const greetFn = person.greet;
greetFn(); // 'Hello, undefined' (this is global/undefined)

function showThis() {
  console.log(this);
}

showThis(); // window (browser) or global (Node.js)

Arrow functions do not have their own this binding. Instead, they inherit this from the surrounding lexical scope where they are defined. This makes arrow functions useful for callbacks where you want to preserve the outer this value.

javascript

const person = {
  name: 'Bob',
  greetRegular: function() {
    setTimeout(function() {
      console.log('Regular: ' + this.name);
    }, 100);
  },
  greetArrow: function() {
    setTimeout(() => {
      console.log('Arrow: ' + this.name);
    }, 100);
  }
};

person.greetRegular(); // 'Regular: undefined'
person.greetArrow(); // 'Arrow: Bob'

The bind() method creates a new function with a permanently bound this value. Unlike call() and apply(), bind() does not immediately invoke the function. It returns a new function that you can call later with the bound context.

javascript

const person = {
  name: 'Charlie'
};

function greet(greeting, punctuation) {
  console.log(greeting + ', ' + this.name + punctuation);
}

const boundGreet = greet.bind(person);
boundGreet('Hello', '!'); // 'Hello, Charlie!'

const boundWithArgs = greet.bind(person, 'Hi');
boundWithArgs('?'); // 'Hi, Charlie?'

The call() method invokes a function immediately with a specified this value and arguments passed individually. It is useful when you want to borrow a method from one object and use it on another object.

javascript

function introduce(city, country) {
  console.log('I am ' + this.name + ' from ' + city + ', ' + country);
}

const user1 = { name: 'Diana' };
const user2 = { name: 'Edward' };

introduce.call(user1, 'Paris', 'France');
// 'I am Diana from Paris, France'

introduce.call(user2, 'Tokyo', 'Japan');
// 'I am Edward from Tokyo, Japan'

The apply() method works like call() but takes arguments as an array instead of individually. It is useful when you have an array of arguments or want to use array methods like Math.max with an array of numbers.

javascript

function introduce(city, country) {
  console.log('I am ' + this.name + ' from ' + city + ', ' + country);
}

const user = { name: 'Fiona' };
const args = ['London', 'UK'];

introduce.apply(user, args);
// 'I am Fiona from London, UK'

const numbers = [5, 2, 9, 1, 7];
const max = Math.max.apply(null, numbers);
console.log(max); // 9

Recursion is when a function calls itself to solve a problem by breaking it into smaller subproblems. Every recursive function needs a base case to stop the recursion and prevent infinite loops.

javascript

function factorial(n) {
  if (n <= 1) {
    return 1;
  }
  return n * factorial(n - 1);
}

console.log(factorial(5)); // 120 (5 * 4 * 3 * 2 * 1)

function countdown(num) {
  if (num <= 0) {
    console.log('Done!');
    return;
  }
  console.log(num);
  countdown(num - 1);
}

countdown(3); // 3, 2, 1, Done!

A pure function always returns the same output for the same input and has no side effects. It does not modify external variables, make API calls, or change anything outside its scope. Pure functions are predictable and easy to test.

javascript

// Pure function
function add(a, b) {
  return a + b;
}

console.log(add(2, 3)); // Always 5
console.log(add(2, 3)); // Always 5

// Impure function (modifies external state)
let total = 0;
function addToTotal(value) {
  total += value;
  return total;
}

console.log(addToTotal(5)); // 5
console.log(addToTotal(5)); // 10 (different result)

Function composition is the process of combining two or more functions to create a new function. The output of one function becomes the input of the next. This allows you to build complex operations from simple, reusable functions.

javascript

const double = x => x * 2;
const addTen = x => x + 10;
const square = x => x * x;

// Manual composition
const result = square(addTen(double(5)));
console.log(result); // 400 ((5*2)+10)^2

// Compose function
const compose = (...fns) => x => 
  fns.reduceRight((acc, fn) => fn(acc), x);

const calculate = compose(square, addTen, double);
console.log(calculate(5)); // 400

Currying transforms a function with multiple arguments into a sequence of functions, each taking a single argument. This allows you to create specialized versions of a function by partially applying arguments.

javascript

// Regular function
function multiply(a, b, c) {
  return a * b * c;
}

// Curried version
function curriedMultiply(a) {
  return function(b) {
    return function(c) {
      return a * b * c;
    };
  };
}

console.log(curriedMultiply(2)(3)(4)); // 24

// Arrow function currying
const add = a => b => a + b;

const addFive = add(5);
console.log(addFive(3)); // 8
console.log(addFive(10)); // 15