Lesson 20-Functional Programming in JavaScript

Named Function Expressions

Named function expressions create anonymous functions with an assigned name. Although the name is not visible in the outer scope (i.e., it cannot be called directly by its name), it is useful within the function for debugging, recursion, and improved readability. Below are common types of named function expressions and their explanations:

Standard Named Function Expression

const myFunction = function namedFunction(param1, param2) {
  // Function body
  return param1 + param2;
};

// Call via variable name
const result = myFunction(3, 4); // result = 7

namedFunction is the internal name, visible only within the function body. External calls use the assigned variable name myFunction. The internal name is primarily for internal references or recursion.

Immediately Invoked Function Expression (IIFE)

(function namedIIFE(param1, param2) {
  // Function body
  console.log(param1 + param2);
})(5, 6); // Output: 11

// Cannot access function by name externally
namedIIFE(); // Error: ReferenceError: namedIIFE is not defined

An IIFE is a named function expression that executes immediately after creation. The function is wrapped in parentheses, followed by a parameter list and invocation. The internal name namedIIFE is only available within the function body.

Named Function Expression as Object Method

const myObject = {
  doSomething: function namedMethod() {
    // Function body
    console.log('Doing something...');
  }
};

// Call via object property
myObject.doSomething(); // Output: Doing something...

// Cannot access method by name externally
namedMethod(); // Error: ReferenceError: namedMethod is not defined

Here, a named function expression defines an object method. The internal name namedMethod is available within the method body, but externally, it’s accessed via the object’s property doSomething.

Named Function Expression as Array Method Callback

const numbers = [1, 2, 3, 4];
numbers.forEach(function namedCallback(number) {
  // Function body
  console.log(number * number);
});

// Cannot access callback by name externally
namedCallback(5); // Error: ReferenceError: namedCallback is not defined

The named function expression serves as a callback for forEach. The internal name namedCallback is only available within the callback, not externally.

Named Function Expression in Constructors

function MyClass() {
  this.myMethod = function namedConstructorMethod() {
    // Function body
    console.log('Method called from constructor');
  };
}

const instance = new MyClass();

// Call via instance method
instance.myMethod(); // Output: Method called from constructor

// Cannot access method by name externally
namedConstructorMethod(); // Error: ReferenceError: namedConstructorMethod is not defined

In a constructor, a named function expression defines an instance method. The internal name namedConstructorMethod is only available within the method body, and externally, it’s accessed via the instance’s myMethod property.

Functional Programming Basics

Pure Functions and Side Effects

Pure functions are a core concept in functional programming, meeting two criteria:

  1. Same input always produces the same output.
  2. No observable side effects (e.g., modifying global variables, performing I/O).
// Pure function example
function add(a, b) {
  return a + b;
}

// Impure function example (with side effect)
let counter = 0;
function increment() {
  counter++; // Modifies external variable
  return counter;
}

// Impure function example (depends on external state)
const PI = 3.14;
function calculateArea(radius) {
  return PI * radius * radius; // Depends on external PI
}

// More complex pure function
function pureFunction(arr) {
  // Creates new array instead of modifying original
  return arr.map(item => item * 2).filter(item => item > 10);
}

// Side effect example
function impureFunction(arr) {
  // Modifies original array
  arr.push(100);
  console.log('Logging is a side effect'); // Console output is a side effect
  return arr.length;
}

Functions as First-Class Citizens (Higher-Order Functions)

In JavaScript, functions are first-class citizens, meaning they can be passed, assigned, or returned like other values. Higher-order functions accept functions as arguments or return functions.

// Higher-order function accepting a function as argument
function higherOrderFunction(callback) {
  console.log('Before callback');
  callback();
  console.log('After callback');
}

higherOrderFunction(() => {
  console.log('Callback executed');
});

// Higher-order function returning a function
function createMultiplier(factor) {
  return function(number) {
    return number * factor;
  };
}

const double = createMultiplier(2);
const triple = createMultiplier(3);

console.log(double(5)); // 10
console.log(triple(5)); // 15

// Practical higher-order function
function withLogging(fn) {
  return function(...args) {
    console.log(`Calling function with args: ${args}`);
    const result = fn(...args);
    console.log(`Function returned: ${result}`);
    return result;
  };
}

const loggedAdd = withLogging(add);
loggedAdd(2, 3); // Logs call and return

Currying and Partial Application

Currying transforms a multi-parameter function into a sequence of single-parameter functions. Partial application fixes some parameters of a function, producing a new function with fewer parameters.

// Currying example
function curryAdd(a) {
  return function(b) {
    return a + b;
  };
}

const add5 = curryAdd(5);
console.log(add5(3)); // 8

// Generic currying function
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args);
    } else {
      return function(...moreArgs) {
        return curried.apply(this, args.concat(moreArgs));
      };
    }
  };
}

const curriedAdd = curry(function(a, b, c) {
  return a + b + c;
});

console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
console.log(curriedAdd(1, 2, 3)); // 6

// Partial application example
function partial(fn, ...presetArgs) {
  return function(...laterArgs) {
    return fn.apply(this, presetArgs.concat(laterArgs));
  };
}

