Functions Advanced - Exercise 3

Memoization is an optimization technique that caches the results of expensive function calls. When the same inputs occur again, the cached result is returned instead of recomputing the value.

javascript

// Create a memoize function
function memoize(fn) {
  const cache = {};
  return function(...args) {
    const key = JSON.stringify(args);
    if (key in cache) {
      console.log('Returning cached result');
      return cache[key];
    }
    const result = fn.apply(this, args);
    cache[key] = result;
    return result;
  };
}

// Example: memoized factorial
const factorial = memoize(function(n) {
  if (n <= 1) return 1;
  return n * factorial(n - 1);
});

console.log(factorial(5)); // Computes: 120
console.log(factorial(5)); // Returns cached: 120

Partial application is a technique where you create a new function by pre-filling some arguments of an existing function. The new function takes the remaining arguments when called.

javascript

// Basic partial application
function multiply(a, b, c) {
  return a * b * c;
}

// Create a partial function with first argument fixed
function partial(fn, ...fixedArgs) {
  return function(...remainingArgs) {
    return fn(...fixedArgs, ...remainingArgs);
  };
}

const multiplyBy2 = partial(multiply, 2);
console.log(multiplyBy2(3, 4)); // 2 * 3 * 4 = 24

const multiplyBy2And3 = partial(multiply, 2, 3);
console.log(multiplyBy2And3(4)); // 2 * 3 * 4 = 24

// Using bind for partial application
const double = multiply.bind(null, 2, 1);
console.log(double(5)); // 2 * 1 * 5 = 10

Generators are special functions that can pause and resume execution. They are defined using function* syntax and return an iterator object that produces values on demand using yield.

javascript

// Define a generator function
function* numberGenerator() {
  yield 1;
  yield 2;
  yield 3;
}

const gen = numberGenerator();

console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: undefined, done: true }

// Infinite sequence generator
function* infiniteCounter() {
  let count = 0;
  while (true) {
    yield count++;
  }
}

const counter = infiniteCounter();
console.log(counter.next().value); // 0
console.log(counter.next().value); // 1
console.log(counter.next().value); // 2

The yield keyword pauses generator function execution and returns a value to the caller. When next() is called again, execution resumes from where it paused. Yield can also receive values passed to next().

javascript

// Basic yield usage
function* fruitGenerator() {
  yield 'Apple';
  yield 'Banana';
  return 'Done';
}

const fruits = fruitGenerator();
console.log(fruits.next()); // { value: 'Apple', done: false }
console.log(fruits.next()); // { value: 'Banana', done: false }
console.log(fruits.next()); // { value: 'Done', done: true }

// Receiving values through yield
function* conversation() {
  const name = yield 'What is your name?';
  const hobby = yield `Hello ${name}! What is your hobby?`;
  yield `${name} likes ${hobby}!`;
}

const chat = conversation();
console.log(chat.next().value);        // "What is your name?"
console.log(chat.next('Alice').value); // "Hello Alice! What is your hobby?"
console.log(chat.next('coding').value); // "Alice likes coding!"

A function factory is a function that returns other functions. It uses closures to create specialized functions with pre-configured behavior or values.

javascript

// Function factory for creating multipliers
function createMultiplier(multiplier) {
  return function(number) {
    return number * multiplier;
  };
}

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

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

// Function factory for creating validators
function createValidator(minLength, maxLength) {
  return function(value) {
    const len = value.length;
    return len >= minLength && len <= maxLength;
  };
}

const validateUsername = createValidator(3, 20);
const validatePassword = createValidator(8, 50);

console.log(validateUsername('Jo')); // false
console.log(validateUsername('John')); // true
console.log(validatePassword('pass')); // false

The module pattern uses closures to create private variables and methods while exposing a public API. It helps organize code and prevents pollution of the global namespace.

javascript

// Module pattern using IIFE
const Counter = (function() {
  // Private variable
  let count = 0;
  
  // Private function
  function log(message) {
    console.log(`[Counter] ${message}`);
  }
  
  // Public API
  return {
    increment: function() {
      count++;
      log(`Incremented to ${count}`);
    },
    decrement: function() {
      count--;
      log(`Decremented to ${count}`);
    },
    getCount: function() {
      return count;
    }
  };
})();

Counter.increment(); // [Counter] Incremented to 1
Counter.increment(); // [Counter] Incremented to 2
console.log(Counter.getCount()); // 2
console.log(Counter.count); // undefined (private)

Debouncing delays function execution until a certain time has passed since the last call. If the function is called again before the delay ends, the timer resets. This is useful for search inputs and resize handlers.

javascript

// Debounce implementation
function debounce(func, delay) {
  let timeoutId;
  return function(...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => {
      func.apply(this, args);
    }, delay);
  };
}

// Example: debounced search
function searchAPI(query) {
  console.log(`Searching for: ${query}`);
}

