【原生JS教程】第18课:高阶函数与函数式编程

👽第18节:高阶函数与函数式编程

💻引言

在现代JavaScript开发中,函数式编程(Functional Programming)已经成为一种重要的编程范式。它强调使用纯函数、避免共享状态和可变数据,使代码更加可预测、易于测试和调试。高阶函数作为函数式编程的核心概念,为我们提供了强大的抽象能力,让我们能够写出更简洁、可读性更强的代码。

本节将深入探讨JavaScript中的高阶函数,包括mapfilterreduce等核心方法,以及柯里化、纯函数等概念,帮助你掌握函数式编程的基本思想和实践方法。

❓一、高阶函数基础概念

✅1.1 什么是高阶函数?

高阶函数(Higher-Order Function)是指满足以下条件之一的函数:

  1. 接受一个或多个函数作为参数
  2. 返回一个函数作为结果
// ✅示例1:接受函数作为参数
function calculate(operation, a, b) {
    return operation(a, b);
}

function add(x, y) {
    return x + y;
}

function multiply(x, y) {
    return x * y;
}

console.log(calculate(add, 5, 3)); // 8
console.log(calculate(multiply, 5, 3)); // 15

// ✅示例2:返回函数作为结果
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

🌐高阶函数的核心思想是将函数作为"一等公民",可以像其他数据类型一样被传递、赋值和返回。

❗二、核心高阶函数详解

✅2.1 map函数 - 数据转换

map函数用于将数组中的每个元素通过一个转换函数处理,返回一个新数组。

// ✅基础用法
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(x => x * 2);
console.log(doubled); // [2, 4, 6, 8, 10]

// ✅实际应用示例
const users = [
    { name: 'Alice', age: 25 },
    { name: 'Bob', age: 30 },
    { name: 'Charlie', age: 35 }
];

// ✅提取所有用户名
const names = users.map(user => user.name);
console.log(names); // ['Alice', 'Bob', 'Charlie']

// ✅转换数据结构
const userAges = users.map(user => ({
    name: user.name,
    isAdult: user.age >= 18
}));
console.log(userAges);
// [
//   { name: 'Alice', isAdult: true },
//   { name: 'Bob', isAdult: true },
//   { name: 'Charlie', isAdult: true }
// ]

🌐map的核心特点:

  • 不修改原数组
  • 新数组长度与原数组相同
  • 每个元素都经过转换函数处理

✅2.2 filter函数 - 数据筛选

filter函数用于筛选数组中满足条件的元素,返回一个新数组。

// ✅基础用法
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const evenNumbers = numbers.filter(x => x % 2 === 0);
console.log(evenNumbers); // [2, 4, 6, 8, 10]

// ✅实际应用示例
const products = [
    { name: 'iPhone', price: 999, category: 'electronics' },
    { name: 'T-shirt', price: 20, category: 'clothing' },
    { name: 'MacBook', price: 1500, category: 'electronics' },
    { name: 'Jeans', price: 50, category: 'clothing' }
];

// ✅筛选电子产品
const electronics = products.filter(product => product.category === 'electronics');
console.log(electronics);

// ✅筛选价格大于100的商品
const expensiveProducts = products.filter(product => product.price > 100);
console.log(expensiveProducts);

🌐filter的核心特点:

  • 不修改原数组
  • 新数组长度可能小于等于原数组
  • 返回满足条件的元素

✅2.3 reduce函数 - 数据聚合

reduce函数用于将数组中的元素通过累积器函数聚合为单个值。

// ✅基础用法:求和
const numbers = [1, 2, 3, 4, 5];
const sum = numbers.reduce((accumulator, currentValue) => accumulator + currentValue, 0);
console.log(sum); // 15

// ✅详细解释reduce的执行过程
const result = numbers.reduce((acc, cur, index, array) => {
    console.log(`${index}次迭代: acc=${acc}, cur=${cur}`);
    return acc + cur;
}, 0);
// 输出:
// 第0次迭代: acc=0, cur=1
// 第1次迭代: acc=1, cur=2
// 第2次迭代: acc=3, cur=3
// 第3次迭代: acc=6, cur=4
// 第4次迭代: acc=10, cur=5

// ✅实际应用示例
const orders = [
    { id: 1, amount: 100 },
    { id: 2, amount: 200 },
    { id: 3, amount: 150 }
];

// 计算总金额
const totalAmount = orders.reduce((sum, order) => sum + order.amount, 0);
console.log(totalAmount); // 450