const add10 = partial(add, 10);
console.log(add10(5)); // 15

// Practical currying and partial application
const users = [
  { name: 'Alice', age: 25 },
  { name: 'Bob', age: 30 },
  { name: 'Charlie', age: 35 }
];

// Standard function
function filterUsers(users, condition) {
  return users.filter(condition);
}

// Curried version
const curryFilterUsers = curry(function(users, condition) {
  return users.filter(condition);
});

const filterAdults = curryFilterUsers(users);
const adults = filterAdults(user => user.age >= 18);
console.log(adults);

// Partial application version
const filterByAge = partial(filterUsers, users);
const youngUsers = filterByAge(user => user.age < 30);
console.log(youngUsers);

Closures and Scope Chain

A closure is a function that remembers and accesses its lexical scope, even when executed outside that scope. Closures are key for data privacy and function factories in JavaScript.

// Basic closure example
function createCounter() {
  let count = 0; // Private variable

  return function() {
    count += 1;
    return count;
  };
}

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

// Closure and scope chain
function outer() {
  const outerVar = 'I am outer';

  function inner() {
    const innerVar = 'I am inner';
    console.log(outerVar); // Accesses outer variable
    console.log(innerVar); // Accesses inner variable
  }

  return inner;
}

const innerFunc = outer();
innerFunc(); // Accesses outerVar even after outer execution

// Practical closure - Module pattern
const module = (function() {
  const privateVar = 'I am private';

  function privateFunction() {
    console.log(privateVar);
  }

  return {
    publicMethod: function() {
      privateFunction();
    }
  };
})();

module.publicMethod(); // Accesses private members
// console.log(module.privateVar); // undefined
// module.privateFunction(); // TypeError

// Closure with loops - Classic issue and solutions
const funcs = [];
for (var i = 0; i < 3; i++) {
  funcs.push(function() {
    console.log(i); // Outputs 3 for all
  });
}

funcs.forEach(func => func());

// Solution 1: Use IIFE to create closure
const funcs1 = [];
for (var i = 0; i < 3; i++) {
  (function(j) {
    funcs1.push(function() {
      console.log(j);
    });
  })(i);
}

funcs1.forEach(func => func());

// Solution 2: Use let (block scope)
const funcs2 = [];
for (let i = 0; i < 3; i++) {
  funcs2.push(function() {
    console.log(i);
  });
}

funcs2.forEach(func => func());

Recursion and Tail Recursion Optimization

Recursion is a common functional programming technique where a function calls itself to solve a problem. Tail recursion is a special form that can be optimized to avoid stack overflow.

// Basic recursion - Factorial
function factorial(n) {
  if (n === 0) return 1;
  return n * factorial(n - 1);
}

console.log(factorial(5)); // 120

// Basic recursion - Fibonacci
function fibonacci(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
}

console.log(fibonacci(10)); // 55

// Tail recursion - Factorial
function factorialTail(n, accumulator = 1) {
  if (n === 0) return accumulator;
  return factorialTail(n - 1, n * accumulator);
}

console.log(factorialTail(5)); // 120

// Tail recursion - Fibonacci
function fibonacciTail(n, a = 0, b = 1) {
  if (n === 0) return a;
  if (n === 1) return b;
  return fibonacciTail(n - 1, b, a + b);
}

console.log(fibonacciTail(10)); // 55

// Note: Most JavaScript engines (including V8) do not yet optimize tail calls (TCO)
// Manual optimization using loops

// Manual tail recursion optimization - Factorial
function factorialOptimized(n) {
  let result = 1;
  for (let i = 1; i <= n; i++) {
    result *= i;
  }
  return result;
}

console.log(factorialOptimized(5)); // 120

// Manual tail recursion optimization - Fibonacci
function fibonacciOptimized(n) {
  if (n <= 1) return n;

  let a = 0, b = 1;
  for (let i = 2; i <= n; i++) {
    const temp = a + b;
    a = b;
    b = temp;
  }
  return b;
}

console.log(fibonacciOptimized(10)); // 55

// Practical recursion - Tree traversal
const tree = {
  value: 1,
  children: [
    {
      value: 2,
      children: [
        { value: 4, children: [] },
        { value: 5, children: [] }
      ]
    },
    {
      value: 3,
      children: [
        { value: 6, children: [] },
        { value: 7, children: [] }
      ]
    }
  ]
};

// Recursive tree traversal
function traverseTree(node, callback) {
  callback(node.value);
  node.children.forEach(child => traverseTree(child, callback));
}

traverseTree(tree, value => console.log(value));

// Tail-recursive tree traversal (simulated)
function traverseTreeOptimized(node, callback, stack = []) {
  callback(node.value);
  if (node.children.length > 0) {
    const [firstChild, ...restChildren] = node.children;
    stack.push(...restChildren);
    traverseTreeOptimized(firstChild, callback, stack);
  } else if (stack.length > 0) {
    traverseTreeOptimized(stack.shift(), callback, stack);
  }
}

traverseTreeOptimized(tree, value => console.log(value));

Membership Required

You must be a member to access this content.

View Membership Levels

Already a member? Log in here
Share your love