函数式编程(2024 面试版)

函数式编程


函数式编程初探 - 阮一峰的网络日志

1、什么是函数式编程

函数式编程是一种编程范式

。主要特点包括:不可变性(数据不可改变)、无副作用(函数输出只依赖于输入,不依赖于外部状态)、高阶函数(函数可以作为参数传递或作为返回值)和函数组合。

以下是一些鲜明的特点

  • 1. 函数是"第一等公民"
  • 2. 只用"表达式",不用"命令式语句"
  • 3. 没有"副作用"
  • 4. 不修改状态
  • 5. 引用透明
  • 高阶函数与柯里化
  • 递归与流式处理

函数式编程就是规范的使用函数,组合一些小函数来构建一个新函数;函数式编程是面向数学的抽象,将计算描述为一种表达式求值,一句话,函数式程序就是一个表达式

函数式编程具有五个鲜明的特点。

1. 函数是"第一等公民"

所谓"第一等公民"(first class),指的是函数与其他数据类型一样,处于平等地位,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。

举例来说,下面代码中的print变量就是一个函数,可以作为另一个函数的参数。

var print = function(i){ console.log(i);};

[1,2,3].forEach(print);

2. 只用"表达式",不用"语句"

"表达式"(expression)是一个单纯的运算过程,总是有返回值;"语句"(statement)是执行某种操作,没有返回值。函数式编程要求,只使用表达式,不使用语句。也就是说,每一步都是单纯的运算,而且都有返回值。

原因是函数式编程的开发动机,一开始就是为了处理运算(computation),不考虑系统的读写(I/O)。"语句"属于对系统的读写操作,所以就被排斥在外。

当然,实际应用中,不做I/O是不可能的。因此,编程过程中,函数式编程只要求把I/O限制到最小,不要有不必要的读写行为,保持计算过程的单纯性。

3. 没有"副作用"

所谓"副作用"(side effect),指的是函数内部与外部互动(最典型的情况,就是修改全局变量的值),产生运算以外的其他结果。

函数式编程强调没有"副作用",意味着函数要保持独立,所有功能就是返回一个新的值,没有其他行为,尤其是不得修改外部变量的值。

4. 不修改状态

上一点已经提到,函数式编程只是返回新的值,不修改系统变量。因此,不修改变量,也是它的一个重要特点。

在其他类型的语言中,变量往往用来保存"状态"(state)。不修改变量,意味着状态不能保存在变量中。函数式编程使用参数保存状态,最好的例子就是递归。下面的代码是一个将字符串逆序排列的函数,它演示了不同的参数如何决定了运算所处的"状态"。

function reverse(string) {

if(string.length == 0) {

return string;

} else {

return reverse(string.substring(1, string.length)) + string.substring(0, 1);

}

}

由于使用了递归,函数式语言的运行速度比较慢,这是它长期不能在业界推广的主要原因。

5. 引用透明

引用透明(Referential transparency),指的是函数的运行不依赖于外部变量或"状态",只依赖于输入的参数,任何时候只要参数相同,引用函数所得到的返回值总是相同的。

有了前面的第三点和第四点,这点是很显然的。其他类型的语言,函数的返回值往往与系统状态有关,不同的状态之下,返回值是不一样的。这就叫"引用不透明",很不利于观察和理解程序的行为。

2、函数式编程有什么特点

1. 代码简洁,开发快速

函数式编程大量使用函数,减少了代码的重复,因此程序比较短,开发速度较快。

2. 接近自然语言,易于理解

函数式编程不依赖、也不会改变外界的状态,只要给定输入参数,返回的结果必定相同。因此,每一个函数都可以被看做独立单元,很有利于进行单元测试(unit testing)和除错(debugging),以及模块化组合。

3. 更方便的代码管理

函数式编程不依赖、也不会改变外界的状态,只要给定输入参数,返回的结果必定相同。因此,每一个函数都可以被看做独立单元,很有利于进行单元测试(unit testing)和除错(debugging),以及模块化组合。

4. 易于"并发编程"

函数式编程不需要考虑"死锁"(deadlock),因为它不修改变量,所以根本不存在"锁"线程的问题。不必担心一个线程的数据,被另一个线程修改,所以可以很放心地把工作分摊到多个线程,部署"并发编程"(concurrency)。

