15 minlesson

Advanced Function Patterns

Advanced Function Patterns

This lesson covers advanced function techniques that you'll encounter in professional codebases.

The this Keyword in Depth

this is one of the most confusing aspects of JavaScript. Its value depends on how a function is called, not where it's defined.

Four Rules for this

javascript
1// 1. Default binding: this = global (or undefined in strict mode)
2function showThis() {
3 console.log(this);
4}
5showThis(); // window (or undefined)
6
7// 2. Implicit binding: this = object before the dot
8const user = {
9 name: "Alice",
10 greet() {
11 console.log(this.name);
12 }
13};
14user.greet(); // "Alice" (this = user)
15
16// 3. Explicit binding: this = what you specify
17function greet() {
18 console.log(this.name);
19}
20const bob = { name: "Bob" };
21greet.call(bob); // "Bob"
22greet.apply(bob); // "Bob"
23greet.bind(bob)(); // "Bob"
24
25// 4. new binding: this = newly created object
26function Person(name) {
27 this.name = name;
28}
29const alice = new Person("Alice"); // this = new object

Losing this

A common bug occurs when methods are passed as callbacks:

javascript
1const user = {
2 name: "Alice",
3 greet() {
4 console.log(`Hi, I'm ${this.name}`);
5 }
6};
7
8user.greet(); // "Hi, I'm Alice"
9
10// BUG: this is lost!
11const greetFn = user.greet;
12greetFn(); // "Hi, I'm undefined"
13
14// BUG: In callbacks
15setTimeout(user.greet, 100); // "Hi, I'm undefined"
16
17// FIX 1: bind
18setTimeout(user.greet.bind(user), 100);
19
20// FIX 2: Arrow function wrapper
21setTimeout(() => user.greet(), 100);
22
23// FIX 3: Arrow method (but with caveats)
24const user2 = {
25 name: "Bob",
26 greet: () => console.log(`Hi, I'm ${this.name}`) // Still wrong!
27};

call, apply, and bind

These methods let you control this:

call() - Invoke with arguments

javascript
1function introduce(greeting, punctuation) {
2 console.log(`${greeting}, I'm ${this.name}${punctuation}`);
3}
4
5const person = { name: "Alice" };
6introduce.call(person, "Hello", "!"); // "Hello, I'm Alice!"

apply() - Invoke with array of arguments

javascript
1introduce.apply(person, ["Hi", "?"]); // "Hi, I'm Alice?"
2
3// Useful for spreading arrays into functions
4const numbers = [5, 6, 2, 3, 7];
5Math.max.apply(null, numbers); // 7
6
7// Modern alternative: spread
8Math.max(...numbers); // 7

bind() - Create new function with fixed this

javascript
1const boundIntroduce = introduce.bind(person);
2boundIntroduce("Hey", "."); // "Hey, I'm Alice."
3
4// Partial application
5const sayHiToAlice = introduce.bind(person, "Hi");
6sayHiToAlice("!"); // "Hi, I'm Alice!"

The arguments Object

While rest parameters are preferred, understanding arguments is useful:

javascript
1function oldStyleSum() {
2 // arguments is array-like but NOT an array
3 console.log(arguments.length); // Works
4 console.log(arguments[0]); // Works
5 arguments.map(x => x); // Error! Not an array
6
7 // Convert to real array
8 const args = Array.from(arguments);
9 // or
10 const args2 = [...arguments];
11 // or (old way)
12 const args3 = Array.prototype.slice.call(arguments);
13
14 return args.reduce((a, b) => a + b, 0);
15}
16
17// Arrow functions don't have arguments
18const arrowSum = () => {
19 console.log(arguments); // ReferenceError (or parent's arguments)
20};

Recursion

Functions calling themselves:

javascript
1// Factorial: n! = n * (n-1) * (n-2) * ... * 1
2function factorial(n) {
3 if (n <= 1) return 1; // Base case
4 return n * factorial(n - 1); // Recursive case
5}
6
7factorial(5); // 120 (5 * 4 * 3 * 2 * 1)
8
9// Traversing nested structures
10function sumNested(arr) {
11 let total = 0;
12 for (const item of arr) {
13 if (Array.isArray(item)) {
14 total += sumNested(item); // Recurse
15 } else {
16 total += item;
17 }
18 }
19 return total;
20}
21
22sumNested([1, [2, [3, 4]], 5]); // 15

Tail Call Optimization (TCO)

A special optimization for recursive functions (limited browser support):

javascript
1// NOT tail-recursive (has to do multiplication after return)
2function factorial(n) {
3 if (n <= 1) return 1;
4 return n * factorial(n - 1);
5}
6
7// Tail-recursive (return value is just the recursive call)
8function factorialTCO(n, accumulator = 1) {
9 if (n <= 1) return accumulator;
10 return factorialTCO(n - 1, n * accumulator);
11}

Memoization

