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. 关于 ECMAScript、JavaScript、ES6、ECMAScript 2015

    ECMAScript 是一种规范,而 JavaScript 是对规范的实现.ECMA 是标准化组织. 最早的 JavaScript 是由 Netscape 公司开发的,并提交给 ECMA 标准化组织, ...

  2. 一键同步,紧跟潮流——CODING 现已支持导入 GitHub 仓库

    为方便用户从 GitHub 快速迁移到 CODING 并开始使用,CODING 现已支持导入 GitHub 仓库.免去繁琐步骤,只需简单两步操作即可完成导入,让仓库静默同步,无缝衔接,平滑过渡:同时还 ...

  3. CMD/ENTROYPOINT区别

    CMD/ENTROYPOINT区别 相同点:都是指定一个容器:启动时要运行的命令 不同点(重点): CMD: dockerfile中可以有多个CMD指令,但是只有最后一个生效,CMD会被docker ...

  4. python数据清洗

    盖帽法 分箱法 简单随机抽和分层抽

  5. Presto在滴滴的探索与实践

    ​桔妹导读:Presto在滴滴内部发展三年,已经成为滴滴内部Ad-Hoc和Hive SQL加速的首选引擎.目前服务6K+用户,每天读取2PB ~ 3PB HDFS数据,处理30万亿~35万亿条记录,为 ...

  6. empty()和size() == 0有区别吗

    empty()和size() 这里说的empty()和size()都是STL的容器中提供的接口,分别用来判断当前容器是否为空和获取当前包含的元素个数 区别 其实按道理来说两者应该是相等的,而且STL容 ...

  7. python 不定长参数

    1 #不定长参数 * 元祖 ** 字典 2 def item(a,b,*c,**d): 3 print(a) 4 print(b) 5 print(c) 6 print(d) 7 8 item(11, ...

  8. 一文读懂Redis常见对象类型的底层数据结构

    Redis是一个基于内存中的数据结构存储系统,可以用作数据库.缓存和消息中间件.Redis支持五种常见对象类型:字符串(String).哈希(Hash).列表(List).集合(Set)以及有序集合( ...

  9. 某次burp抓包出错的解决办法

    前些日子同事发微信问我一个问题 没听懂他说的没回显是啥意思,于是叫他把站发给我. 浏览器不挂burp代理能正常打开,挂上burp代理以后浏览器显示连接超时 首先测试burp能抓其他的包应不是这个原因 ...

  10. 记录Spring Boot 2.3.4.RELEASE版注解方式实现AOP和通知的执行顺序

    1.advice 按照以下的顺序执行 输出结果:(正常和异常) 说明:Spring boot 2.3.4.RELEASE 版本使用的AOP是spring-aop-5.2.9.RELEASE,AOP的通 ...