Learning Objectives

  • Understand how closures work with array methods
  • Create reusable callback factories
  • Build custom array utilities using closures
  • Apply functional programming patterns

Closures in Array Methods

Array methods like map, filter, and reduce take callback functions that naturally create closures, allowing them to access variables from their surrounding scope.

const multiplier = 3;

const numbers = [1, 2, 3, 4, 5];

// Callback has access to multiplier
const tripled = numbers.map(n => n * multiplier);
console.log(tripled); // [3, 6, 9, 12, 15]

Creating Configurable Callbacks

Use closures to create reusable, configurable callbacks:

// Callback factories
const multiplyBy = factor => n => n * factor;
const addTo = amount => n => n + amount;
const greaterThan = threshold => n => n > threshold;
const lessThan = threshold => n => n < threshold;

const numbers = [1, 2, 3, 4, 5];

console.log(numbers.map(multiplyBy(2)));     // [2, 4, 6, 8, 10]
console.log(numbers.map(addTo(10)));         // [11, 12, 13, 14, 15]
console.log(numbers.filter(greaterThan(3))); // [4, 5]
console.log(numbers.filter(lessThan(4)));    // [1, 2, 3]

Practical Example: Data Transformation

const users = [
    { name: 'Alice', age: 25, active: true },
    { name: 'Bob', age: 17, active: false },
    { name: 'Charlie', age: 30, active: true },
    { name: 'David', age: 16, active: true }
];

// Create reusable filters
const isActive = user => user.active;
const isAdult = user => user.age >= 18;
const hasName = name => user => user.name === name;

// Create reusable mappers
const getName = user => user.name;
const getAge = user => user.age;
const addProperty = (key, value) => obj => ({ ...obj, [key]: value });

// Use them
const activeAdults = users
    .filter(isActive)
    .filter(isAdult)
    .map(getName);

console.log(activeAdults); // ['Alice', 'Charlie']

// Add property to all users
const usersWithRole = users.map(addProperty('role', 'user'));
console.log(usersWithRole);

Practical Example: Accumulator with Closure

function createAccumulator(operation) {
    return function(array, initialValue) {
        return array.reduce((acc, item) => {
            return operation(acc, item);
        }, initialValue);
    };
}

// Create specialized accumulators
const sum = createAccumulator((acc, n) => acc + n);
const product = createAccumulator((acc, n) => acc * n);
const concat = createAccumulator((acc, str) => acc + str);
const merge = createAccumulator((acc, obj) => ({ ...acc, ...obj }));

const numbers = [1, 2, 3, 4, 5];
console.log(sum(numbers, 0));      // 15
console.log(product(numbers, 1));  // 120

const strings = ['Hello', ' ', 'World'];
console.log(concat(strings, ''));  // "Hello World"

const objects = [{ a: 1 }, { b: 2 }, { c: 3 }];
console.log(merge(objects, {}));   // { a: 1, b: 2, c: 3 }

Practical Example: Grouping and Counting

function createGrouper(keyFn) {
    return function(array) {
        return array.reduce((groups, item) => {
            const key = keyFn(item);
            if (!groups[key]) {
                groups[key] = [];
            }
            groups[key].push(item);
            return groups;
        }, {});
    };
}

function createCounter(keyFn) {
    return function(array) {
        return array.reduce((counts, item) => {
            const key = keyFn(item);
            counts[key] = (counts[key] || 0) + 1;
            return counts;
        }, {});
    };
}

const users = [
    { name: 'Alice', age: 25, country: 'USA' },
    { name: 'Bob', age: 25, country: 'UK' },
    { name: 'Charlie', age: 30, country: 'USA' },
    { name: 'David', age: 25, country: 'Canada' }
];

const groupByAge = createGrouper(user => user.age);
const groupByCountry = createGrouper(user => user.country);
const countByAge = createCounter(user => user.age);

console.log(groupByAge(users));
console.log(groupByCountry(users));
console.log(countByAge(users)); // { '25': 3, '30': 1 }

Practical Example: Chaining with Closures

function createPipeline() {
    const operations = [];
    
    return {
        map: function(fn) {
            operations.push(arr => arr.map(fn));
            return this;
        },
        
        filter: function(fn) {
            operations.push(arr => arr.filter(fn));
            return this;
        },
        
        reduce: function(fn, initial) {
            operations.push(arr => arr.reduce(fn, initial));
            return this;
        },
        
        execute: function(array) {
            return operations.reduce((result, operation) => {
                return operation(result);
            }, array);
        }
    };
}