// ✅统计每个类别的商品数量
const items = [
    { name: 'iPhone', category: 'electronics' },
    { name: 'MacBook', category: 'electronics' },
    { name: 'T-shirt', category: 'clothing' },
    { name: 'Jeans', category: 'clothing' }
];

const categoryCount = items.reduce((count, item) => {
    count[item.category] = (count[item.category] || 0) + 1;
    return count;
}, {});
console.log(categoryCount); // { electronics: 2, clothing: 2 }

🌐reduce的核心特点:

  • 可以将数组聚合为任意类型的值
  • 提供初始值参数
  • 通过累积器函数逐步处理每个元素

🚀三、函数式编程核心概念

✅3.1 纯函数(Pure Functions)

纯函数是指满足以下条件的函数:

  1. 相同输入总是产生相同输出
  2. 没有副作用(不修改外部状态)
// 纯函数示例
function add(a, b) {
    return a + b;
}

function calculateArea(width, height) {
    return width * height;
}

// 非纯函数示例
let total = 0;
function addToTotal(value) {
    total += value; // 修改了外部状态
    return total;
}

function getCurrentTime() {
    return new Date(); // 相同输入产生不同输出
}

// 纯函数的优势
// 1. 可预测性
console.log(add(2, 3)); // 总是返回5
console.log(add(2, 3)); // 总是返回5

// 2. 可缓存性
const memoize = (fn) => {
    const cache = {};
    return (...args) => {
        const key = JSON.stringify(args);
        if (cache[key]) {
            console.log('从缓存获取结果');
            return cache[key];
        }
        console.log('计算结果');
        const result = fn(...args);
        cache[key] = result;
        return result;
    };
};

const pureAdd = (a, b) => a + b;
const memoizedAdd = memoize(pureAdd);

console.log(memoizedAdd(2, 3)); // 计算结果: 5
console.log(memoizedAdd(2, 3)); // 从缓存获取结果: 5

✅3.2 柯里化(Currying)

柯里化是一种将接受多个参数的函数转换为一系列接受单一参数的函数的技术。

// 普通函数
function multiply(a, b, c) {
    return a * b * c;
}

console.log(multiply(2, 3, 4)); // 24

// 柯里化版本
function curryMultiply(a) {
    return function(b) {
        return function(c) {
            return a * b * c;
        };
    };
}

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

// 使用箭头函数简化
const curryMultiplyArrow = a => b => c => a * b * c;
console.log(curryMultiplyArrow(2)(3)(4)); // 24

// 通用柯里化函数
function curry(fn) {
    return function curried(...args) {
        if (args.length >= fn.length) {
            return fn.apply(this, args);
        }
        return function(...nextArgs) {
            return curried.apply(this, args.concat(nextArgs));
        };
    };
}

// 使用示例
function addThree(a, b, c) {
    return a + b + c;
}

const curriedAdd = curry(addThree);
console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
console.log(curriedAdd(1)(2, 3)); // 6
console.log(curriedAdd(1, 2, 3)); // 6

// 实际应用:创建专用函数
const multiplyBy = curry((multiplier, number) => multiplier * number);
const double = multiplyBy(2);
const triple = multiplyBy(3);

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

✅3.3 函数组合(Function Composition)

函数组合是将多个函数组合成一个新函数的技术。

// 基础组合函数
const compose = (f, g) => (x) => f(g(x));

// 示例函数
const addOne = x => x + 1;
const multiplyByTwo = x => x * 2;
const square = x => x * x;

// 组合函数
const addOneThenSquare = compose(square, addOne);
console.log(addOneThenSquare(3)); // 16 ((3+1)^2)

// 多函数组合
const composeMultiple = (...fns) => (value) => 
    fns.reduceRight((acc, fn) => fn(acc), value);

const complexOperation = composeMultiple(
    square,
    multiplyByTwo,
    addOne
);

console.log(complexOperation(3)); // 64 (((3+1)*2)^2)

// 实际应用示例:数据处理管道
const users = [
    { name: 'alice', email: 'ALICE@EXAMPLE.COM' },
    { name: 'bob', email: 'BOB@EXAMPLE.COM' },
    { name: 'charlie', email: 'CHARLIE@EXAMPLE.COM' }
];

// 处理函数
const toLowerCase = str => str.toLowerCase();
const capitalize = str => str.charAt(0).toUpperCase() + str.slice(1);
const formatName = str => capitalize(toLowerCase(str));
const getEmailDomain = email => email.split('@')[1];