5. 代码的热升级

函数式编程没有副作用,只要保证接口不变,内部实现是外部无关的。所以,可以在运行状态下直接升级代码,不需要重启,

方便单元测试


 

一句话总结:函数式编程将函数作为一等公民,主要是利用函数等特性,即给一个确定的输入总能保证相同的输出,函数只做一件事情等等,让代码看起来更简短且维护性更高

  1. 流程变量赋值后就不去修改,算不上纯粹声明式编程,但利于人脑理解。
  2. 处理容器数据结构时多用高阶函数,少用for,while和break,虽然不一定能算上函数式,但利于产生更短小易理解的代码

案例:

函数式和命令式

给你两个接口获取数据,

将几个集合(数组中每一项为对象)中index = 6 的那个show改为false

正常的 命令式做法就是 直接for循环到那一步然后修改

违背了原则

  • 直接修改变量(改值时万恶之源)
  • 可能修改外部的变量产生副作用

3、如何实践

  • map、filter、reduce、every、some、includes、建议使用 loadsh
  • 对象和数组尽量使用 assign、结构等用法
  • 单纯的 boolean、数字等 建议使用函数输出
  • 尽量不是要 for
  • 尽量不是要 let var,只使用 const
  • 尽量不是要偏函数 、函数柯里化
    • 不要过度 柯里化 add(1,2,3) => add(1)(2)(3)
    • 不要过度 柯里化偏函数 const cdd = add(1)(2) cdd(3) cdd(4)

其实偏函数相当于提前设定好了某个函数需要的部分参数,并返回一个新的函数来等待接下来的参数传入:

一些函数式编程解释

很多人或ES5重度使用者 长期使用for 循环,但是你在阅读一些优质框架源码时会发现基本会被map filter reduce forEach代替,尽管很多人证明for循环或者多次定义变量对性能并没什么影响,反而函数式编程会牺牲一点性能,并以此为理由排斥函数式编程。

针对以上疑惑,我给自己的解释:

1、js性能很重要,但是没你想的那么重要

现代js 运行环境,无论是手机还是PC,js的性能针对页面微不足道,js是单线程,再看看各位电脑和手机的硬件配置。js所谓影响的一丢丢性能真的是微乎其微。反而是多图或者多http请求导致页面问题。或者你可以试试1000*1000的双for循环 浏览器多长时间跑完。(并不是要建议你可以写双循环)

2、代码少bug,维护性高才是第一追求

当前还是有很多人说微不足道的js性能(其实就是懒得提升自己代码质量)。函数式编程,可以很清晰的跟踪维护核心数据的每一步变动。只要是团队稍微有一些函数式编程的经验加上提交测试前端代码review,维护同事的代码成本一下子降下来。笔者所在的团队,线上bug几乎不会有前端逻辑上或者数据上的问题,只会有一些兼容、逻辑遗漏之类的bug。整体low的bug减少明显,代码维护性高。

以下给出一些案例(前几题太简单可以选择忽略)进行比较:

函数式编程常见面试题

问题1:请解释什么是函数式编程,并列举它的几个主要特点。

函数式编程是一种编程范式

。主要特点包括:不可变性(数据不可改变)、无副作用(函数输出只依赖于输入,不依赖于外部状态)、高阶函数(函数可以作为参数传递或作为返回值)和函数组合。

问题2:在JavaScript中,如何实现一个纯函数?

答案纯函数是指对于相同的输入,总是返回相同的输出,而且没有任何可观察的副作用。在JavaScript中,可以通过不修改外部状态、不引用外部可变变量、并且只返回计算结果来实现纯函数。

问题3:请解释map、filter和reduce这三个高阶函数的作用,并给出示例。

答案:map、filter和reduce都是高阶函数,它们接受一个函数作为参数并应用于集合中的每个元素。map将函数应用于每个元素并返回新的集合,filter返回符合函数条件的元素组成的新集合,reduce将函数应用于每个元素并累积结果。例如,使用map可以将数组中的每个元素乘以2,使用filter可以筛选出偶数元素,使用reduce可以计算数组的总和。

