JavaScript 作为一种典型的多范式编程语言,这两年随着React\vue的火热,函数式编程的概念也开始流行起来,lodashJS、folktale等多种开源库都使用了函数式的特性。

一.认识函数式编程

程序的本质是:根据输入通过某种运算得到输出

函数式编程(Functional programming)是一种编程思想/范式 ,其核心思想是将运算过程抽象成函数(指数学的函数而不是程序的方法或函数即纯函数),也就是面向函数编程,描述函数/数据 之间的映射,做到最大程度的复用; 学习函数式编程真正的意义在于让你认识到一种用函数的角度去抽象问题的思路。如果你手中只有一个锤子,那你看什么都像钉子。

没有最好的,只有最适合的

函数式编程起源是一门叫做范畴论(Category Theory)的数学分支。理解函数式编程的关键,就是理解范畴论。它是一门很复杂的数学,认为世界上所有的概念体系,都可以抽象成一个个的"范畴"(category)。

    • 所有成员是一个集合
    • 变形关系是函数

引用维基百科

"范畴就是使用箭头连接的物体。"(In mathematics, a category is an algebraic structure that comprises "objects" that are linked by "arrows". )

https://en.wikipedia.org/wiki/Category_(mathematics)

也就是说,彼此之间存在某种关系的概念、事物、对象等等,都构成"范畴"。只要能找出它们之间的关系,就能定义一个"范畴",代码中称之为容器

    • 值(value)
    • 值的变形关系(函数)
class container {
constructor(v) {
this._v = v;
} addOne(x) {
return x + 1;
}
}

container是一个类,也是一个容器,里面包含一个值(this._v)和一种变形关系(addOne)。这里的范畴,就是所有彼此之间相差1的数字。

本质上,函数式编程只是范畴论的运算方法,跟数理逻辑、微积分、行列式是同一类东西,都是数学方法,只是碰巧它能用来写程序。

二. 函数相关知识

1. 函数是一等公民 (MON first class function)

函数可以存储变量 即函数表达式

var fn = function (){};

函数可以是参数

var fn = function (fn1){...}

fn(function(){...})

函数可以是返回值

function fn(){return function(){...}}

2.高阶函数(higher order function),用于抽象通用问题;

把函数作为参数传递给另一个函数

function forEach(arr ,fn){
for(var i = 0; i < arr.length; i++){
fn1(arr[i], i);
}
}
forEach([1,2,3], function(el, i){...})

函数作为另一个函数的返回结果

function fn(){
return function(){
console.log(1)
}
}
fn()();

3.闭包(Closure),函数按值传递的引用都是闭包

function fn(){
var a = 1;
return function(){
console.log(a)
}
}
var r = fn();
r();
r(); function Once(fn, context) {
return function () {
fn.apply(context || this, arguments);
runOnce = null;
}
}

三.函数式编程基础

1.纯函数:相同输入永远得到相同输出,并没有任何副作用,其是描述输入输出的关系;

面向对象语言的问题是,它们永远都要随身携带那些隐式的环境。你只需要一个香蕉,但却得到一个拿着香蕉的大猩猩...以及整个丛林

by Erlang 作者:Joe Armstrong

所以纯函数的好处:

可缓存(Cacheable)

可测试 (Testable)

并行处理 (Parallel Code)

可移植性/自文档化(Portable / Self-Documenting)

function fn(a){
return function(b){
return a + b;
}
}
var n = fn(20)
n(20)// 40
n(20)// 40
//纯函数可缓存
function fn(a){
let obj = {};
return function(b){
let key = b.toString();
return obj[key]? obj[key]:(obj[key]=a + b,console.log('相同输入执行一次'),obj[key]);
}
}
var n = fn(20)
n(20)// 40
n(20)// 40
n(30)// 50
n(30)// 50

纯函数的副作用会让纯函数变得不纯,副作用产生来源来自外部交互/数据;副作用不能完全禁止;

副作用是在计算的过程中,系统状态的一种变化,或者与外部世界进行的可观察的交互。

只要是跟函数外部环境发生的交互就都是副作用,这一点可能会让你怀疑无副作用编程的可行性。

函数式编程的哲学就是假定副作用是造成不正当行为的主要原因。

Shared mutable state is the root of all evil

共享可变状态是万恶之源

by Pete Hunt

//不纯的函数 保留计算中间的结果
function fn(a){
var res = 0;
return function(b){
res += a + b;
return res;
}
}
var n = fn(20)
n(20)// 40
n(20)// 80
n(20)// 120
// 不纯的函数 引用外部数据
var a = 2;
function fn(n){
return n > a;
// 这里 a 就是副作用来源
}
// 修改即
function fn(n){
var a = 2;
return n > a;
// 虽然改成纯函数了但引发了硬编码问题
}
// 再次修改 此处用高阶函数
function fn(base){
return function(n){
return base > n
}
}