// 组合处理
const processUser = user => ({
    ...user,
    name: formatName(user.name),
    email: toLowerCase(user.email),
    domain: getEmailDomain(user.email)
});

const processedUsers = users.map(processUser);
console.log(processedUsers);

🌋四、链式调用与函数式编程

// 使用链式调用实现函数式数据处理
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

const result = numbers
    .filter(x => x % 2 === 0)      // 筛选偶数
    .map(x => x * x)               // 平方
    .reduce((sum, x) => sum + x, 0); // 求和

console.log(result); // 220 (2² + 4² + 6² + 8² + 10² = 4 + 16 + 36 + 64 + 100)

// 复杂数据处理示例
const products = [
    { name: 'iPhone', price: 999, category: 'electronics', inStock: true },
    { name: 'T-shirt', price: 20, category: 'clothing', inStock: false },
    { name: 'MacBook', price: 1500, category: 'electronics', inStock: true },
    { name: 'Jeans', price: 50, category: 'clothing', inStock: true },
    { name: 'iPad', price: 300, category: 'electronics', inStock: false }
];

const expensiveElectronicsInStock = products
    .filter(product => product.category === 'electronics')
    .filter(product => product.inStock)
    .filter(product => product.price > 500)
    .map(product => ({
        name: product.name,
        displayPrice: `$${product.price}`,
        discountPrice: product.price * 0.9
    }))
    .sort((a, b) => b.discountPrice - a.discountPrice);

console.log(expensiveElectronicsInStock);

🔨五、重难点分析

✅5.1 理解reduce函数的执行机制

// reduce的详细执行过程演示
const numbers = [1, 2, 3, 4];

// 带日志的reduce执行
const result = numbers.reduce((acc, cur, index, array) => {
    console.log(`步骤${index + 1}:`);
    console.log(`  累积器(acc): ${acc}`);
    console.log(`  当前值(cur): ${cur}`);
    console.log(`  返回值: ${acc + cur}`);
    console.log('---');
    return acc + cur;
}, 0);

console.log(`最终结果: ${result}`);

// 不同初始值的影响
console.log('初始值为0:', numbers.reduce((a, b) => a + b, 0)); // 10
console.log('初始值为100:', numbers.reduce((a, b) => a + b, 100)); // 110
console.log('无初始值:', numbers.reduce((a, b) => a + b)); // 10

// 处理空数组的情况
const emptyArray = [];
try {
    console.log(emptyArray.reduce((a, b) => a + b)); // 报错
} catch (e) {
    console.log('空数组无初始值会报错');
}
console.log(emptyArray.reduce((a, b) => a + b, 0)); // 0

✅5.2 柯里化与部分应用的区别

// 柯里化:每次只接受一个参数
function curryAdd(a) {
    return function(b) {
        return function(c) {
            return a + b + c;
        };
    };
}

console.log(curryAdd(1)(2)(3)); // 6

// 部分应用:可以接受多个参数
function partialAdd(a, b, c) {
    return a + b + c;
}

function partial(fn, ...args) {
    return function(...restArgs) {
        return fn(...args, ...restArgs);
    };
}

const addWithFirst = partial(partialAdd, 1);
const addWithFirstTwo = partial(partialAdd, 1, 2);

console.log(addWithFirst(2, 3)); // 6
console.log(addWithFirstTwo(3)); // 6

✅5.3 性能考虑

// 链式调用 vs 循环优化
const data = Array.from({length: 1000000}, (_, i) => i);

// 链式调用(多次遍历)
console.time('链式调用');
const result1 = data
    .filter(x => x % 2 === 0)
    .map(x => x * 2)
    .reduce((sum, x) => sum + x, 0);
console.timeEnd('链式调用');

// 单次循环优化
console.time('单次循环');
let sum = 0;
for (let i = 0; i < data.length; i++) {
    if (data[i] % 2 === 0) {
        sum += data[i] * 2;
    }
}
console.timeEnd('单次循环');

// 函数式编程的性能优化:延迟计算
function* filterGenerator(predicate, iterable) {
    for (const item of iterable) {
        if (predicate(item)) {
            yield item;
        }
    }
}

function* mapGenerator(mapper, iterable) {
    for (const item of iterable) {
        yield mapper(item);
    }
}

// 使用生成器实现惰性求值
const lazyResult = mapGenerator(
    x => x * 2,
    filterGenerator(
        x => x % 2 === 0,
        data
    )
);

