avatar
函数式编程
😆

函数式编程

keywords
published_date
最新编辑 2024年10月09日
Created time
2023年03月16日
slug
函数式编程强调不可变性、函数作为一等公民、无副作用和高阶函数等核心特点。通过示例展示了如何使用高阶函数和纯函数来简化代码,提升可读性和可维护性。柯里化和组合函数的概念被详细介绍,强调了它们的灵活性和复用性。最后,介绍了 JavaScript 中的 reduce() 方法及其用法,展示了如何通过高阶函数处理数组数据。
tags
JavaScript

函数式编程

函数是可以接受并且返回任何类型的值。一个函数如果可以接受或返回一个甚至多个函数,它被叫做高阶函数。

1.核心特点

  1. 不可变性:函数式编程强调数据不可变性,一旦定义了一个值,就不能再改变它的值。这样可以避免因为副作用带来的不可预测性。
  1. 函数作为一等公民:在函数式编程中,函数被视为一等公民,就像其他数据类型一样,可以赋值给变量、作为函数的参数或返回值
  1. 无副作用:函数式编程强调无副作用,即函数的执行不会对外部状态造成任何影响,只通过输入和输出来处理数据。
  1. 高阶函数:函数式编程可以使用高阶函数,即接受函数作为参数或返回函数的函数,来实现抽象和复用
  1. 延迟计算:函数式编程通常采用延迟计算,也就是只在需要的时候才计算表达式的值,这可以减少不必要的计算。
  1. 递归:函数式编程经常使用递归来处理数据结构,例如列表和树。
 
例子:
函数式编程
// 定义一个函数式编程的解决方案 function doubleEvenNumbers(numbers) { return numbers .filter(function(num) { return num % 2 === 0; // 保留偶数 }) .map(function(num) { return num * 2; // 将偶数加倍 }); } // 调用函数并输出结果 var numbers = [1, 2, 3, 4, 5, 6]; var doubledEvens = doubleEvenNumbers(numbers); console.log(doubledEvens); // [4, 8, 12]
上面的代码中,我们定义了一个函数 doubleEvenNumbers,它使用了函数式编程中的 filter()map() 方法来对输入的数字数组进行处理。filter() 方法会过滤出数组中的偶数,而 map() 方法则将这些偶数加倍。最终,该函数返回了一个新的数组,其中包含了加倍后的偶数。
通过这个例子,我们可以看到使用函数式编程的方式,可以让代码更加简洁、可读性更好,也更容易进行并行处理。而且,这种方式对于一些数据处理的场景也很有效。
命令式编程
// 定义一个命令式编程的解决方案 function doubleEvenNumbers(numbers) { var doubledEvens = []; for (var i = 0; i < numbers.length; i++) { if (numbers[i] % 2 === 0) { // 保留偶数 doubledEvens.push(numbers[i] * 2); // 将偶数加倍并添加到结果数组中 } } return doubledEvens; } // 调用函数并输出结果 var numbers = [1, 2, 3, 4, 5, 6]; var doubledEvens = doubleEvenNumbers(numbers); console.log(doubledEvens); // [4, 8, 12]
上面的代码中,我们使用了一个 for 循环来遍历输入的数字数组,并判断每个数字是否是偶数。如果是偶数,则将其加倍并添加到结果数组中。最终,该函数返回了一个新的数组,其中包含了加倍后的偶数。
相比较函数式编程的方式,命令式编程的代码通常会更加复杂和冗长,也更难以理解和维护。而且,命令式编程方式往往会有副作用,可能会导致代码难以调试和测试。因此,在适合使用函数式编程的场景下,尽量使用函数式编程的方式可以让代码更加简洁、可读性更好、维护性更强。
 

2.纯函数

在函数式编程中,纯函数(Pure Function)是指一个函数具有以下两个特性:
  1. 输入参数决定函数返回值,相同的输入参数始终返回相同的输出结果。
  1. 函数在执行过程中没有副作用,也就是说不会对系统环境造成任何影响,如修改输入参数、改变全局变量、文件读写、网络请求等操作
 
纯函数对于函数式编程非常重要,因为它们保证了代码的可靠性和可测试性。由于纯函数的输出结果只取决于输入参数,因此在使用相同的输入参数时,函数的返回值总是一致的,不会受到外部环境的影响。这也使得纯函数的行为更加可预测,更容易进行测试和调试。
另外,由于纯函数没有副作用,因此它们也更容易进行代码重用和并行处理。多个纯函数可以并行执行,而不需要担心竞态条件等问题,因为它们之间不会互相影响。
 