问题4:在函数式编程中,如何处理错误和异常?

答案:函数式编程通常倾向于使用“错误处理”而不是“异常处理”。一种常见的做法是使用“Maybe”或“Either”这样的数据类型来表示可能失败的操作。这些数据类型允许你在不抛出异常的情况下处理错误,并通过函数组合和链式调用来传播错误状态。在JavaScript中,可以使用类似Promise的结构来处理异步错误。

问题5:什么是柯里化(Currying)?请给出一个JavaScript中的柯里化函数示例。

答案柯里化是一种将使用多个参数的函数转换成一系列使用一个参数的函数的技术。在JavaScript中,可以通过逐步接收参数并返回新函数的方式实现柯里化。例如,一个加法函数可以柯里化为先接收第一个参数并返回一个新函数,新函数再接收第二个参数并返回结果。

问题6:请解释什么是闭包(Closure),并给出一个JavaScript中的闭包示例。

答案:闭包是JavaScript函数的一个特性,它允许函数在其词法作用域之外被调用时仍能访问其作用域内的变量。换句话说,闭包可以让函数记住并访问其定义时所在的词法作用域,即使函数在其原始作用域之外执行。例如,一个外部函数返回一个内部函数,内部函数可以访问外部函数的变量,即使外部函数已经执行完毕。

问题7:在函数式编程中,如何实现函数的组合?请给出一个JavaScript中的函数组合示例。

答案:函数组合是将多个函数组合成一个新函数的过程,其中每个函数的输出作为下一个函数的输入。在JavaScript中,可以使用高阶函数来实现函数组合。例如,可以定义一个compose函数,它接受一系列函数作为参数,并返回一个新函数。新函数从右到左依次调用传入的函数,并将每个函数的输出作为下一个函数的输入。

问题8:什么是函数的副作用?在函数式编程中如何避免副作用?

答案:函数的副作用是指函数除了返回计算结果之外,还修改了外部状态或产生了其他可观察的效果。在函数式编程中,应该尽量避免副作用,以保持函数的纯粹性和可预测性。可以通过使用不可变数据和无状态函数来避免副作用。此外,还可以使用函数式编程库和工具来辅助编写无副作用的代码。

问题9:请解释什么是函数的惰性求值(Lazy Evaluation),并给出一个JavaScript中的惰性求值示例。

答案:惰性求值是一种计算策略,它延迟表达式的求值直到其值真正被需要时才进行计算。在JavaScript中,可以使用生成器函数或迭代器来实现惰性求值。例如,可以定义一个生成器函数来生成一个无限序列,并在每次迭代时只计算下一个值。这样,序列的求值就被延迟到了需要时才进行。

问题10:在函数式编程中,如何实现函数的记忆化(Memoization)?请给出一个JavaScript中的记忆化函数示例。

答案:函数的记忆化是一种优化技术,它通过将函数的计算结果缓存起来,以便在后续调用中直接使用缓存结果而避免重复计算。在JavaScript中,可以使用闭包和对象来实现函数的记忆化。例如,可以定义一个记忆化函数,它接受一个函数作为参数并返回一个新函数。新函数在调用时会检查缓存中是否已存在结果,如果存在则直接返回缓存结果,否则计算结果并缓存起来再返回。这样可以提高函数的执行效率并减少重复计算。

问题 11:前端函数式编程 中说的副作用和 react 中 useEffect 的副作用 请给出合理解释

在函数式编程中,副作用是指函数执行过程中对外部环境产生的影响,如修改全局变量、修改文件或数据库等。函数式编程倡导的是无副作用的编程,即一个函数的执行只依赖于输入参数,不会对外部环境产生任何影响,这样可以避免不可预知的行为和副作用的累积。

而在React中,副作用是指组件在渲染过程中对DOM产生的影响,如修改DOM元素、发送网络请求等。React中的副作用被定义为“具有可观察的行为,但不会影响组件渲染结果的代码”。React通过使用useEffect Hook来管理组件的副作用,useEffect可以在组件挂载、更新、卸载时执行一些额外的操作,而不会影响组件的渲染结果。