// 只取前10个结果
const firstTen = [];
let count = 0;
for (const item of lazyResult) {
    if (count >= 10) break;
    firstTen.push(item);
    count++;
}
console.log(firstTen); // [0, 4, 8, 12, 16, 20, 24, 28, 32, 36]

⚡六、常见应用场景

✅6.1 数据处理管道

// 构建通用的数据处理管道
class Pipeline {
    constructor(value) {
        this.value = value;
    }
    
    pipe(fn) {
        this.value = fn(this.value);
        return this;
    }
    
    result() {
        return this.value;
    }
}

// 使用示例
const processText = (text) => 
    new Pipeline(text)
        .pipe(str => str.toLowerCase())
        .pipe(str => str.replace(/\s+/g, ' '))
        .pipe(str => str.trim())
        .pipe(str => str.charAt(0).toUpperCase() + str.slice(1))
        .result();

console.log(processText('  HELLO    WORLD  ')); // "Hello world"

✅6.2 事件处理

// 函数式事件处理
const createEventHandler = (...handlers) => (event) => {
    handlers.forEach(handler => handler(event));
};

const logEvent = event => console.log('Event:', event.type);
const preventDefault = event => event.preventDefault();
const stopPropagation = event => event.stopPropagation();

const handleClick = createEventHandler(
    logEvent,
    stopPropagation
);

// document.addEventListener('click', handleClick);

📚七、总结

函数式编程和高阶函数为我们提供了强大的抽象能力,让代码更加简洁、可读和可维护。通过掌握mapfilterreduce等核心函数,以及纯函数、柯里化等概念,我们可以写出更加优雅的JavaScript代码。

关键要点:

  1. 高阶函数是接受函数作为参数或返回函数的函数
  2. map用于转换数据,filter用于筛选数据,reduce用于聚合数据
  3. 纯函数具有可预测性和可缓存性
  4. 柯里化可以创建更灵活的函数
  5. 函数组合让我们能够构建复杂的数据处理管道

🌊八、高频面试题

✅1. 什么是纯函数?请举例说明纯函数和非纯函数的区别。

答案:
纯函数是指满足以下两个条件的函数:

  1. 相同输入总是产生相同输出
  2. 没有副作用(不修改外部状态,不进行I/O操作等)
// 纯函数示例
function add(a, b) {
    return a + b;
}

function calculateArea(width, height) {
    return width * height;
}

// 非纯函数示例
let total = 0;
function addToTotal(value) {
    total += value; // 修改了外部状态 - 副作用
    return total;
}

function getCurrentTime() {
    return new Date(); // 依赖外部状态 - 副作用
}

✅2. 实现一个通用的curry函数。

function curry(fn) {
    return function curried(...args) {
        // 如果参数足够,执行原函数
        if (args.length >= fn.length) {
            return fn.apply(this, args);
        }
        // 否则返回一个新函数等待更多参数
        return function(...nextArgs) {
            return curried.apply(this, args.concat(nextArgs));
        };
    };
}

// 测试
function add(a, b, c) {
    return a + b + c;
}

const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
console.log(curriedAdd(1)(2, 3)); // 6

✅3. 解释reduce函数的参数和执行过程。

答案:
reduce函数接受两个参数:

  1. 累积器函数(reducer):(accumulator, currentValue, currentIndex, array) => newValue
  2. 初始值(可选)

执行过程:

  • 如果提供初始值,第一次调用时accumulator为初始值,currentValue为数组第一个元素
  • 如果不提供初始值,第一次调用时accumulator为数组第一个元素,currentValue为第二个元素
const numbers = [1, 2, 3, 4];
const sum = numbers.reduce((acc, cur, index, array) => {
    console.log(`acc: ${acc}, cur: ${cur}, index: ${index}`);
    return acc + cur;
}, 0);

✅4. 如何用reduce实现map和filter功能?

// 用reduce实现map
Array.prototype.myMap = function(callback) {
    return this.reduce((acc, cur, index, array) => {
        acc.push(callback(cur, index, array));
        return acc;
    }, []);
};

// 用reduce实现filter
Array.prototype.myFilter = function(callback) {
    return this.reduce((acc, cur, index, array) => {
        if (callback(cur, index, array)) {
            acc.push(cur);
        }
        return acc;
    }, []);
};

// 测试
const numbers = [1, 2, 3, 4, 5];
console.log(numbers.myMap(x => x * 2)); // [2, 4, 6, 8, 10]
console.log(numbers.myFilter(x => x % 2 === 0)); // [2, 4]

✅5. 什么是函数组合?实现一个compose函数。