2.珂里化:当一个函数有多个参数时可以先传递一部分参数(这部分参数不再改变)并返回新的函数用于接收剩余参数 并返回计算结果 柯里化强调的是生成单元函数,部分函数应用强调的固定任意元参数,而我们平时生活中常用的其实是部分函数应用,这样的好处是可以固定参数,降低函数通用性,提高函数的适合用性。

珂里化函数也是高阶函数

降维处理 转化为一元函数 粒度小更灵活

function test(a,b,c){
console.log(a,b,c)
}
function curry(fn){
return function fn1(...a){
if(arguments.length >= fn.length)return fn(...a)
return function(...b){
return fn1(...a.concat(b))
}
}
}
var s = curry(test);
s(1)(2)(3)// 纯珂里化
s(1)(2,4)// 部分函数应用 这里也是 高级珂里化
s(1,4)(2)// 部分函数应用 这里也是 高级珂里化

不难发现以上的curry函数其实不是一个真正的珂里化函数,如果你用过lodash.Ramda 这些库中实现的 curry 函数的行为会发现他们是一样的。其实,这些库中的 curry 函数都做了很多优化导致这些库中实现的柯里化其实不是纯粹的柯里化,我们可以把他们理解为“高级柯里化”。实现可以根据你输入的参数个数,返回一个柯里化函数/结果值。参数个数满足了函数条件,则返回值。这样可以解决一个问题,就是如果一个函数是多输入,就可以避免使用 s(1)(2)(3) 这种形式传参了。

我们可以用高级柯里化去实现部分函数应用,但是柯里化不等于部分函数应用。

3.函数组合(compose),解决洋葱代码 fn(fn1(fn2(fn3(x)))); 默认从右到左执行

function fn(a){
return a + 2
}
function fn1(a){
return a * 2
}
function fn2(a){
return a * a
}
function compose(...a){
a.reverse();
return function(v){
return f(a, v)()
}
function f(arr, v){
var i = 0;
var d = v;
return function c(){
if(i >= arr.length)return d;
return (d = arr[i](d),i++, c());
}
}
}
var r = compose(fn1, fn2, fn)
r(2) // 64
r(4) // 144
r(3) // 100
console.log(r(2),r(3),r(4))
// 函数组合要满足结合律即数学中的结合律
var r = compose(fn2, compose(fn1, fn))
r(2) // 64
r(4) // 144
r(3) // 100
var r = compose(compose(fn2, fn1), fn)
r(2) // 64
r(4) // 144
r(3) // 100
// 函数组合调试 借用辅助函数即可
var log = function(tar, v){
console.log(tar, v)
return v
}
var r = compose(fn1,log('fn2函数处理结果:'), fn2, log('fn函数处理结果:'), fn)
r(5)
4.pointfree 是一种编程风格 组合函数就是pointfree风格

不需要指明处理的数据

只需要合成运算过程

需要定义一些辅助的基本函数

函数式编程就是把运算过程抽象成函数 而pointfree是在此基础上在合成新的函数

// 非pointfree
function fn(v){
return v.split('').reverse().join('')
}
// pointfree
function split(v, reg){
return v.split(reg)
}
function reverse(v){
return v.reverse()
}
function join(v, reg){
return v.join(reg)
}
var f1 = curry(split) // 珂里化函数,
var f2 = curry(join)
var r = compose(f2(''), reverse, f1('')) // 函数组合

4.函子(functor),函子是函数式编程里面最重要的数据类型,也是基本的运算单位和功能单位。 函子首先是一容器,包含了值和变形关系。比较特殊的是,它的变形关系可以依次作用于每一个值,将当前容器变形成另一个容器。就是将一个容器转成另一个容器;

函子是一个特殊的对象,其对外提供一个map方法对值进行操作 该方法接受一个纯函数作为参数

可以链式调用

class container{
constructor(v){
this._v = v
}
map(fn){
return new container(fn(this._v))
}
}
var r = new container(2).map(function(v){
return v + 2
}).map(function(v){
return v * v
});
console.log(r)
// 此时 r是一个函子对象 {_v: 16} 函子将数据包装在内部不对外公布 若对值进行操作/使用在map中完成;
// 将new 也封装在函子内部
class container{
static of(v){
return new container(v)
}
constructor(v){
this._v = v
}
map(fn){
return container.of(fn(this._v))
}
}
var r = container.of(2).map(function(v){
return v + 2
}).map(function(v){
return v * v
});
console.log(r)
// {_v: 16}
// 函子 null || undefined 问题 var r = container.of(null)
.map(function(v){
return v + 2
})
.map(function(v){
return v * v
});
console.log(r)
// error ---- Uncaught SyntaxError: Unexpected token 'null'