const debouncedSearch = debounce(searchAPI, 300);

// Simulating rapid typing
debouncedSearch('h');
debouncedSearch('he');
debouncedSearch('hel');
debouncedSearch('hello');
// Only "Searching for: hello" is logged after 300ms

// Usage with input element
// input.addEventListener('input', debouncedSearch);

Throttling ensures a function is called at most once within a specified time period. Unlike debouncing, throttling guarantees regular execution during continuous events like scrolling.

javascript

// Throttle implementation
function throttle(func, limit) {
  let inThrottle = false;
  return function(...args) {
    if (!inThrottle) {
      func.apply(this, args);
      inThrottle = true;
      setTimeout(() => {
        inThrottle = false;
      }, limit);
    }
  };
}

// Example: throttled scroll handler
function handleScroll() {
  console.log('Scroll position:', window.scrollY);
}

const throttledScroll = throttle(handleScroll, 100);

// Logs at most once every 100ms during scroll
window.addEventListener('scroll', throttledScroll);

// Difference from debounce:
// Debounce: waits for pause in events
// Throttle: executes regularly during events

Tagged template literals let you parse template literals with a custom function. The tag function receives the string parts and interpolated values as separate arguments, allowing custom processing.

javascript

// Basic tagged template
function highlight(strings, ...values) {
  let result = '';
  strings.forEach((str, i) => {
    result += str;
    if (values[i] !== undefined) {
      result += `<strong>${values[i]}</strong>`;
    }
  });
  return result;
}

const name = 'Alice';
const age = 25;
const output = highlight`Name: ${name}, Age: ${age}`;
console.log(output);
// "Name: <strong>Alice</strong>, Age: <strong>25</strong>"

// Practical example: SQL escaping
function sql(strings, ...values) {
  return strings.reduce((result, str, i) => {
    const value = values[i - 1];
    const escaped = typeof value === 'string' 
      ? value.replace(/'/g, "''") 
      : value;
    return result + escaped + str;
  });
}

const userInput = "O'Brien";
const query = sql`SELECT * FROM users WHERE name = '${userInput}'`;
console.log(query); // Safe query with escaped quotes

The try catch statement handles runtime errors gracefully. Code that might throw an error goes in the try block, and error handling logic goes in the catch block. The finally block runs regardless of the outcome.

javascript

// Basic try catch
function parseJSON(jsonString) {
  try {
    const data = JSON.parse(jsonString);
    return { success: true, data };
  } catch (error) {
    return { success: false, error: error.message };
  }
}

console.log(parseJSON('{"name":"John"}')); 
// { success: true, data: { name: 'John' } }

console.log(parseJSON('invalid json')); 
// { success: false, error: 'Unexpected token...' }

// Using finally
function fetchData() {
  let loading = true;
  try {
    // Simulating operation that might fail
    throw new Error('Network error');
  } catch (error) {
    console.log('Error:', error.message);
  } finally {
    loading = false;
    console.log('Loading:', loading); // Always runs
  }
}

fetchData();
// Error: Network error
// Loading: false

Tail call optimization (TCO) is a feature where the engine reuses the current stack frame for a function call in tail position. This prevents stack overflow in recursive functions. Note that TCO is only fully supported in Safari.

javascript

// Not tail call optimized (operation after recursion)
function factorial(n) {
  if (n <= 1) return 1;
  return n * factorial(n - 1); // Multiplication happens after call
}

// Tail call optimized version
function factorialTCO(n, accumulator = 1) {
  if (n <= 1) return accumulator;
  return factorialTCO(n - 1, n * accumulator); // Last action is the call
}

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

// Tail optimized sum
function sumTCO(arr, index = 0, total = 0) {
  if (index === arr.length) return total;
  return sumTCO(arr, index + 1, total + arr[index]);
}

console.log(sumTCO([1, 2, 3, 4, 5])); // 15

// Note: Use strict mode for TCO
// 'use strict';

The arguments object is an array-like object available inside regular functions that contains all passed arguments. It is useful when you do not know how many arguments will be passed. Arrow functions do not have their own arguments object.

javascript

// Using arguments object
function sum() {
  let total = 0;
  for (let i = 0; i < arguments.length; i++) {
    total += arguments[i];
  }
  return total;
}

console.log(sum(1, 2, 3)); // 6
console.log(sum(1, 2, 3, 4, 5)); // 15

// Converting arguments to array
function logArgs() {
  const argsArray = Array.from(arguments);
  console.log(argsArray);
}

logArgs('a', 'b', 'c'); // ['a', 'b', 'c']

// Modern alternative: rest parameters
function modernSum(...numbers) {
  return numbers.reduce((sum, n) => sum + n, 0);
}

console.log(modernSum(1, 2, 3, 4)); // 10

// Arrow functions use outer arguments
function outer() {
  const arrow = () => console.log(arguments[0]);
  arrow();
}

outer('Hello'); // 'Hello'