// 从右到左执行
function compose(...fns) {
    return function(value) {
        return fns.reduceRight((acc, fn) => fn(acc), value);
    };
}

// 从左到右执行
function pipe(...fns) {
    return function(value) {
        return fns.reduce((acc, fn) => fn(acc), value);
    };
}

// 测试
const addOne = x => x + 1;
const multiplyByTwo = x => x * 2;
const square = x => x * x;

const composed = compose(square, multiplyByTwo, addOne);
console.log(composed(3)); // ((3+1)*2)^2 = 64

const piped = pipe(addOne, multiplyByTwo, square);
console.log(piped(3)); // ((3+1)*2)^2 = 64

✅6. map、filter、reduce的时间复杂度是多少?

答案:
这三个函数的时间复杂度都是O(n),因为它们都需要遍历整个数组。但需要注意的是:

  1. 链式调用会导致多次遍历,如arr.map().filter().map()会遍历数组3次
  2. 在性能敏感的场景下,可以考虑使用单次循环或惰性求值

✅7. 如何实现防抖(debounce)和节流(throttle)高阶函数?

// 防抖函数
function debounce(func, delay) {
    let timeoutId;
    return function(...args) {
        clearTimeout(timeoutId);
        timeoutId = setTimeout(() => func.apply(this, args), delay);
    };
}

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

// 使用示例
const debouncedSearch = debounce(function(query) {
    console.log('搜索:', query);
}, 300);

const throttledScroll = throttle(function() {
    console.log('滚动事件');
}, 100);

✅8. 什么是偏函数应用(Partial Application)?与柯里化有什么区别?

答案:
偏函数应用是固定一个函数的部分参数,返回一个接受剩余参数的新函数。

区别:

  • 柯里化:将多参数函数转换为一系列单参数函数
  • 偏函数应用:固定部分参数,返回接受剩余参数的函数
// 偏函数应用实现
function partial(fn, ...args) {
    return function(...restArgs) {
        return fn(...args, ...restArgs);
    };
}

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

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

// 柯里化版本
const curryMultiply = a => b => c => a * b * c;
const curryMultiplyByTwo = curryMultiply(2);
console.log(curryMultiplyByTwo(3)(4)); // 24

✅9. 如何用函数式编程思想处理异步操作?

// Promise链式调用
function fetchUserData(userId) {
    return fetch(`/api/users/${userId}`)
        .then(response => response.json())
        .then(user => ({
            ...user,
            fullName: `${user.firstName} ${user.lastName}`
        }))
        .catch(error => {
            console.error('获取用户数据失败:', error);
            return null;
        });
}

// 使用async/await
async function processUsers(userIds) {
    const users = await Promise.all(
        userIds.map(id => fetchUserData(id))
    );
    
    return users
        .filter(user => user !== null)
        .map(user => user.fullName);
}

// 函数式错误处理
const safeParseJSON = (str) => {
    try {
        return { success: true, data: JSON.parse(str) };
    } catch (error) {
        return { success: false, error: error.message };
    }
};

const processData = (data) => 
    safeParseJSON(data)
        .filter(result => result.success)
        .map(result => result.data)
        .map(parsedData => ({
            ...parsedData,
            processed: true
        }));

✅10. 实现一个memoize高阶函数,并说明其应用场景。

function memoize(fn) {
    const cache = new Map();
    
    return function(...args) {
        const key = JSON.stringify(args);
        
        if (cache.has(key)) {
            console.log('从缓存返回结果');
            return cache.get(key);
        }
        
        console.log('计算新结果');
        const result = fn.apply(this, args);
        cache.set(key, result);
        return result;
    };
}

// 应用场景1:计算密集型函数
const fibonacci = memoize(function(n) {
    if (n <= 1) return n;
    return fibonacci(n - 1) + fibonacci(n - 2);
});

console.log(fibonacci(10)); // 计算新结果
console.log(fibonacci(10)); // 从缓存返回结果

// 应用场景2:API请求缓存
const fetchWithCache = memoize(async function(url) {
    const response = await fetch(url);
    return response.json();
});

// 应用场景3:复杂对象处理
const processExpensiveData = memoize(function(data) {
    // 复杂的数据处理逻辑
    return data.map(item => 
        item.value * Math.pow(item.factor, 2)
    ).filter(value => value > 100);
});

🤖这些面试题涵盖了高阶函数和函数式编程的核心概念,掌握了这些内容,就能在实际开发中灵活运用函数式编程的思想来解决问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

全栈前端老曹

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值