以上代码最后 null 引发了副作用 使得map的参数函数变成非纯函数, 纯函数是相同的输入始终有相同的输出; 而这里直接抛出异常了.

MayBe 函子 处理异常空值 上面问题 由MayBe函子来捕获处理

class MayBe{
static of(v){
return new MayBe(v)
}
constructor(v){
this._v = v;
}
map(fn){
return this.isNull()? MayBe.of(this._v):MayBe.of(fn(this._v))
}
isNull(){
return this._v === null || this._v === undefined;
}
}
var r = MayBe.of(null)
.map(function(v){
return v + 2
});
console.log(r)
// {_v: null}
// 此时maybe函子虽然解决了异常空值但延伸了另一个问题
var r = MayBe.of(null)
.map(v => v+2)
.map(function(v){
return null;
})
.map(function(v){
return v + 2
})
console.log(r)
//{_v: null}

MayBe函子虽然处理了空值异常,但捕获不到异常具体信息,无法定位哪个函子出错;

Either 函子 类似if...else..,下面示例将处理捕获异常信息,及定位出错函子;

class left {
static of(v){
return new left(v)
}
constructor(v) {
this._v = v
}
map(fn){
return this
}
}
class right {
static of(v){
return new right(v)
}
constructor(v) {
this._v = v
}
map(fn){
return right.of(fn(this._v))
}
}
function test(v){
try{
return right.of(JSON.parse(v))
}catch(e){
return left.of({ error: e.message})
}
}
var r = test('{a": "1"}')
.map(x => x.a+2)
console.log(r)
// {_v: {error: "Unexpected token a in JSON at position 1"}}

IO函子: input output 惰性执行不纯的操作,使当前函数变为纯函数,其实就是通过compose组合函数根据map调用次数依次组合成一个纯函数返回

class IO {
static of(v){
return new IO(function(){
return v
})
}
constructor(fn) {
this._v = fn
}
map(fn){
return new IO(compose(fn, this._v))
}
}
var r = IO.of(location).map(l => l.href)
console.log(r)
// {_v: function}
// _v就是组合函数生成的新的纯函数 需要调用的话 可以直接 r._v(); 但是这样就不符合属性私有化了 更重要的是如果函子嵌套就要不停的._v()
var io1 = function(x){
return new IO(function(){
return x * 2
})
}
var io2 = function(x){
return new IO(function(){
return x
})
}
var comIo = compose(io2, io1);
var r = comIo(2)._v()._v()

monad 函子 解决函子嵌套问题

如果一个函子具有join,map两个方法并遵守一些定律 就是一个monad

class IO {
static of(v){
return new IO(function(){
return v
})
}
constructor(fn) {
this._v = fn
}
map(fn){
return new IO(compose(fn, this._v))
}
join(){
return this._v()
}
}
var r = io1(2)
.map(io2)
.join()
.join()
console.log(r) // 4
// 虽然封装进了join方法但还要去一次调用,还可以在改进下
class IO {
static of(v){
return new IO(function(){
return v
})
}
constructor(fn) {
this._v = fn
}
map(fn){
return new IO(compose(fn, this._v))
}
join(){
return this._v()
}
flatMao(fn){
return this.map(fn).join()
}
}
var r = io1(2)
.flatMao(io2)
.join()
console.log(r)// 4
// flatMao 是处理返回值是函子的情况 而map 则是处理数据
var r = io1(2)
.map(x => x + 2)
.flatMao(io2)
.map(v => v * v)
.join()
console.log(r) // 36

pointed 函子 指实现了of静态方法的函子 以上函子均属于pointed函子;

函数式编程往往会导致函数过度包装,影响性能

为减少函数副作用也会使资源占用不能及时释放 使得 Garbage Collection 压力增加

函数式编程尾递归使用频繁,不利于编译器优化

