之前在网上看到了一篇教你如何用 js 写出装逼的代码
经过学些以及扩展很有收获在这里记录一下
原文章找不到了所以就不在这附上链接了
大家看下下面两段 js 代码
上面两端代码效果是一模一样的, 都是在一个指定的数组中, 找到指定的数字所在的下标第一个大多数人都看得懂第二个就不一定了
这里就带大家从一步步最初的版本演化到最终的函数式的版本
希望大家以后再遇到如此难以阅读的代码, 知道怎么去理解
为了从上面上面变到下面, 我们需要了解以下知识
箭头函数
三元运算符(不讲)
尾递归优化
匿名函数(不讲)
柯里化
高阶函数
其中, 尾递归优化, 和柯里化, 高阶函数, 都是函数式编程里面的东西所以有必要先简单介绍一下什么是函数式编程
这里先引用下知乎的图
上面的图并不是说, 最下面的最高级, 而是越靠下面的越费脑子
什么是函数式编程?
事实上函数式编程是从范畴论 (category theory) 发展过来的而范畴论其实和微积分, 数理逻辑, 等等都是一种数学理论而函数式编程只是参考这种思想发展过来的, 就像设计模式最初是来源于建筑学一样
为了更好的理解函数式编程, 这里也再简单的介绍下范畴论高深的我也不懂
在维基百科里面是这么定义的
也就是说只要是存在某种关系, 可以从一个对象转化为另外一个对象, 那么对象和它们之间的关系就构成一个范畴
下面的图是一个示意图
红色的点和黄色的箭头在一起就构成一个范畴
箭头 arrow, 还有一个正式的名字叫做态射 (morphism) 范畴论认为, 同一个范畴的所有成员, 就是不同状态的 "变形"(transformation)通过 "态射", 一个成员可以变形成另一个成员
上面的很抽象, 我们举例子说明下
大家都对面向对象的思想比较了解, 在面向对象的思想里, 万事万物都是对象, 而对象又可以抽象成为类比如, 黄种人, 白种人, 黑人都是对象, 可以抽象为人这个类这是他们有共性但是他们之间并不存在转化关系, 黑人不可能转化为白种人, 就算他整容整的很白, 概念上他还是个黑人
而数字 1,2,3,4,5 他们之间存在一定的关系, 1+1 可以变成 2,2+1 可变成 3,2+2 又可以变成 4 我们可以认为, 数字和它们之间的态射就是一个范畴每一个数字, 通过一个态势可以变为另外一个数字
所以范畴包含两部分
成员
关系
而过度到程序里面就是
值
函数
简单的来说就是, 一个值可以通过一个函数变为另外一个值所以函数式编程要求每一个函数必须是干净的, 进入一个值, 出去另外一个值不会操作任何方法外的数据
函数式编程有两个最基本的运算
合成
合成的概念就是如果一个值需要经过多个函数才能变成另外一个值, 就可以把两个函数合成一个函数比如 1, 需要经过 add1 和 add2 才能变成 4, 那么就可以合成一个 add3 出来
假设 add1=f,add2=g
上面看起来函数有点多, 那我们把 add1 和 add2 都给匿名了, 看起来会好一点
函数的合成还要满足结合律
假设 f,g,h 分别是 add1,add2,add3.
那么(h.g).f 就是 add3(add1(add2)) 而 h.(g.f) 就是 add1(add2(add3))
他们两者应该是相等的
2. 柯里化
f(x)和 g(x)合成为 f(g(x)), 有一个隐藏的前提, 就是 f 和 g 都只能接受一个参数如果可以接受多个参数, 比如 f(x, y)和 g(a, b, c), 函数合成就非常麻烦
这时就需要函数柯里化了所谓 "柯里化", 就是把一个多参数的函数, 转化为单参数函数
这里解释一下, 柯里化之后是采用的 js 里面的链式调用 AddX(2)其实返回的是 function(x)
{return x+2;} 这时候再跟一个(1), 就是把 1 传入里面执行了
当然函数式编程还有很多别的东西, 这里就不一一介绍了, 有兴趣的的可以自己查下
下面说下, 尾递归优化
我们知道递归的害处, 那就是如果递归很深的话, stack 受不了, 并会导致性能大幅度下降所以, 我们使用尾递归优化技术每次递归时都会重用 stack, 这样一来能够提升性能, 当然, 这需要语言或编译器的支持 Java 就不支持, 但是 javascript 支持而所谓的支持, 就是说编译器会自动优化, 对于尾递归的代码会自动优化成
普通递归
下面是入栈和出栈的过程会保存上一步的计算状态, 太深的话就会栈溢出
- fac(5)
- (5*fac(4))
- (5*(4*fac(3)))
- (5*(4*(3*fac(2))))
- (5*(4*(3*(2*fac(1)))))
- (5*(4*(3*2)))
- (5*(4*(6)))
- (5*24)
- 120
尾递归
当递归调用是整个函数体中最后执行的语句且它的返回值不属于表达式的一部分时, 这个递归调用就是尾递归
每次都是执行一个独立的函数, 和之前的函数并没有关联
- fac(5,1)
- fac(4,5)
- fac(3,20)
- fac(2,60)
- fac(1,120)
- 120
普通递归创建 stack 累积而后计算收缩, 尾递归只会占用恒量的内存只需要保存每次计算出来的值, 然后传入同一个函数就好
高阶函数
高阶函数就是函数当参数, 把传入的函数做一个封装, 然后返回这个封装函数上面一直都在用到
箭头函数
ECMAScript2015 引入的箭头表达式箭头函数其实都是匿名函数简单的理解就是通过箭头创建函数
一个参数时候可以省略小括号, 方法体只有一句的时候可以省略大括号
Function (x){return x+1}; 等价于 (x) =>{return x+1} 或者 x=>return x+1;
无参数必须有括号
Function (){return 1+1} 等价于 ()=>{return 1+1}
如果想加名字的话
Var add = ()=>{return 1+1}; 调用 add() 就会返回 2
该介绍的都介绍了, 下面就一步步改造
原始版本
尾递归优化之后
替换三元运算符
函数体内的函数参数化
转化为箭头函数
匿名
引入高阶函数, 并柯里化
为了方便调用再加上名字
下面断点图帮助理解
来源: http://www.bubuko.com/infodetail-2494931.html