JavaScript函数式编程实现介绍
作者:ExMaterial
为什么要学习函数式编程
Vue进入3.*(One Piece 海贼王)世代后,引入的setup语法,颇有向老大哥React看齐的意思,说不定前端以后还真是一个框架的天下。话归正传,框架的趋势确实是对开发者的js功底要求更为严格了,无论是hooks、setup,都离不开函数式编程,抽离代码可复用逻辑,更好地组织及复用代码,有一点我感到很高兴的是,终于可以抛弃烦人的this了,当然,这也不是我为偷懒而生出这样的感想,人家道格拉斯老爷子可是在他的新书《JavaScript悟道》里极力吐槽了一下this,所以,也算是像js大佬看齐了。所以,要想不被前端日新月异的新技术给冲昏头脑,还是适时回来重学一下JavaScript吧。
什么是函数式编程
函数式编程(Functional Programming, FP),FP 是编程范式之一,我们常听说的编程范式还有面向过程编程、面向对象编程。
面向对象编程:面向对象有三大特性,通过封装、继承和多态来演示事物之间的联系,如果更宽泛来说,抽象也应该算进去,但是由于面向对象的本质就是抽象,其不算是三大特性也不为过。
函数式编程:函数式编程的思想主要就是对运算过程进行抽象,它更像一个黑盒,你给入特定的输出,进过黑盒运算后再返回运算结果。你可以将其理解为数学中的y = f(x)。
- 程序的本质:根据输入进行某种运算得到相应的输出。
- x -> f(联系、映射) -> y, y = f(x)
- 函数式编程中的函数其实对应数学中的函数,即映射关系。
- 相同的输入始终要得到相同的输出(纯函数)
- 可复用
前置知识
函数是一等公民
作为一名有一定经验的前端开发者,你一定对JavaScript中“函数是一等公民”这一说法不陌生。
这里给出权威文档MDN的定义:当一门编程语言的函数可以被当作变量一样用时,则称这门语言拥有头等函数。例如,在这门语言中,函数可以被当作参数传递给其他函数,可以作为另一个函数的返回值,还可以被赋值给一个变量。
函数可以储存在变量中
let fn = function() { console.log('Hello First-class Function') } fn()
函数作为参数
function foo(arr, fun) { for (let i = 0; i < arr.length; i++) { fun(arr[i]) } } const array = [1, 2, 3, 4] foo(array, function(a) { console.log(a) })
函数作为返回值
function fun() { return function () { consoel.log('哈哈哈') } } const fn = fun() fn()
高阶函数
什么是高阶函数
高阶函数
- 可以把函数作为参数传递给另外一个函数
- 可以把函数作为另外一个函数的返回结果
函数作为参数(为了避免文章篇幅过长,后面的演示代码就不给出测试代码了,读者可自行复制文章代码在本地编辑器上调试)
function filter(array, fn) { let results = [] for (let i = 0; i < array.length; i++) { if (fn(array[i])) { results.push(array[i]) } } return results } // 测试 let arr = [1, 3, 4, 7, 8] const results = filter(arr, function(num) { return num > 7 }) console.log(results) // [8]
函数作为返回值
// 考虑一个场景,在网络延迟情况下,用户点击支付,你一定不想要用户点完支付没反应后点击下一次支付再重新支付一次,不然,你的公司就离倒闭不远了。 // 所以考虑一下once函数 function once(fn) { let done = false return function() { if (!done) { done = true return fn.apply(this, arguments) } } } let pay = once(function (money) { console.log(`支付: ${money} RMB`) }) pay(5) pay(5) pay(5) pay(5) // 5
使用高阶函数的意义
- 抽象可以帮我们屏蔽细节,只需要关注目标
- 高阶函数是用来抽象通用的问题
常用高阶函数
- forEach(已实现)
- map
const map = (array, fn) => { let results = [] for (let value of array) { results.push(fn(value)) } return results }
- filter
- every
const every = (array, fn) => { let result = true for (let value of array) { result = fn(value) if (!result) { break } } return result }
- some
const some = (array, fn) => { let result = false for (let value of array) { result = fn(value) if (result) { break } } return result }
- find/findIndex
- reduce
- sort
闭包
闭包 (Closure):函数和其周围的状态(词法环境)的引用捆绑在一起形成闭包。
闭包的本质:函数在执行的时候会放到一个执行栈上当函数执行完毕之后会从执行栈上移除,但是堆上的作用域成员因为被外部引用不能释放,因此内部函数依然可以访问外部函数的成员。
function makePower(power) { return function (num) { return Math.pow(num, power) } } // 求平方及立方 let power2 = makePower(2) let power3 = makePower(3) console.log(power2(4)) // 16 console.log(power2(5)) // 25 console.log(power3(4)) // 64
function maekSalary(base) { return function (performance) { return base + performance } } let salaryLevel1 = makeSalary(12000) let salaryLevel2 = makeSalary(15000) console.log(salaryLevel1(2000)) // 14000 console.log(salaryLevel2(3000)) // 18000
其实上面这两个函数都是差不多的,都是通过维持对原函数内部成员的引用。具体可以通过浏览器调试工具自行了解。
纯函数
纯函数概念
纯函数:相同的输入永远会得到相同的输出
lodash 是一个纯函数的功能库,提供了对数组、数字、对象、字符串、函数等操作的一些方法。有人可能会有这样的疑惑,随着ECMAScript的演进,lodash中很多方法都已经在ES6+中逐步实现了,那么学习其还有必要吗?其实不然,lodash中还是有很多很好用的工具函数的,比如说,防抖节流是前端工作中经常用到的,你可不想每次都手写一个函数吧?更何况没有一点js功底还写不出来呢。
话归正传,来看看数组的两个方法:slice和splice。
- slice 返回数组中的指定部分,不会改变原数组
- splice 对数组进行操作返回该数组,会改变原数组
let array = [1, 2, 3, 4, 5] // 纯函数 console.log(array.slice(0, 3)) console.log(array.slice(0, 3)) console.log(array.slice(0, 3)) // 不纯的函数 console.log(array.splice(0, 3)) console.log(array.splice(0, 3)) console.log(array.splice(0, 3))
纯函数的好处
可缓存
因为纯函数对相同的输入始终有相同的结果,所以可以把纯函数的结果缓存起来
function getArea(r) { console.log(r) return Math.PI * r * r } function memoize(f) { let cache = {} return function() { let key = JSON.stringify(arguments) cache[key] = cache[key] || f.apply(f, arguments) return cache[key] } } let getAreaWithMemory = memoize(getArea) console.log(getAreaWithMemory(4)) console.log(getAreaWithMemory(4)) console.log(getAreaWithMemory(4)) // 4 // 50.26548245743669 // 50.26548245743669 // 50.26548245743669
可测试
纯函数让测试更方便
并行处理
在多线程环境下并行操作共享的内存数据很可能会出现意外情况
纯函数不需要访问共享的内存数据,所以在并行环境下可以任意运行纯函数 (Web Worker)
副作用
// 不纯的 let mini = 18 function checkAge (age) { return age >= mini } // 纯的(有硬编码,后续可以通过柯里化解决) function checkAge (age) { let mini = 18 return age >= mini }
副作用让一个函数变的不纯(如上例),纯函数的根据相同的输入返回相同的输出,如果函数依赖于外部的状态就无法保证输出相同,就会带来副作用。
柯里化
柯里化的概念:当一个函数有多个参数的时候先传递一部分参数调用它(这部分参数以后永远不变),然后返回一个新的函数接收剩余的参数,返回结果。
柯里化就可以解决上面代码中的硬编码问题
// 普通的纯函数 function checkAge(min, age) { return age >= min } // 函数的柯里化 function checkAge(min) { return function(age) { return age >= min } } // 当然,上面的代码也可以用ES6中的箭头函数来改造 const checkAge = (min) => (age => age >= min)
下面来手写一个curry函数
function curry(func) { return function curriedFn(...args) { if (args.length < func.length) { return function() { return curriedFn(...args.concat(Array.from(arguments))) } } return func(...args) } }
函数组合
看了这么多代码,你肯定会觉得函数里面有很多return看起来不是很好看,事实也确是如此,所以这就要引出函数组合这个概念。
纯函数和柯里化很容易写出洋葱代码 h(g(f(x)))
获取数组的最后一个元素再转换成大写字母, .toUpper(.first(_.reverse(array))) (这些都是lodash中的方法)
函数组合可以让我们把细粒度的函数重新组合生成一个新的函数
你可以把其想象成一根管道,你将fn管道拆分成fn1、fn2、fn3三个管道,即将不同处理逻辑封装在不同的函数中,然后通过一个compose函数进行整合,将其变为一个函数。
fn = compose(f1, f2, f3) b = fn(a)
Functor(函子)
什么是Functor
- 容器:包含值和值的变形关系(这个变形关系就是函数)
- 函子:是一个特殊的容器,通过一个普通的对象来实现,该对象具有 map 方法,map 行一个函数对值进行处理(变形关系)
// Functor 函子 一个容器,包裹一个值 class Container { constructor(value) { this._value = value } // map 方法,传入变形关系,将容器里的每一个值映射到另一个容器 map(fn) { return new Container(fn(this._value)) } } let r = new Container(5) .map(x => x + 1) .map(x => x * x) console.log(r) // 36
总结
- 函数式编程的运算不直接操作值,而是由函子完成
- 函子就是一个实现了 map 契约的对象
- 我们可以把函子想象成一个盒子,这个盒子里封装了一个值
- 想要处理盒子中的值,我们需要给盒子的 map 方法传递一个处理值的函数(纯函数),由这个函数来对值进行处理
- 最终 map 方法返回一个包含新值的盒子(函子)
可能你不习惯在代码中看到new关键字,所以可以在容器中实现一个of方法。
class Container { static of (value) { return new Container(value) } constructor(value) { this._value = value } map(fn) { return Container.of(fn(this._value)) } }
MayBe 函子
上面的代码中如果传入一个null 或 undefined的话,代码就会抛出错误,所以需要再实现一个方法
class MayBe { static of(value) { return new MayBe(value) } constructor(value) { this._value = value } map(fn) { return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value)) } isNothing() { return this._value == null // 此处双等号等价于this._value === null || this._value === undefined } }
你看下上面的代码,是不是健壮性就好一点了呢?
Either函子
在MayBe函子中,很难确认哪一步产生的空值问题。所以就有了Either
class Left { static of(value) { return new Left(value) } constructor(value) { this._value = value } map(fn) { return this } } class Right { static of(value) { return new Right(value) } constructor(value) { this._value = value } map(fn) { return Right.of(fn(this._value)) } } function parseJSON(str) { try { return Right.of(JSON.parse(str)) } catch (e) { return Left.of({ error: e.message }) } }
到此这篇关于JavaScript函数式编程实现介绍的文章就介绍到这了,更多相关JS函数式编程内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!