例子:
function multiply(a, b) { return a * b; }
这个函数符合纯函数的定义,因为它的返回值只取决于输入参数 ab,没有任何副作用。而且,无论何时使用相同的输入参数调用该函数,它总会返回相同的结果。
 

3.柯里化

柯里化(Currying)是一种函数式编程技术,它将接收多个参数的函数转化为接收一个单一参数的函数序列
换句话说,柯里化是指将接收多个参数的函数转化为一系列只接收一个参数的函数的过程。
例如,一个接收三个参数的函数可以被柯里化为三个只接收一个参数的函数,第一个函数接收第一个参数并返回一个新的函数,第二个函数接收第二个参数并返回一个新的函数,第三个函数接收第三个参数并返回最终结果。这样做的好处是可以让函数更加灵活、可复用、易于组合。
 
例子:
function add(a, b) { return a + b; } // 将 add 函数转换成柯里化函数 function curriedAdd(a) { return function(b) { return a + b; } } const sum = curriedAdd(1)(2); // 3
在上面的例子中,我们首先定义了一个接收两个参数 ab 的函数 add,然后我们定义了一个将 add 函数转换为柯里化函数的函数 curriedAdd,该函数接收一个参数 a,并返回一个新的函数,该新函数接收一个参数 b,并返回 a + b 的结果。
在调用柯里化后的函数时,我们可以像上面的例子一样,使用多个括号依次传递每个参数,每个括号代表一个嵌套函数的调用。
 

优点

  1. 灵活性:柯里化函数能够接收单个参数,并且返回新的函数,这使得函数更加灵活和易于组合。
  1. 代码复用:柯里化函数能够创建一个新的函数,这使得我们可以将函数的一部分封装起来并重用它们。
  1. 可读性:柯里化函数能够更好地表达函数的含义,使代码更加易于理解和维护
 

实现通用柯里化

