最开始接触函数式编程的时候是在小米工做的时候,那个时候看老大之前写的代码各类 compose,而后一些 ramda 的一些工具函数,看着很吃力,而后极力吐槽函数式编程,如今回想起来,那个时候的本身真的是见识短浅,只想说,'真香'。javascript
最近在研究函数式编程,真的是在学习的过程当中感受本身的思惟提高了不少,抽象能力大大的提升了,让我深深的感觉到了函数式编程的魅力。因此我打算后面用 5 到 8 篇的篇幅,详细的介绍一下函数式编程的思想,基础、如何设计、测试等。前端
今天这篇文章主要介绍函数式编程的思想。java
面向对象编程(OOP)经过封装变化使得代码更易理解。
函数式编程(FP)经过最小化变化使得代码更易理解。
-- Michacel Feathers(Twitter)
总所周知 JavaScript 是一种拥有不少共享状态的动态语言,慢慢的,代码就会积累足够的复杂性,变得笨拙难以维护。面向对象设计能帮咱们在必定程度上解决这个问题,可是还不够。数据库
因为有不少的状态,因此处理数据流和变化的传递显得尤其重要,不知道大家知道响应式编程与否,这种编程范式有助于处理 JavaScript 的异步或者事件响应。总之,当咱们在设计应用程序的时候,咱们应该考虑是否遵照了如下的设计原则。编程
我这能这么跟你说,一旦你学会了函数式编程,这些问题迎刃而解,原本函数式编程就是这个思想,一旦你掌握了函数式,而后你再学习响应式编程那就比较容易懂了,这是我亲身体会的。我以前在学 Rxjs 的时候是真的痛苦,说实话,Rxjs 是我学过最难的库了,没有之一。在经历过痛苦的一两个月以后,有些东西仍是不能融会贯通,知道我最近研究函数式编程,才以为是理所固然。毫无夸张,我也尽可能在后面的文章中给你们介绍一下 Rxjs,这个话题我也在公司分享过。redux
简单来讲,函数式编程是一种强调以函数使用为主的软件开发风格。看到这句我想你仍是一脸懵逼,不知道函数式编程是啥,不要着急,看到最后我相信你会明白的。数组
还有一点你要记住,函数式编程的目的是使用函数来抽象做用在数据之上的控制流和操做,从而在系统中消除反作用并减小对状态的改变。浏览器
下面咱们经过例子来简单的演示一下函数式编程的魅力。数据结构
如今的需求就是输出在网页上输出 “Hello World”
。app
可能初学者会这么写。
document.querySelector('#msg').innerHTML = '<h1>Hello World</h1>'
这个程序很简单,可是全部代码都是死的,不能重用,若是想改变消息的格式、内容等就须要重写整个表达式,因此可能有经验的前端开发者会这么写。
function printMessage(elementId, format, message) { document.querySelector(elementId).innerHTML = `<${format}>${message}</${format}>` } printMessage('msg', 'h1', 'Hello World')
这样确实有所改进,可是任然不是一段可重用的代码,若是是要将文本写入文件,不是非 HTML,或者我想重复的显示 Hello World
。
那么做为一个函数式开发者会怎么写这段代码呢?
const printMessage = compose(addToDom('msg', h1, echo)) printMessage('Hello World')
解释一下这段代码,其中的 h1
和 echo
都是函数,addToDom
很明显也能看出它是函数,那么咱们为何要写成这样呢?看起来多了不少函数同样。
其实咱们是讲程序分解为一些更可重用、更可靠且更易于理解的部分,而后再将他们组合起来,造成一个更易推理的程序总体,这是咱们前面谈到的基本原则。
compose 简单解释一下,他会让函数从最后一个参数顺序执行到第一个参数,compose 的每一个参数都是函数,不明白的能够查一下,在 redux 的中间件部分这个函数式精华。
能够看到咱们是将一个任务拆分红多个最小颗粒的函数,而后经过组合的方式来完成咱们的任务,这跟咱们组件化的思想很相似,将整个页面拆分红若干个组件,而后拼装起来完成咱们的整个页面。在函数式编程里面,组合是一个很是很是很是重要的思想。
好,咱们如今再改变一下需求,如今咱们须要将文本重复三遍,打印到控制台。
var printMessaage = compose(console.log, repeat(3), echo) printMessage(‘Hello World’)
能够看到咱们更改了需求并无去修改内部逻辑,只是重组了一下函数而已。
能够看到函数式编程在开发中具备声明模式。为了充分理解函数式编程,咱们先来看下几个基本概念。
函数式编程属于声明是编程范式:这种范式会描述一系列的操做,但并不会暴露它们是如何实现的或是数据流如何传过它们。
咱们所熟知的 SQL 语句就是一种很典型的声明式编程,它由一个个描述查询结果应该是什么样的断言组成,对数据检索的内部机制进行了抽象。
咱们再来看一组代码再来对比一下命令式编程和声明式编程。
// 命令式方式 var array = [0, 1, 2, 3] for(let i = 0; i < array.length; i++) { array[i] = Math.pow(array[i], 2) } array; // [0, 1, 4, 9] // 声明式方式 [0, 1, 2, 3].map(num => Math.pow(num, 2))
能够看到命令式很具体的告诉计算机如何执行某个任务。
而声明式是将程序的描述与求值分离开来。它关注如何用各类表达式来描述程序逻辑,而不必定要指明其控制流或状态关系的变化。
为何咱们要去掉代码循环呢?循环是一种重要的命令控制结构,但很难重用,而且很难插入其余操做中。而函数式编程旨在尽量的提升代码的无状态性和不变性。要作到这一点,就要学会使用无反作用的函数--也称纯函数
纯函数指没有反作用的函数。相同的输入有相同的输出,就跟咱们上学学的函数同样,经常这些状况会产生反作用。
举一个简单的例子
var counter = 0 function increment() { return ++counter; }
这个函数就是不纯的,它读取了外部的变量,可能会以为这段代码没有什么问题,可是咱们要知道这种依赖外部变量来进行的计算,计算结果很难预测,你也有可能在其余地方修改了 counter 的值,致使你 increment 出来的值不是你预期的。
对于纯函数有如下性质:
可是在咱们平时的开发中,有一些反作用是难以免的,与外部的存储系统或 DOM 交互等,可是咱们能够经过将其从主逻辑中分离出来,使他们易于管理。
如今咱们有一个小需求:经过 id 找到学生的记录并渲染在浏览器(在写程序的时候要想到可能也会写到控制台,数据库或者文件,因此要想如何让本身的代码能重用)中。
// 命令式代码 function showStudent(id) { // 这里假如是同步查询 var student = db.get(id) if(student !== null) { // 读取外部的 elementId document.querySelector(`${elementId}`).innerHTML = `${student.id},${student.name},${student.lastname}` } else { throw new Error('not found') } } showStudent('666') // 函数式代码 // 经过 find 函数找到学生 var find = curry(function(db, id) { var obj = db.get(id) if(obj === null) { throw new Error('not fount') } return obj }) // 将学生对象 format var csv = (student) => `${student.id},${student.name},${student.lastname}` // 在屏幕上显示 var append = curry(function(elementId, info) { document.querySelector(elementId).innerHTML = info }) var showStudent = compose(append('#student-info'), csv, find(db)) showStudent('666')
若是看不懂 curry (柯里化)的先不着急,这是一个对于新手来讲比较难理解的一个概念,在函数式编程里面起着相当重要的做用。
能够看到函数式代码经过较少这些函数的长度,将 showStudent 编写为小函数的组合。这个程序还不够完美,可是已经能够展示出相比于命令式的不少优点了。
咱们看到纯函数的输出结果是一致的,可预测的,相同的输入会有相同的返回值,这个其实也被称为引用透明。
引用透明是定义一个纯函数较为正确的方法。纯度在这个意义上表面一个函数的参数和返回值之间映射的纯的关系。若是一个函数对于相同的输入始终产生相同的结果,那么咱们就说它是引用透明。
这个概念很容易理解,简单的举两个例子就好了。
// 非引用透明 var counter = 0 function increment() { return ++counter } // 引用透明 var increment = (counter) => counter + 1
其实对于箭头函数在函数式编程里面有一个高大上的名字,叫 lambda 表达式,对于这种匿名函数在学术上就是叫 lambda 表达式,如今在 Java 里面也是支持的。
不可变数据是指那些建立后不能更改的数据。与许多其余语言同样,JavaScript 里有一些基本类型(String,Number 等)从本质上是不可变的,可是对象就是在任意的地方可变。
考虑一个简单的数组排序代码:
var sortDesc = function(arr) { return arr.sort(function(a, b) { return b - a }) } var arr = [1, 3, 2] sortDesc(arr) // [1, 2, 3] arr // [1, 2, 3]
这段代码看似没什么问题,可是会致使在排序的过程当中会产生反作用,修改了原始引用,能够看到原始的 arr 变成了 [1, 2, 3]
。这是一个语言缺陷,后面会介绍如何克服。
文章内容来至于《JavaScript函数式编程指南》
欢迎关注我的公众号【前端桃园】