虽然React中的副作用和函数式编程中的副作用不同,但是它们都需要注意副作用的影响范围和副作用的执行时机,以避免出现不可预知的行为和副作用的累积。

1. 前端函数式编程中的副作用

在函数式编程中,一个函数通常被看作是一个纯函数(pure function),即它对于相同的输入总是返回相同的输出,并且没有可观察的副作用。然而,在实际应用中,我们经常需要执行一些与外部世界交互的操作,比如修改全局变量、读取或写入文件、发送网络请求等。这些操作被称为副作用。

在函数式编程中,副作用通常被视为不良的,因为它们破坏了函数的纯粹性和可预测性。为了管理副作用,函数式编程提供了一些技术和模式,比如将副作用隔离到特定的函数或模块中,或者使用函数式编程库提供的副作用管理机制(比如Redux的中间件)。

2. React中的副作用(useEffect)

在React中,副作用是指在组件渲染过程中执行的一些额外操作,这些操作可能会影响到组件的状态或外部世界。React通过useEffect Hook提供了管理副作用的机制。

useEffect接受一个函数作为参数,这个函数包含了需要执行的副作用逻辑。当组件渲染或更新时,React会执行这个函数。通过useEffect,我们可以在组件的生命周期中执行各种操作,比如发送网络请求、订阅事件、手动修改DOM等。

需要注意的是,虽然useEffect中的函数可以执行副作用,但它本身并不是一个纯函数。useEffect函数中的逻辑可能会依赖于组件的props或state,并且可能会产生不同的结果或副作用。因此,在使用useEffect时,我们需要注意避免在副作用函数中执行导致组件状态不一致或难以预测的操作。

合理解释

前端函数式编程中的“副作用”概念与React中useEffect所处理的“副作用”本质相同,都是指在计算过程中对程序状态或环境的非纯操作。主要区别在于:

  • 上下文与目标: 函数式编程中的副作用讨论是针对整个编程范式的哲学和实践,旨在追求程序的纯度、可预测性和易于推理。而React中的副作用管理是具体针对UI库的功能需求,确保组件在渲染周期中正确、高效地处理与视图更新相关的非纯行为。
  • 处理策略: 函数式编程倾向于通过抽象(如monads)、隔离(副作用函数)和特殊的数据结构来封装和控制副作用。React则提供了useEffect这一内置Hook作为标准化的接口,直接集成在组件的声明式编写流程中,利用依赖数组实现自动化的调度和清理。
  • 目的相同:尽管上下文和实现方式有所不同,但两者的目的都是为了在保持函数式编程的纯函数特性的同时处理副作用。这有助于提高代码的可预测性、可测试性和可维护性。

问题12:请解释什么是高阶函数,并给出一个JavaScript中的高阶函数示例。

答案:高阶函数是接受一个或多个函数作为参数,并返回一个新函数的函数。在JavaScript中,高阶函数非常常见。例如,Array.prototype.map、Array.prototype.filter和Array.prototype.reduce都是高阶函数。以下是一个简单的高阶函数示例:

function twice(f, x) {  
  return f(f(x));  
}  
  
function addOne(x) {  
  return x + 1;  
}  
  
console.log(twice(addOne, 5)); // 输出 7,因为((5 + 1) + 1) = 7

问题13:在函数式编程中,map和reduce有什么区别?请给出各自的用途和示例。

答案:map和reduce都是高阶函数,用于处理集合数据。map接受一个函数和一个集合,将函数应用于集合的每个元素,并返回一个新的集合,其中包含应用函数后的结果。reduce也接受一个函数和一个集合,但它返回一个单一值,该值是通过将函数累积应用于集合的每个元素而得到的。

示例:

javascript复制代码



const numbers = [1, 2, 3, 4];  
  
// 使用 map 将每个数字乘以 2  
const doubled = numbers.map(n => n * 2);  
console.log(doubled); // 输出 [2, 4, 6, 8]  
  
// 使用 reduce 计算数字的总和  
const sum = numbers.reduce((acc, n) => acc + n, 0);  
console.log(sum); // 输出 10

问题14:请解释什么是纯函数,并给出一个非纯函数的例子。