通用的柯里化函数 curry的作用就是将任意函数进行柯里化。它接收一个函数 fn作为参数,返回一个新的函数 curried。这个新函数 curried可以接收任意数量的参数,并根据参数的个数来决定是否需要继续柯里化或直接调用原始函数。具体实现如下:
const curry = (fn) => { // 定义 curry 函数,接收一个函数 fn 作为参数 const arity = fn.length; // 获取 fn 的参数个数,即 fn 的元数 return function curried(...args) { // 定义新函数 curried,它可以接收任意数量的参数 if (args.length < arity) { // 如果当前接收的参数个数小于 fn 的元数 return function(...rest) { // 则返回一个新函数,接收剩余的参数 rest return curried(...args.concat(rest)); // 递归调用 curried,并将之前的参数 args 与当前的参数 rest 合并 }; } else { // 如果当前接收的参数个数等于 fn 的元数 return fn.apply(null, args); // 则直接调用 fn 并传入所有参数 args } }; };
这个实现中有两个关键点:
  1. 使用 fn.length 获取函数 fn 的参数个数,即函数的元数。这个参数个数可以帮助我们判断柯里化的过程何时结束,需要一次性调用原始函数并返回结果。
  1. 返回一个新的函数 curried,该函数可以接收任意数量的参数,并根据参数个数判断是否需要继续柯里化或直接调用原始函数。
    1. 如果当前接收的参数个数小于 arity,则说明还需要继续柯里化,我们返回一个新的函数,该函数接收剩余的参数 rest 并将其与之前的参数 args 合并,然后递归调用 curried 函数。
      如果参数个数等于 arity,则说明已经接收了所有参数,我们直接调用原始函数 fn 并传入所有参数 args
 
以下是几道关于柯里化的面试题:
  1. 实现一个通用的柯里化函数 curry,它接收一个函数作为参数,返回一个新的函数,该新函数可以对原始函数进行柯里化。
  1. 实现一个 add 函数的柯里化版本 addCurry,使得该函数可以支持以下用法:
addCurry(1)(2)(3)(); // 6 addCurry(1)(2)(3)(4)(); // 10 addCurry(1, 2, 3)(); // 6 addCurry(1)(2, 3)(4)(); // 10
答案
一种用法 addCurry(1)(2)(3)() 表示先传入 1,然后传入 2,再传入 3,最终返回的函数没有参数,调用它会返回 6。
第二种用法 addCurry(1)(2)(3)(4)() 表示先传入 1,然后传入 2,再传入 3,最后再传入 4,最终返回的函数没有参数,调用它会返回 10。
第三种用法 addCurry(1, 2, 3)() 表示一次性传入 1、2、3 三个参数,最终返回的函数没有参数,调用它会返回 6。
第四种用法 addCurry(1)(2, 3)(4)() 表示先传入 1,然后传入 2 和 3,最后传入 4,最终返回的函数没有参数,调用它会返回 10。
function addCurry(...args) { const sum = args.reduce((a, b) => a + b, 0); function curry(...innerArgs) { if (innerArgs.length === 0) { return sum; } else { return addCurry(sum, ...innerArgs); } } curry.valueOf = function() { return sum; } return curry; }
  1. 实现一个通用的 compose函数,它接收任意数量的函数作为参数,返回一个新的函数,该新函数可以将所有传入的函数按照从右到左的顺序组合成一个新的函数。
例如:
const f = (x) => x + 1; const g = (x) => x * 2; const h = (x) => x / 3; const composed = compose(h, g, f); console.log(composed(1)); // 1.3333333333333333
  1. 实现一个函数 memoize,该函数接收一个函数作为参数,并返回一个新的函数,该新函数可以缓存原始函数的计算结果,并在下次相同的参数调用时直接返回缓存的结果
例如:
const fibonacci = function(n) { if (n < 2) { return n; } else { return fibonacci(n - 1) + fibonacci(n - 2); } }; const memoizedFibonacci = memoize

4.compose函数和pipeline函数

compose函数

💡
接收多个函数作为参数的高阶函数,它将这些函数从右往左依次组合起来,返回一个新的函数,该函数将会按照从右到左的顺序依次执行这些函数,将每个函数的返回值作为下一个函数的输入值
var compose = function(f,g) { return function(x) { return f(g(x)); }; }; // 例子 var toUpperCase = function(x) { return x.toUpperCase(); }; var exclaim = function(x) { return x + '!'; }; var shout = compose(exclaim, toUpperCase); shout("send in the clowns"); //=> "SEND IN THE CLOWNS!"
Array.prototype.reduce() 是 JavaScript 中的一个高阶函数,它用于将一个数组中的所有元素归纳为一个单独的值。它接收两个参数:一个回调函数和一个可选的初始值。
回调函数接收四个参数:累加器(初始值或上一次回调函数的返回值),当前元素,当前索引和原数组。在每次调用回调函数时,将使用回调函数的返回值更新累加器。最后,reduce() 方法返回累加器的最终值。
下面是一个简单的例子,展示如何使用 reduce() 将数组中的所有元素相加:
javascriptCopy code const numbers = [1, 2, 3, 4, 5]; const sum = numbers.reduce((accumulator, currentValue) => accumulator + currentValue); console.log(sum); // 15
在这个例子中,回调函数接收两个参数:累加器 accumulator 和当前元素 currentValue。回调函数将累加器和当前元素相加,并返回结果。在第一次调用回调函数时,累加器的值为数组的第一个元素(1),当前元素的值为数组的第二个元素(2)。在第二次调用回调函数时,累加器的值为上一次回调函数的返回值(3),当前元素的值为数组的第三个元素(3),以此类推。最后,reduce() 方法返回累加器的最终值(15)。
reduce() 方法的第二个可选参数是初始值。如果提供了初始值,则在第一次调用回调函数时,累加器的值将为初始值,当前元素的值将为数组的第一个元素。如果未提供初始值,则在第一次调用回调函数时,累加器的值将为数组的第一个元素,当前元素的值将为数组的第二个元素。
下面是一个使用初始值的例子:
javascriptCopy code const numbers = [1, 2, 3, 4, 5]; const initialValue = 10; const sum = numbers.reduce((accumulator, currentValue) => accumulator + currentValue, initialValue); console.log(sum); // 25
在这个例子中,我们将初始值设置为 10。因此,在第一次调用回调函数时,累加器的值为 10,当前元素的值为数组的第一个元素(1)。在第二次调用回调函数时,累加器的值为上一次回调函数的返回值(11),当前元素的值为数组的第二个元素(2),以此类推。最后,reduce() 方法返回累加器的最终值(25)。
reduce() 方法不会修改原数组,它只是返回一个新值。另外,需要注意的是,如果数组为空并且没有提供初始值,则 reduce() 方法将抛出一个错误。
 
 
 
 
 
 
 

关于我

My desire to practice my skills and share my acquired knowledge fuels my endeavors.

联系 : znjessie858@gmail.com

订阅

订阅我的文章,收到我的最新发布通知,可随时取消订阅!