Caching function results for expensive computations:

javascript
1function memoize(fn) {
2 const cache = new Map();
3
4 return function(...args) {
5 const key = JSON.stringify(args);
6
7 if (cache.has(key)) {
8 console.log("Cache hit!");
9 return cache.get(key);
10 }
11
12 const result = fn.apply(this, args);
13 cache.set(key, result);
14 return result;
15 };
16}
17
18// Expensive function
19function fibonacci(n) {
20 if (n <= 1) return n;
21 return fibonacci(n - 1) + fibonacci(n - 2);
22}
23
24// Memoized version
25const memoFib = memoize(function fib(n) {
26 if (n <= 1) return n;
27 return memoFib(n - 1) + memoFib(n - 2);
28});
29
30memoFib(40); // Fast!
31fibonacci(40); // Very slow without memoization

Currying

Transforming a function that takes multiple arguments into a sequence of functions:

javascript
1// Non-curried
2function add(a, b, c) {
3 return a + b + c;
4}
5add(1, 2, 3); // 6
6
7// Curried
8function curriedAdd(a) {
9 return function(b) {
10 return function(c) {
11 return a + b + c;
12 };
13 };
14}
15curriedAdd(1)(2)(3); // 6
16
17// Arrow function curry
18const curriedAdd2 = a => b => c => a + b + c;
19
20// Partial application via currying
21const add1 = curriedAdd(1);
22const add1and2 = add1(2);
23add1and2(3); // 6

Auto-curry Helper

javascript
1function curry(fn) {
2 return function curried(...args) {
3 if (args.length >= fn.length) {
4 return fn.apply(this, args);
5 }
6 return function(...moreArgs) {
7 return curried.apply(this, args.concat(moreArgs));
8 };
9 };
10}
11
12const add = curry((a, b, c) => a + b + c);
13add(1, 2, 3); // 6
14add(1)(2)(3); // 6
15add(1, 2)(3); // 6
16add(1)(2, 3); // 6

Debouncing and Throttling

Control how often a function executes:

Debounce

Wait for a pause in calls before executing:

javascript
1function debounce(fn, delay) {
2 let timeoutId;
3
4 return function(...args) {
5 clearTimeout(timeoutId);
6
7 timeoutId = setTimeout(() => {
8 fn.apply(this, args);
9 }, delay);
10 };
11}
12
13// Use case: Search input
14const search = debounce((query) => {
15 console.log(`Searching for: ${query}`);
16}, 300);
17
18// Rapid typing only triggers one search after 300ms pause
19search("h");
20search("he");
21search("hel");
22search("hell");
23search("hello"); // Only this triggers the search

Throttle

Execute at most once per time period:

javascript
1function throttle(fn, limit) {
2 let inThrottle;
3
4 return function(...args) {
5 if (!inThrottle) {
6 fn.apply(this, args);
7 inThrottle = true;
8 setTimeout(() => inThrottle = false, limit);
9 }
10 };
11}
12
13// Use case: Scroll handler
14const onScroll = throttle(() => {
15 console.log("Scroll position:", window.scrollY);
16}, 100);
17
18window.addEventListener('scroll', onScroll);

Partial Application

Pre-fill some arguments of a function:

javascript
1function partial(fn, ...presetArgs) {
2 return function(...laterArgs) {
3 return fn(...presetArgs, ...laterArgs);
4 };
5}
6
7function greet(greeting, name, punctuation) {
8 return `${greeting}, ${name}${punctuation}`;
9}
10
11const sayHello = partial(greet, "Hello");
12sayHello("Alice", "!"); // "Hello, Alice!"
13
14const sayHelloToAlice = partial(greet, "Hello", "Alice");
15sayHelloToAlice("?"); // "Hello, Alice?"

Pure Functions

Functions without side effects that always return the same output for the same input:

javascript
1// PURE: No side effects, predictable
2function add(a, b) {
3 return a + b;
4}
5
6function sortArray(arr) {
7 return [...arr].sort(); // Doesn't modify original
8}
9
10// IMPURE: Has side effects
11let counter = 0;
12function increment() {
13 counter++; // Modifies external state
14 return counter;
15}
16
17function appendToArray(arr, item) {
18 arr.push(item); // Mutates input
19 return arr;
20}
21
22// IMPURE: Unpredictable output
23function getRandomValue() {
24 return Math.random();
25}

Benefits of Pure Functions

  1. Predictable: Same input = same output
  2. Testable: Easy to unit test
  3. Cacheable: Can be memoized
  4. Parallelizable: No shared state

Summary

PatternUse Case
bind/call/applyControl this explicitly
RecursionTree/nested data traversal
MemoizationCache expensive calculations
CurryingCreate specialized functions
DebounceWait for input to stop
ThrottleLimit execution rate
PartialPre-fill function arguments
Pure FunctionsPredictable, testable code

These patterns form the foundation of functional programming in JavaScript.