OOP(面向对象)和 FP(函数式)之间竞争就从来没有停止过,大家都在伯仲之间,其实没有优劣之分,只有适不适合。
从顺应时代上角度来看,似乎 FP 最近占了些上风,不过感觉要想理解函数编程的思想以及将其熟练运用到开发中还是要花费一些时间和经历,毕竟没有 OOP 那么直观容易上手。
而且,不同的语言对 OOP 和 FP 的支持程度也不同,语言更适合哪种方式,这要看他们的天赋。OOP 和 FP 究竟哪一个更好不是今天讨论的重点。
今天我们是来看一看 javascript 更适合 FP 还是 OOP,以及如何在实际开发中将 FP 应用到我们项目中。
我们先不解释为什么 FP 适合 javascript。我们先看一看 OOP 在 javascript 应用的问题。
随着 javascript 这两年的迅速崛起,许多后端开发人员纷纷转型为前段开发人员。
可能是从 java 或是 net 转型过来的开发人员。他们多少都是 OOP 出身,来到 javascript 的世界同时也带来了 OOP 到 javascript。以 OOP 形式组织代码进行开发,典型就是通过五花八门的方式类实现类,以及各种在 OOP 中的设计模式。
但在 javascript 核心就是函数,prototype (原型链)是 javascript 实现 OOP 的基础,如果还不了解或是对 javascript 原型链了解还是那么彻底,自己查一查资料吧,想要学习好 javascript 就要深刻了解 javascript 的原型链。
所以javascript从本质上来看他与那些骨子里就支持 OOP 的语言像 java 呀是不同,不同在于javascript 是考原型链或其他方式模拟类,以及类之间的关系,最终他还是 function 而不是类概念,而 java 就不同了 java 是基于对象来实现的类和类关系。
好所以说 javascript 并不是适合 OOP 。
在聊一聊函数编程在 javascript 的应用,利益是驱动我们学习和工作的动力,那么学习并应用函数式编程的动力是什么呢?是炫酷吗?当然不是炫酷,是因为函数式编程能能好地满足现代 web 前端应用开发的需求,便于程序维护和扩展,降低测试成本。
- 动机
- 技术
- 函数式编程在 js 中应用
我们学习一门语言,需要学习他的语法和语义,在 javascript 语言中,我们有更多的语法和语义需要熟悉并记忆。例如 javascript 中的等于和真等于的区别等。还有 javascript 变量作用域是函数而不是块级别。
现在有了 Jshint ,可以给我们许多友好的提示,提示我们 code 一些存在的问题。
还有就是,在今天各种框架中,例如 Angular 或是 react 中,也都有自己的语法和语义,除了javascript 语言的语义之外,我们还需要记住这些框架各自的语法和语义。
自己曾经使用 Typescript 开发过一个应用模块,如何您有 c# 开发背景,然后想转型为 javascript。可以先学习 typescript 因为他是 javascript 超级,些 typescript 就像些 c# 或 java,一些思维同样适用,然后会编译为 javascript。而且轻松地讲您的 OOP 贯彻到您的 typescript 代码中。
class Greeter {
greeting: string;
constructor(message:string){
this.greeting = message;
}
greet(){
return "Hello," + this.greeting;
}
}
var greeter = new Greeter("world")
但是我想说我们为什么绕弯子?我们真需要 OOP 在 javascript 这一门函数是一等公民的语言中?当时当然是没有必要。我们考虑语言本身特性,还是函数式编程更适合 javascript。
下图中是当下比较火的实现了函数式编程的框架。
- Rxjs
- React
简单的数据结构、高阶函数和泛化再使用这些一切一起的 javascript 特性都是适合函数式编程。
- 简单数据结构
- 高阶函数
- 泛化和复用
在函数式编程中代码表述性会更清晰,我们下面代码风格进行对比一种是 Imperative 后面是函数式。当然是后者更清晰,利于我们阅读代码,表示对数组每一个元素使用 print 方法。而我们更熟悉的 imperative 确实需要花一定时间来读懂代码含义。
function printArray(array){
for(var i = 0; i < array.length; i++)
print(array[i]);
}
//推荐写法
forEach(["hello","functional","programming"],print);
我们先看一看 map 这个函数,输入时集合同样输出也是集合,作为参数参入 map 的函数会作用,也就是操作集合每一个元素。好处是对集合逻辑处理可以共享出来,提供复用性。
如果学习过 Rxjs 一定对这张图不会陌生,map 接受一个集合,然后操作集合每一个元素,可能是转换另一种元素,或是数据结构,所有操作后的元素组成我们输出的集合。 想一想我们函数就是输入一个变量输出一个变量,他们的值是一一对应的。
下图帮助您更好理解 reduce 这个函数,我们集合中的元素,一层一层地在原有基础上(也就是上一次的结果值)进行包裹等到最终的结果。
我们现在看一看代码,这里我们引用一个轻量级的 javascript 库 underscore。这是个人非常喜欢一个 javascript 库,强烈推荐给大家,即使您不算采用函数式编程,了解和使用 underscore 也会给你带工作上轻松和。老外很多事例都是做pizza,今天给出的事例也不例外,我们准备一些蔬菜,然后就是切菜的工作。下图的方式一种传统的 Imperative 方式来编写方式。
我们再进一步,用循环数组方式来完成将买来的菜切丁呀准备呀。
现在是时候让我们函数式的 map 登场了,这里 map 接受两个参数,第一个参数就是我们数组,第二个参数是操作数组每个元素的函数。然后将操作后的每一个元素组成一个数组来输出。
看输出的结果不错,好我们已经成功地迈出第一步,将我们 Imperative 式的程序改造为函数式程序。
准备好了我们事先切好的菜,就可以下锅了,放入到 Pot 开煮,煮我们除了需要材料还需两个参数,时间长短和火候,这里用 temperature 和 duration 表示温度和时间。setStoveTemp 设置温度
在开始烹饪菜肴之前,我们需要将材料进行混合,addIngedientsToPot 我们需要将切好材料(数组)放入 pot 中,
这样依次在上一次结果基础进行操作我们就会想到 reduce ,reduce 接受三个参数,第一个就是输入的数组,第二个就是我们函数,这个函数接受两个参数
我们来举一个简单事例来说明 reduce 是如何使用的,reduce 第一个参数就是我们接受数组 arrayOfNums ,第二个参数是一个函数,这个函数接受两个参数,memo 是用于记忆每一次操作后结果,i 为数组每一个元素,我们通过可以将 i 加入 memo 来事前求和,最后一个参数为 reduce 的初始值 0。
我们做肉汁 cookGravy 方法,可以先设定好烹饪的温度和时间参数,然后在调用 cookInPot 方法来根据设定进行烹饪肉汁。
这时我们可以用到 partial 这个偏函数,我们定义偏函数,偏函数是返回一个函数,将事先准备好的参数传递给返回函数。
先看一个简单偏函数, sum 是取和函数接受两个参数 a 和 b,如果我们函数是对输入加 2 呢,我们可以改造一下
如何恰当解释让您了解什么是偏函数,偏函数目的在于减少我们函数的参数,例如函数 funa 需要三个参数(a,b,c),我们可以用一个函数 funcurry 接受 a,b 两个参数然后返回函数接受参数 c。这样我们就可以减少 funa 的参数个数。从而我们解决了 cookStew 嵌套 cookInPot 这个函数,取而代之用 cookStewCurry 接受 _.partial 返回的函数,
我们同样创建其他的偏函数。
在 cookInPot 方法中,我们先执行 addIngredientsToPot 我们先将准备材料放置容器中进行烹调。这两个步骤是有一定先后顺序,请求前一个函数的执行结果作为后一个函数的参数被后一个函数所使用。
上面我们学习了偏函数,然后来学习 compose 将多个函数组合来使用,使用的方式先执行最右侧的函数执行结果作为左侧
在 underscore 的 compose 可以返回函数集 functions 组合后的复合函数, 也就是一个函数执行完之后把返回的结果再作为参数赋给下一个函数来执行. 以此类推. 在数学里, 把函数 f(), g(), 和 h() 组合起来可以得到复合函数 f(g(h()))。