const pipeline = createPipeline()
    .filter(n => n > 0)
    .map(n => n * 2)
    .filter(n => n < 20)
    .reduce((sum, n) => sum + n, 0);

console.log(pipeline.execute([1, -2, 3, 4, 5, 10, 15])); // 24

Practical Example: Memoized Array Operations

function memoizeArrayOp(fn) {
    const cache = new Map();
    
    return function(array, ...args) {
        const key = JSON.stringify([array, ...args]);
        
        if (cache.has(key)) {
            console.log('Cache hit');
            return cache.get(key);
        }
        
        console.log('Computing...');
        const result = fn(array, ...args);
        cache.set(key, result);
        return result;
    };
}

const expensiveFilter = memoizeArrayOp((array, threshold) => {
    return array.filter(n => {
        // Simulate expensive operation
        for (let i = 0; i < 1000000; i++) {}
        return n > threshold;
    });
});

const numbers = [1, 2, 3, 4, 5];
expensiveFilter(numbers, 3); // Computing...
expensiveFilter(numbers, 3); // Cache hit

Practical Example: Conditional Array Processing

function createConditionalProcessor(condition) {
    return {
        map: function(fn) {
            return array => array.map(item => 
                condition(item) ? fn(item) : item
            );
        },
        
        filter: function(fn) {
            return array => array.filter(item =>
                condition(item) ? fn(item) : true
            );
        }
    };
}

const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// Only process even numbers
const evenProcessor = createConditionalProcessor(n => n % 2 === 0);

const doubled = evenProcessor.map(n => n * 2)(numbers);
console.log(doubled); // [1, 4, 3, 8, 5, 12, 7, 16, 9, 20]

const filtered = evenProcessor.filter(n => n > 4)(numbers);
console.log(filtered); // [1, 2, 3, 4, 6, 8, 10]

Practical Example: Array Validator

function createValidator(rules) {
    return function(array) {
        const errors = [];
        
        array.forEach((item, index) => {
            rules.forEach(rule => {
                if (!rule.test(item)) {
                    errors.push({
                        index,
                        item,
                        message: rule.message
                    });
                }
            });
        });
        
        return {
            isValid: errors.length === 0,
            errors
        };
    };
}

const validateNumbers = createValidator([
    {
        test: n => typeof n === 'number',
        message: 'Must be a number'
    },
    {
        test: n => n >= 0,
        message: 'Must be non-negative'
    },
    {
        test: n => n <= 100,
        message: 'Must be <= 100'
    }
]);

const data = [10, -5, 150, 'abc', 50];
const result = validateNumbers(data);
console.log(result);
// { isValid: false, errors: [...] }

Practical Example: Array Transformer Factory

function createTransformer(config) {
    const { mappers = [], filters = [], sorters = [] } = config;
    
    return function(array) {
        let result = [...array];
        
        // Apply filters
        filters.forEach(filter => {
            result = result.filter(filter);
        });
        
        // Apply mappers
        mappers.forEach(mapper => {
            result = result.map(mapper);
        });
        
        // Apply sorters
        sorters.forEach(sorter => {
            result = result.sort(sorter);
        });
        
        return result;
    };
}

const userTransformer = createTransformer({
    filters: [
        user => user.active,
        user => user.age >= 18
    ],
    mappers: [
        user => ({ ...user, fullName: `${user.firstName} ${user.lastName}` }),
        user => ({ ...user, ageGroup: user.age < 30 ? 'young' : 'mature' })
    ],
    sorters: [
        (a, b) => a.age - b.age
    ]
});

const users = [
    { firstName: 'Alice', lastName: 'Smith', age: 25, active: true },
    { firstName: 'Bob', lastName: 'Jones', age: 17, active: true },
    { firstName: 'Charlie', lastName: 'Brown', age: 35, active: true }
];

console.log(userTransformer(users));

Key Takeaways

  • ✅ Array method callbacks naturally create closures
  • ✅ Use closures to create configurable callbacks
  • ✅ Build reusable transformation utilities
  • ✅ Closures enable memoization and caching
  • ✅ Combine closures with array methods for powerful data processing

Next Steps

Now that you understand closures with array methods, in the next lesson we'll explore memoization and caching - powerful optimization techniques enabled by closures.