大家好,我是指针。冬天到了,人也变懒了,为了让自己动起来,我报名参加了拉勾教育的大前端高薪训练营。学习需要总结,需要分享,需要鞭策,于是便有了《针爱学前端》这一系列,希望大家看完能够有收获。如果文章中有不对的地方,希望能批评指正,不吝赐教!!!
函数式编程是甚么
今天突然有人问我,指老师发生甚麽事啦,我说怎么回事,给我发了一段视频,我一看!嗷!源赖氏函数式编程。我啪的一下就解释了起来,很快啊!
其实说来也简单,函数式编程是一种编程的思想,注重于对运算过程的抽象。类似于数学函数之间的映射:y = f(x),我只知道我输入x,要始终得到y,其中的计算过程抽象成f函数。
这么说还是有点绕啊,通俗的说,你需要计算a+b的和,c+d的和,你发现自己需要写两遍一模一样的相加逻辑,于是你选择将相加的逻辑写成了一个方法叫add。这样以后就算叫你计算e+f的和你也可以直接调用add方法,这其实就是函数式编程的思想,我们有时在工作中早就无形的使用到了它,只是不知道如何去描述它而已。
// 非函数式
let num1 = 1
let num2 = 2
let sum = num1 + num2
console.log(sum)
// 函数式
// 我们将计算过程封装在了一个方法里,add(x)的每次结果必然是相同的
// 但是我们却不必每次写x=>y的逻辑,因为我们已经抽象成立方法add
function add (a, b) {
return a + b
}
let sum = add(1, 2)
cosole.log(sum)
为什么要学习函数式编程
- 用的越来越多,
react,vue3都在用,用了都说好! - 打包过程可以更好的利用
tree shaking过滤无用代码(深了啊)。 - 生态也不错,有很多现成的库使用,如
lodash、uderscore、ramda。 - 代码重用率高,开发快速。
- 函数式编程不依赖、也不会改变外界的状态,只要给定输入参数,返回的结果必定相同。因此,每一个函数都可以被看做独立单元,很有利于进行单元测试和除错,以及模块化组合。
- 函数式编程不修改变量,所以多个方法可以同时执行,而不必担心一个方法的数据被另一个方法修改,也就是方便并行处理。
函数式编程的知识点
-
函数是一等公民
如果经常刷题的小伙伴应该下面的题目不陌生,这道题考的就是
JavaScript中函数是一等公民的知识。// 问打印什么 var a = 1 console.log(a) var a = function () { }函数是一等公民的三个特性
-
函数可以储存在变量中
-
函数可以作为参数
-
函数可以作为返回值
// 在JavaScript中,函数就是一个普通对象(可以通过new Function()生成) // 既然是变量,那便可以赋值给变量,作为参数,作为返回值 // 把函数赋值给变量 let print = function (x) { console.log(x) } print("print") // 把函数作为参数(模拟foreach) function myForEach (array,fn) { for(let item of array) { fn(item) } } let arr = ["my", "name", "is", "zhizhen"] myForEach(arr, print) // 把函数作为返回值 function returnFn () { let msg = "hello lagou" return print(msg) }
-
-
高阶函数(Higher-order function)
什么是高阶函数
- 参数可以是函数
- 返回值可以是函数
高阶函数的意义
抽象的说就是:帮助我们抽象函数实现的细节,只需要我们关注输入与输出
通俗的说就是:你需要遍历1个数组,然后打印,你只需要将打印的方法作为参数传到一个新的方法里,那么那个新的方法里就不需要再关注打印的方法了,只需要关注传入的数组就行。
// 面向过程编程 let array = [1, 2, 3, 4] for(let item of array) { console.log(item) } // 高阶函数 function print (item) { cosole.log(print) } function map (arr, fn) { for(let item of arr) { fn(item) } } map(array, print)es6中有许多高阶函数forEachmapfilter- ...
-
闭包(Closure)
闭包,作为一个
JavaScript的特性,是面试官最爱问的问题之一,关于它的总结网上有好多,我也说说我的总结,不一定百分百准确。A函数返回了B函数,在B函数中访问了A函数作用域中的参数,导致A函数一直无法被销毁。这样的一种情况就是闭包。
// 借用闭包,我们可以实现一个一次性函数,不需要使用外部变量的那种 function once (fn) { let done = false return function () { if(!done) { done = true // apply的第二个参数为数组,fn.apply()可以使fn使用匿名函数传递的参数 // 当然,如果使用es6的语法就不用这么麻烦了 return fn.apply(this, arguments) } } // es6语法返回 // return (...args) => { // if(!done) { // done = true // return fn(...args) // } // } } let oncePrint = once(print) // 只会打印一次 // 因为once中的done在第一次过后便被置为true,且因为返回的匿名函数一直在使用done // 导致once无法被回收,done一直是true oncePrint("hello lagou") oncePrint("hello lagou")上一个例子,done是once里定义的,我们也可以活用它,看下面的例子
// 现在过年了,你需要群发拜年短信,现在有两种人,一种是上司,一种是下属 // 两者发的短信内容是不一样的 // 让我们用闭包去模拟一下 function sayHi (type) { let msg = "" if(type == 1) { msg = "您忠诚的下属指针祝您新的一年万事大吉!" }else { msg = "还不快来给我拜年,小兔崽子" } return function (name) { return name + msg } } let toBoss = sayHi(1) let toSmallRabbitBaby = say(2) toBoss("张三") toBoss("李四") toSmallRabbitBaby。。。我忘了我没有下属了,再见:(
函数式编程的实现
-
纯函数
-
纯函数的概念
相同的输入永远会得到相同的输出,而且没有任何可观察的副作用,这样的函数就是纯函数。
let arr = [1, 2, 3, 4] // slice函数就是纯函数 // 它执行多少次都会返回一样的数组 arr.slice(0, 3) // splice不是纯函数 // 它有一个可观察的副作用:改变了原数组。 // 即使[1, 2, 3, 4].splice(0, 3)每次返回的数组都一样 arr.splice(0, 3) -
纯函数的好处
相同的输入,永远会得到相同的输出,所以我们可以将输出结果缓存起来,当遇到相同的请求时,直接返回缓存中的结果即可,节省资源。
函数式编程的好处里提到的方便测试以及并行处理,就是因为纯函数不依赖于外部参数,也不改变外部参数。
// 手写一个缓存函数 function memoize (f) { let caches = {} return function () { let argString = JSON.stringify(arguments) console.time("memoize") caches[argString] = caches[argString] || f.apply(this, arguments) console.timeEnd("memoize") return caches[argString] } } memoize(5) memoize(5) -
副作用
副作用就是会导致纯函数不纯的作用,一个是依赖外部参数,一个是改变外部参数
// 依赖外部参数 // 如果外部参数num发生改变,checkAge的输出也会发生改变 let num = 18 function checkAge (age) { return age > num } // 改变外部参数 // 改变了外部参数arr,如果此时另一个线程在使用或者改变arr,就会发生冲突 arr.splice(0, 3)副作用无法完全禁止,我们将在函子中学会如何尽可能控制副作用在可控范围内发生
-
-
柯里化(Curry)
当一个函数有多个参数时,可以将它转化成先接受一部分参数,并且固化,以后永远不变,再返回一个新的函数用于接收剩下的参数。
例子:
add(1, 2, 3) => curryadd(1)(2)(3)让我们使用柯里化来控制一些副作用吧
// 上例中的checkAge方法可以先优化成一般的纯函数 // 当然存在硬编码问题(num初始就赋值) function checkAge (age) { let num = 18 return age > num } // 使用柯里化 function curryCheckAge (num, age) { return function (age) { return age > num } } // es6的写法 let curryCheckAge = num => (age => age > num) // 先传入num,并且以后保持不变,再返回一个新的函数,接受剩下的参数 let checkAge18 = curryCheckAge(18) checkAge18(20)看上去似乎很简单,但是现在的问题是,函数的参数数量不一定是两个,有可能是3个,4个或者更多,这时候就需要一个真正的柯里化方法,当然,你可以使用
lodash等库里现成的方法,这里我们自己实现一个function myCurry (fn) { let paramsList = [] return function curryFn(...args1) { // 比较传入的参数个数和fn应该传入的参数个数比较 // 如果相等,直接执行fn(...args) // 如果小于,需要再返回一个新的函数,合并参数,比较参数个数 if(args1.length < fn.length) { return function (...args2) { return curryFn(...args1.concat(...args2)) } } return fn(...args1) } } function add (a, b, c) { return a + b + c } let curryAdd = myCurry(add) console.log(curryAdd(1)(2)(3)) console.log(curryAdd(1, 2)(3)) console.log(curryAdd(1)(2, 3))总结
- 柯里化实际是对函数参数的一种缓存
- 可以使函数颗粒度更小
- 可以把多元函数转换成一元函数,用于下面的函数组合
-
函数组合
函数组合可以让我们把细粒度的函数组合成一个新的函数,每个函数的返回作为下一个函数的参数传入
例子:
h(g((x)))=> compose(h, g, f)(x)函数的组合还要符合结合律:
compose(h,g,f) === compose(h, compose(g, f))这个函数组合呢,可以使用已有库里的,当然也可以自己撸一个,本着自己动手丰衣足食的理念,开整
// 利用了es6的reduce语法 // fns反转是为了从右往左执行,更符合我们的习惯 function myCompose (...fns) { return function (value) { return fns.reserve().reduce(function(acc, fn) { return fn(acc) }, value) } } // 如果不用reduce function myCompose (...fns) { return function (value) { let r = value fns = fns.reverse() while (fns.length) { r = fns[0](r) fns.shift() } return r } } -
Point Free
无值风格:不需要多个运算的中间值,只合成运算过程
将多个函数组成一个复杂的运算,中间的数据不需要知道
// 将一个数先加一,再取平方,再减去100 // 我们不需要知道x+1为多少,也不需要x+1的平方是多少 // 直接将运算合在一起 let addAndSquare = myCompose(minus, squre, add) addAndSquare(5) -
函子(Functor)
之前纯函数时提到了副作用,函子就是控制副作用的
-
什么是函子
-
函子是函数式编程里面最重要的数据类型,也是基本的运算单位和功能单位。
-
函子是一个特殊的容器,通过一个普通的对象实现,这个对象里拥有一个map方法来处理值,理论上拥有map方法的对象都可以是函子。
-
函子里的值我们不直接操作,而是通过map方法传递一个处理值的函数
-
map最终返回一个新的函子,包含新的值
-
-
Functor函子
class Container { constructor (value) { this._value = value } map (fn) { return new Container(fn(this._value)) } } -
Pointed函子
Pointed函子实现了of静态方法,用于初始化对象,避免使用new来创建对象,因为new是面向对象编程思想,与函数式编程思想不符。
class Container { static of (value) { return new Container(value) } constructor (value) { this._value = value } map (fn) { return Container.of(fn(this._value)) } } -
MayBe函子MayBe函子就是处理空值的情况,控制副作用在允许的范围内class MayBe { static of (value) { return new MayBe(value) } constructor (value) { this._value = value } map (fn) { return this.isNull() ? MayBe.of(null) : MayBe.of(fn(this._value)) } isNull () { return this._value === null || this._value === undefined } } -
IO函子
- IO函子中的_value是一个函数,这里将函数作为值来处理
- IO函子将不纯的操作存储到_value中,延迟执行,最后调用的时候再执行
- 如果IO初始包裹的是一个对象,则使用()=> x ,使得IO包裹的为一个函数
class IO { constructor (fn) { this._value = fn } static of (x) { return new IO(x) } map (fn) { return new IO(myCompose(fn, this)) } } let r = IO.of(() => process).map(p => p.execPath) let r2 = IO.of(readFile()) -
Monad单子
Monda用于处理函子嵌套,它可以使函子扁平化它的重要作用就是实现IO操作
class Monda extends IO { join () { return this._value() } chain (fn) { // 如果产生函子嵌套,就执行一次,取出之前函子的值,保证始终是个单层的函子 return this.map(fn).join() } } // 读取文件操作和打印文件操作都是不纯的,因为他们依赖于读取的文件本身的内容 // 所以我们将这两个方法包裹在函子中,使它们变纯 function readFile (filename) { return Monda.of(function () { return fs.readFileSync(filename, 'utf-8') }) } function print (x) { return Monda.of(function () { return x }) } let r = readFile('package.json') .chain(print) .join()
-
导航
针要学前端 | JavaScript深度挖掘之手写Promise
针要学前端 | JavaScript深度挖掘之ECMAScript
参考
以上皆由拉勾教育大前端训练营提供材料😀