答案:纯函数是指对于相同的输入总是返回相同的输出,并且没有可观察的副作用的函数。纯函数不依赖于程序执行期间的任何外部状态,也不修改任何外部状态。相反,非纯函数可能会读取或修改全局变量、引发异常、执行I/O操作等。

非纯函数示例:

javascript复制代码



let counter = 0;  
function incrementCounter() {  
  counter++; // 修改外部状态  
  return counter;  
}  
  
console.log(incrementCounter()); // 输出 1  
console.log(incrementCounter()); // 输出 2,相同的输入但输出不同,因此是非纯函数

问题15:在JavaScript中如何实现函数的柯里化?请给出一个柯里化函数的示例。

答案:柯里化是一种将使用多个参数的函数转换成一系列使用一个参数的函数的技术。在JavaScript中,可以通过逐步接收参数的方式实现柯里化。

示例:

javascript复制代码



function curryAdd(x) {  
  return function(y) {  
    return x + y;  
  };  
}  
  
const addFive = curryAdd(5); // 创建一个新函数,该函数将固定第一个参数为5  
console.log(addFive(3)); // 输出 8,因为 5 + 3 = 8

问题16:请解释什么是函数的组合,并给出一个JavaScript中实现函数组合的示例。

答案:函数的组合是将多个函数组合成一个新函数的过程,其中每个函数的输出作为下一个函数的输入。在JavaScript中,可以使用高阶函数来实现函数的组合。

示例:

javascript复制代码



function compose(f, g) {  
  return function(x) {  
    return f(g(x)); // 先调用g,然后将其结果作为f的输入  
  };  
}  
  
function double(x) {  
  return x * 2;  
}  
  
function increment(x) {  
  return x + 1;  
}  
  
const doubleThenIncrement = compose(increment, double);  
console.log(doubleThenIncrement(5)); // 输出 11,因为 ((5 * 2) + 1) = 11

问题17:在函数式编程中,如何确保数据的不可变性?给出一种在JavaScript中实现不可变数据的方法。

答案:在函数式编程中,数据的不可变性是通过不直接修改数据而是通过创建新数据来保持的。在JavaScript中,可以通过使用Object.freeze方法来浅冻结对象,或者使用库(如Immutable.js)来实现深层次的不可变数据结构。另外,使用纯函数和避免直接修改状态也是确保数据不可变性的关键。

浅冻结示例:

javascript复制代码



const obj = { foo: 'bar' };  
Object.freeze(obj); // 冻结对象  
  
// 尝试修改对象将失败,但在非严格模式下不会抛出错误  
obj.foo = 'baz'; // 不起作用,因为对象被冻结了  
console.log(obj.foo); // 输出 'bar'

问题18:请解释什么是函数的偏函数应用(Partial Application),并给出一个JavaScript中的偏函数应用示例。

答案:偏函数应用是指将一个函数与一个或多个参数预先结合,从而产生一个新的函数的技术。新函数需要的参数比原始函数少,并且当被调用时,会使用预先结合的参数。

示例:

javascript复制代码



function greet(greeting, name) {  
  return `${greeting}, ${name}!`;  
}  
  
// 使用偏函数应用创建一个新函数,它固定了greeting参数为"Hello"  
const sayHello = greet.bind(null, "Hello");  
  
console.log(sayHello("Alice")); // 输出 "Hello, Alice!"

问题19:在JavaScript中如何实现一个简单的函数管道(Function Pipeline)?请给出一个示例。

答案:函数管道是将一系列函数链接在一起,使得每个函数的输出成为下一个函数的输入的一种方式。在JavaScript中,可以使用高阶函数和数组的reduce方法来实现函数管道。

示例:

javascript复制代码



function pipe(functions) {  
  return function(initialValue) {  
    return functions.reduce((currentValue, currentFunction) => {  
      return currentFunction(currentValue);  
    }, initialValue);  
  };  
}  
  
// 定义一系列函数  
const double = x => x * 2;  
const increment = x => x + 1;  
const square = x => x * x;  
  
// 创建一个管道,将这三个函数链接在一起  
const pipeline = pipe([double, increment, square]);  
  
// 使用管道处理一个初始值  
console.log(pipeline(5)); // 输出 ((5 * 2) + 1) ^ 2 = 121
  • 29
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值