javascript函数式编程基础随笔的更多相关文章

  1. 转:JavaScript函数式编程(三)

    转:JavaScript函数式编程(三) 作者: Stark伟 这是完结篇了. 在第二篇文章里,我们介绍了 Maybe.Either.IO 等几种常见的 Functor,或许很多看完第二篇文章的人都会 ...

  2. 转: JavaScript函数式编程(二)

    转: JavaScript函数式编程(二) 作者: Stark伟 上一篇文章里我们提到了纯函数的概念,所谓的纯函数就是,对于相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用,也不依赖外部环 ...

  3. 转:JavaScript函数式编程(一)

    转:JavaScript函数式编程(一) 一.引言 说到函数式编程,大家可能第一印象都是学院派的那些晦涩难懂的代码,充满了一大堆抽象的不知所云的符号,似乎只有大学里的计算机教授才会使用这些东西.在曾经 ...

  4. JavaScript 函数式编程读书笔记2

    概述 这是我读<javascript函数式编程>的读书笔记,供以后开发时参考,相信对其他人也有用. 说明:虽然本书是基于underscore.js库写的,但是其中的理念和思考方式都讲的很好 ...

  5. JavaScript 函数式编程读书笔记1

    概述 这是我读<javascript函数式编程>的读书笔记,供以后开发时参考,相信对其他人也有用. 说明:虽然本书是基于underscore.js库写的,但是其中的理念和思考方式都讲的很好 ...

  6. Scala_函数式编程基础

    函数式编程基础 函数定义和高阶函数 函数字面量 字面量包括整数字面量.浮点数字面量.布尔型字面量.字符字面 量.字符串字面量.符号字面量.函数字面量和元组字面量. scala> val i = ...

  7. 大数据技术之_16_Scala学习_04_函数式编程-基础+面向对象编程-基础

    第五章 函数式编程-基础5.1 函数式编程内容说明5.1.1 函数式编程内容5.1.2 函数式编程授课顺序5.2 函数式编程介绍5.2.1 几个概念的说明5.2.2 方法.函数.函数式编程和面向对象编 ...

  8. 一文带你了解JavaScript函数式编程

    摘要: 函数式编程入门. 作者:浪里行舟 Fundebug经授权转载,版权归原作者所有. 前言 函数式编程在前端已经成为了一个非常热门的话题.在最近几年里,我们看到非常多的应用程序代码库里大量使用着函 ...

  9. javascript函数式编程和链式优化

    1.函数式编程理解 函数式编程可以理解为,以函数作为主要载体的编程方式,用函数去拆解.抽象一般的表达式 与命令式相比,这样做的好处在哪?主要有以下几点: (1)语义更加清晰 (2)可复用性更高 (3) ...

随机推荐

  1. linux块设备驱动---概念与框架(转)

    基本概念   块设备(blockdevice) --- 是一种具有一定结构的随机存取设备,对这种设备的读写是按块进行的,他使用缓冲区来存放暂时的数据,待条件成熟后,从缓存一次性写入设备或者从设备一次性 ...

  2. js拖拽上传 文件上传之拖拽上传

    由于项目需要上传文件到服务器,于是便在文件上传的基础上增加了拖拽上传.拖拽上传当然属于文件上传的一部分,只不过在文件上传的基础上增加了拖拽的界面,主要在于前台的交互, 从拖拽的文件中获取文件列表然后调 ...

  3. Docker-V 详解

      1. 作用 挂载宿主机的一个目录. 2. 案例 譬如我要启动一个centos容器,宿主机的/test目录挂载到容器的/soft目录,可通过以下方式指定:   # docker run -it -v ...

  4. centos 7.8 添加磁盘后查看、分区、格式化、挂载

    基础环境 公有云 由于磁盘空间快用完了,现在决定多加一个40G磁盘 第一步 分区 fdisk -l #查看当前磁盘信息 fdisk /dev/vdb #对指定磁盘进行操作 如上图一般磁盘的第一个分区都 ...

  5. jquery1.9+,jquery1.10+ 为什么不支持live方法了?

    live() 替换成 on() die()  替换成off() 根据jQuery的官方描述,live方法在1.7中已经不建议使用,在1.9中删除了这个方法.并建议在以后的代码中使用on方法来替代. o ...

  6. 使用Navicat远程连接阿里云ECS服务器上的MySQL数据库

    一.必须给服务器的安全组规则设置端口放行规则,在管理控制台中设置: 之后填写配置,授权对象是授权的IP,其中0.0.0.0/0为所有IP授权,之后保存; 二.Navicat使用的配置 在编辑连接处,要 ...

  7. C#语言下使用gRPC、protobuf(Google Protocol Buffers)实现文件传输

    初识gRPC还是一位做JAVA的同事在项目中用到了它,为了C#的客户端程序和java的服务器程序进行通信和数据交换,当时还是对方编译成C#,我直接调用. 后来,自己下来做了C#版本gRPC编写,搜了很 ...

  8. slf4j -->log4j --> logback -->log4j2

    slf4j是一个接口:log4j\logback\log4j2是slf4j接口的持续更新的日志框架实现类:按照面向接口编程,java中导入slf4j最好,可以持续更新日志框架实现类. 详细情况见链接 ...

  9. 使用contentProvider

    内部利用contentProvider暴露接口供外部查询删除操作,外部查询删除使用contentResolver,首先使用sqlite创建一个数据库表student,然后使用contentProvid ...

  10. leetcode学习总结

    转自https://leetcode-cn.com/ 1.两数之和 给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标. 你可以 ...