【学习笔记】Part1·JavaScript·深度剖析-函数式编程与 JS 异步编程、手写 Promise(课前准备)
【学习笔记】Part1·JavaScript·深度剖析-函数式编程与 JS 异步编程、手写 Promise(一、函数式编程范式)
【学习笔记】Part1·JavaScript·深度剖析-函数式编程与 JS 异步编程、手写 Promise(二、JavaScript 异步编程)
【学习笔记】Part1·JavaScript·深度剖析-函数式编程与 JS 异步编程、手写 Promise(三、手写Promise源码)
【Part1作业】https://gitee.com/zgp-qz/part01-task
【学习笔记】Part1·JavaScript·深度剖析-ES 新特性与 TypeScript、JS 性能优化(一、ECMAScript 新特性)
【学习笔记】Part1·JavaScript·深度剖析-ES 新特性与 TypeScript、JS 性能优化(二、TypeScript 语言)
【学习笔记】Part1·JavaScript·深度剖析-ES 新特性与 TypeScript、JS 性能优化(三、JavaScript 性能优化1)
【学习笔记】Part1·JavaScript·深度剖析-ES 新特性与 TypeScript、JS 性能优化(四、JavaScript 性能优化2)
【Part2作业】https://gitee.com/zgp-qz/part02-homework
函数式编程
-
为什么要学习函数式编程
1.1 函数式编程是随着 React 的流行受到越来越多的关注
1.2 vue3 也开始拥抱函数式编程
1.3 函数式编程可以抛弃 this
1.4 打包过程中可以更好的利用 tree shaking 过滤无用的代码
1.4 方便测试,方便并行处理
1.5 有很多库可以帮助我们进行函数式开发:lodash、underscore、ramda
-
函数式编程的概念
函数式编程(Function Programming,FP),FP 是编程范式之一,常听说的编程范式还有面向过程编程和面向对象编程。
2.1 面向对象编程的思维方式:把现实世界中的事物抽象成程序世界中的类和对象,通过封装、继承和多态来演示事物事件的联系。
2.2 函数式编程的思维方式:把现实世界的事物和事物之间的联系抽象到程序世界(对运算过程进行抽象)
2.2.1 程序的本质:根据输入通过某种运算获得相应的输出,程序开发过程中会涉及很多有输入和输出的函数 2.2.2 x->f(联系、映射)->y,y=f(x) 2.2.3 函数式编程中的函数指的不是程序中的函数(方法),而是数学中的函数即映射关系,例如:y=sin(x),x 和 y 的关系 2.2.4 相同的输入始终要得到相同的输出(纯函数) 2.2.5 函数式编程用来描述数据(函数)之间的映射
//非函数式编程 let num1 = 2 let num2 = 3 let sum = num1 + num2 console.log(sum) //函数式编程 function add(n1, n2){ return n1 + n2 } let sum = add(2, 3) console.log(sum)
前置知识:
- 函数是一等公民
- 高阶函数
- 闭包
-
函数是一等公民
MDN 解释:
- 函数可以储存在变量中 - 函数作为参数 - 函数作为返回值
在 Javascript 中函数就是一个普通的对象(可以通过 new Function()),我们可以把函数存储到 变量/数组 中,它还可以作为另一个函数的参数和返回值,甚至我们可以在程序运行的时候通过 new Function(‘alert(1)’) 来构造一个全新的函数。
把函数赋值给变量。
//把函数赋值给变量 let fn = function(){ console.log('hello first-class function') } fn() //一个示例 const BlogController = { index(posts){return Views.index(posts)}, show(post){return Views.show(post)}, create(attrs){return Db.create(attrs)}, update(post,attrs){return Db.update(post,attrs)}, destroy(post){return Db.destroy(post)} } //优化 const BlogController = { index: Views.index, show: Views.show, create: Db.create, update: Db.update, destroy: Db.destroy }
-
高阶函数
什么是高阶函数
高阶函数(Higher-order function)
- 可以把函数作为参数传递给另一个函数 - 可以把函数作为另一个函数的返回结果
4.1 函数作为参数:
// 高阶函数-函数作为参数 // forEach function forEach(array, fn) { for (let i = 0; i < array.length; i++) { fn(array[i]) } } // 测试 let arr = [1, 3, 5, 7, 9, 11] forEach(arr, function (item) { console.log(item) }) // filter 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, 5, 7, 8, 9, 10, 11] let r = filter(arr, function (item) { return item % 2 === 0 }) console.log(r)
4.2 函数作为返回值:
// 高阶函数-函数作为返回值 function makeFn() { let msg = 'hello function' return function () { console.log(msg) } } //测试 // const fn = makeFn() // fn() // makeFn()() // once function once(fn) { let done = false return function () { if (!done) { done = true return fn(...arguments) // return fn.apply(this, arguments) // return fn.call(this, ...arguments) } } } let pay = once(function (money) { console.log(`支付:${money} RMB`) }) pay(5) pay(6) pay(2) pay(3) pay(9)
4.3 使用高阶函数的意义
- 抽象可以帮我们屏蔽细节,只需要关注与我们的目标 - 高阶函数是用来抽象通用的问题
// 面向过程的方式 let arr = [1, 3, 5, 7, 8, 9, 10, 11] for (let i = 0; i < arr.length; i++) { console.log(arr[i]) } // 高阶函数 let arr = [1, 3, 5, 7, 9, 11] forEach(arr, function (item) { console.log(item) }) let arr = [1, 3, 5, 7, 8, 9, 10, 11] let r = filter(arr, function (item) { return item % 2 === 0 }) console.log(r)
4.4 常用的高阶函数:
forEach | map | filter | every | some | find/findIndex | reduce | sort ...
// 模拟常用高阶函数:map、every、some // map const map = (array, fn) => { let results = [] for (let value of array) { results.push(fn(value)) } return results } // 测试 // let arr = [1, 2, 3, 4, 5] // arr = map(arr, v => v * v) // console.log(arr) // every 检测是否都满足条件 const every = (array, fn) => { let result = true for (let value of array) { result = fn(value) if (!result) { break } } return result } // 测试 // let arr1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 12, 14] // let arr2 = [11, 12, 14] // let bool1 = every(arr1, v => v > 10) // let bool2 = every(arr2, v => v > 10) // console.log(bool1,bool2) // some 检测是否有一个满足条件 const some = (array, fn) => { let result = false for (let value of array) { result = fn(value) if (result) { break } } return result } // 测试 let arr1 = [1, 3, 4, 5, 7, 8, 9] let arr2 = [1, 3, 5, 7, 9, 11] let bool1 = some(arr1, v => v % 2 === 0) let bool2 = some(arr2, v => v % 2 === 0) console.log(bool1) console.log(bool2)
-
闭包:
5.1 闭包(Closure):函数和其周围的状态(词法环境)的引用捆绑在一起形成闭包。
- 可以在另一个作用域中调用一个函数的内部函数并访问到该函数的作用域中的成员
闭包的本质:函数在执行的时候会放到一个执行栈上,当函数执行完毕之后会从执行栈上移除,但是堆上的作用域成员因为被外部引用不能释放,因此内部函数依然可以访问外部函数的成员
延长了外部函数内部成员的作用范围
5.2 闭包:案例
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>closure</title> </head> <body> <script> // Math.pow(4,2) // Math.pow(5,2) function makePower(power) { return function(number) { return Math.pow(number, power) } } // 求平方 let power2 = makePower(2) // 求立方 let power3 = makePower(3) console.log(power2(4)) console.log(power2(5)) console.log(power3(2)) </script> </body> </html>
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>closure</title> </head> <body> <script> // 假设求员工的工资 getSalary 基本工资 + 绩效工资 // getSalary(12000, 2000) // getSalary(15000, 3000) // getSalary(15000, 4000) // 为不同级别的员工生成一个计算总工资的函数,相同级别的基本工资是一样的 function makeSalary(base) { return function(performance) { return base + performance } } let salaryLevel1 = makeSalary(12000) let salaryLevel2 = makeSalary(15000) console.log(salaryLevel1(2000)) console.log(salaryLevel2(3000)) console.log(salaryLevel2(4000)) </script> </body> </html>
-
纯函数
纯函数:相同的输入永远会得到相同的输出,而且没有任何可观察的副作用。
- 纯函数就类似数学中的函数(用来描述输入和输出之间的关系),y=f(x)
lodash 是一个纯函数的功能库,提供了对数组、数字、对象、字符串函数等操作的一些方法
数组的 slice 和 splice 分别是:纯函数和不纯的函数- slice 返回数组中的指定部分,不会改变原数组 - splice 对数组进行操作,返回该数组,会改变原数组
// 纯函数和不纯的函数 // slice / splice let array = [1, 2, 3, 4, 5] //纯函数 console.log(array.slice(0, 3)) // [ 1, 2, 3 ] console.log(array.slice(0, 3)) // [ 1, 2, 3 ] console.log(array.slice(0, 3)) // [ 1, 2, 3 ] console.log(' --- 分割线 --- ') //不纯的函数 console.log(array.splice(0, 3)) // [ 1, 2, 3 ] console.log(array.splice(0, 3)) // [ 4, 5 ] console.log(array.splice(0, 3)) // [] // 纯函数 function getSum(n1, n2) { return n1 + n2 } console.log(getSum(1, 2)) console.log(getSum(1, 2)) console.log(getSum(1, 2))
函数式编程不会保留计算中的结果,所以变量是不可变的(无状态的)
我们可以把一个函数的执行结果交给另一个函数去处理 -
Lodash
A modern JavaScript utility library delivering modularity, performance & extras.
是一个一致性、模块化、高性能的 JavaScript 实用工具库。// 演示 lodash // first / last / toUpper / reverse / each / includes / find / findIndex const _ = require('lodash') const array = ['Tom','jack','Rose','locy','kate'] console.log(_.first(array)) console.log(_.last(array)) console.log(_.toUpper(_.first(array))) console.log(_.reverse(array)) let r = _.each(array,(item,index)=>{ console.log(item,index); }) console.log(r) r = _.find(array,item=>{ return item === 'Tom' }) console.log(r) r = _.findIndex(array,item=>item === 'Tom') console.log(r); const arr1 = [ {'user':'ming','active':false}, {'user':'gang','active':true}, {'user':'liang','active':false} ] let r1 = _.findIndex(arr1,{'user':'liang'}) console.log(r1)
-
纯函数的好处
可缓存 - 因为纯函数对相同的输入始终有相同的结果,所以可以把纯函数的结果缓存起来
# 安装 lodash 依赖 npm init -y npm i lodash
// 记忆函数 const _ = require('lodash') function getArea(r){ console.log(r) return Math.PI * r * r } let getAreaWithMemory = _.memoize(getArea) console.log(getAreaWithMemory(4)) console.log(getAreaWithMemory(4)) console.log(getAreaWithMemory(4)) console.log(getAreaWithMemory(4))
自己模拟一个记忆函数
// 模拟 memoize 方法实现 /** * * @param {纯函数} fn */ function memoize(fn){ let cache = {} return function(){ let key = JSON.stringify(arguments) cache[key] = cache[key] || fn.apply(fn,arguments) return cache[key] } } let getAreaWithMemory = memoize(getArea) console.log(getAreaWithMemory(4)) console.log(getAreaWithMemory(4)) console.log(getAreaWithMemory(4)) console.log(getAreaWithMemory(4))
可测试 - 纯函数让测试更方便
并行处理:
在多线程环境下并行操作共享内存数据可能会出现意外情况 纯函数不需要访问共享的内存数据,所以在并行环境下可以任意运行纯函数(Web Worker)
-
纯函数的副作用
纯函数:对于相同的输入永远会得到相同的输出,而且没有任何可观察的 副作用// 不纯的 let mini = 18 function checkAge(age){ return age >= mini } // 纯的(有硬编码,后续可以通过柯里化解决) function checkAge(age){ let mini = 18 // 有硬编码 return age >= mini }
副作用让一个函数变得不纯(如上例),纯函数是根据相同的输入返回相同的输出,如果函数依赖于外部的状态,就无法保证输出相同,就会带来副作用。
副作用来源:
- 配置文件 - 数据库 - 获取用户的输入 - 等等 ...
所有的外部交互都有可能带来副作用,副作用也使得方法通用性下降,不适合扩展和可重用性,同时副作用会给程序中带来安全隐患给程序带来不确定性,但是副作用不可能完全禁止,尽可能控制它们在可控范围内发生。
-
柯里化(Haskell Brooks Curry)
使用柯里化解决上一个案例中硬编码的问题
// 柯里化演示 function checkAge(age) { let mini = 18 // 硬编码 return age >= mini } // 普通的纯函数 function checkAge(mini,age){ return age >= mini } console.log(checkAge(18,20)) console.log(checkAge(18,24)) console.log(checkAge(22,24)) console.log(checkAge(22,20)) // 函数的柯里化 function checkAge(mini){ return function (age){ return age >= mini } } // es6 写法 let checkAge = mini => (age => age >= mini) let checkAge18 = checkAge(18) let checkAge22 = checkAge(22) console.log(checkAge18(20)) console.log(checkAge18(24)) console.log(checkAge22(24)) console.log(checkAge22(20))
柯里化(Currying)
- 当一个函数有多个参数的时候,先传递一部分参数调用它(这部分参数以后永远不变) - 然后返回一个新的函数接收剩余的参数,返回结果
lodash 中的柯里化函数:
_.curry | func
功能:创建一个函数,该函数接收一个或多个 func 的参数,如果 func 所需要的参数都被提供则执行 func 并返回执行的结果,否则继续返回该函数等待接收剩余参数
参数:需要柯里化的函数
返回值:柯里化后的函数
// lodash 中的 curry 基本使用 const _ = require('lodash') function getSum(a, b, c) { return a + b + c } const curried = _.curry(getSum) // console.log(curried(1, 2, 3)); // console.log(curried(1, 2)); console.log(curried(1, 2)(3)); // console.log(curried(1)); console.log(curried(1)(2, 3)); console.log(curried(1)(2)(3));
柯里化案例:
// 柯里化案例 // ''.match(/\s+/g) // 匹配所有空白字符 // ''.match(/\d+/g) // 匹配所有数字 const _ = require('lodash') // function match(reg, str) { // return str.match(reg) // } const match = _.curry(function (reg, str) { return str.match(reg) }) // 是否有空白字符 const haveSpace = match(/\s+/g) // console.log(haveSpace('hello world')); const haveNumber = match(/\d+/g) // console.log(haveNumber('123abc')); // console.log(haveNumber('a1b2cde3f')); // console.log(haveNumber('abc')); const filter = _.curry(function (func, array) { return array.filter(func) }) console.log(filter(haveSpace, ['John Connor', 'John_Donne'])); const findSpace = filter(haveSpace) console.log(findSpace(['John Connor', 'John_Donne']));
模拟柯里化:
// 模拟实现 lodash 中的 curry 方法 const _ = require('lodash') function getSum(a, b, c) { return a + b + c } // const curried = _.curry(getSum) // console.log(curried(1, 2, 3)); // console.log(curried(1, 2)(3)); // console.log(curried(1)(2, 3)); // console.log(curried(1)(2)(3)); function curry (func) { return function curriedFn(...args) { // 判断实参和形参的个数 if(args.length < func.length){ return function () { return curriedFn(...args.concat(Array.from(arguments))) } } return func(...args) } } const curried = curry(getSum) console.log(curried(1, 2, 3)); console.log(curried(1, 2)(3)); console.log(curried(1)(2, 3)); console.log(curried(1)(2)(3));
柯里化总结:
- 柯里化可以让我们给一个函数传递较少的参数得到一个已经记住了某些固定参数的新函数 - 这是一种对函数参数的 '缓存' - 让函数变得更灵活,让函数的粒度更小 - 可以把多元函数转换成一元函数,可以组合使用函数产生强大功能
-
函数的组合
纯函数和柯里化很容易写出洋葱代码 h(g(f(x)))
例:获取数组的最后一个元素再转换成大写字母,.toUpper(.first(_.reverse(array)))
函数组合可以让我们把细粒度的函数重新组合成一个新的函数来实现相同的功能管道
下面这张图表示程序中使用函数的过程,给 fn 函数输入参数 a ,返回结果 b ,可以想想 a 数据通过一个管道得到了 b 数据。
当 fn 函数比较复杂的时候,我们可以把函数 fn 拆分成多个小函数,此时多了中间运算过程产生的 m 和 n。下面这张图中可以想象成把 fn 这个管道拆分成了 3 个管道 f1,f2,f3,数据 a 通过管道 f3 得到数据 m ,再通过管道 f2 得到结果 n ,n 通过管道 f1 得到结果 b 。
fn = compose(f1, f2, f3) b = fn(a)
函数组合(compose):如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把中间过程的函数合并成一个函数
函数就像是数据的管道,函数组合就是把这些管道连接起来,让数据穿过多个管道形成最终结果。
函数组合默认是从右到左执行
// 函数组合演示 function compose(f, g) { // 只处理两个函数的组合 return function (value) { return f(g(value)) } } // 测试 // 比如:使用函数组合求数组中的最后一个元素,仅当测试示例 function reverse(array){ return array.reverse(array) } function first(array){ return array[0] } const last = compose(first, reverse) console.log(last([1,2,3,4]));
lodash 中的组合函数:
flow() 或者 flowRight() 都可以组合多个函数
flow() 是从左到右运行,flowRight() 是从右到左运行
// lodash 中的函数组合的方法 _.flowRight() const _ = require('lodash') const reverse = arr => arr.reverse() const first = arr => arr[0] const toUpper = s => s.toUpperCase() const f = _.flowRight(toUpper, first, reverse) console.log(f(['one', 'two', 'three']))
组合函数的实现原理:
// 模拟 lodash 中的 flowRight const reverse = arr => arr.reverse() const first = arr => arr[0] const toUpper = s => s.toUpperCase() // function compose (...args) { // return function (value) { // return args.reverse().reduce(function (acc, fn) { // return fn(acc) // }, value) // } // } const compose = (...args) => value => args.reverse().reduce((acc, fn) => fn(acc), value) const f = compose(toUpper, first, reverse) console.log(f(['one', 'two', 'three']))
结合律
函数组合要满足结合律,
结合律就是说假如有三个函数,先组合前两个再组合第三个,和 先组合后两个再组合第一个,结果都是一样的// 函数组合要满足结合律 const _ = require('lodash') // const f = _.flowRight(_.toUpper, _.first, _.reverse) // const f = _.flowRight(_.flowRight(_.toUpper, _.first), _.reverse) // 先组合前两个 const f = _.flowRight(_.toUpper, _.flowRight(_.first, _.reverse)) // 先组合后两个 console.log(f(['one', 'two', 'three']))
函数组合如何调试
// 函数组合 调试 // NEVER SAY DIE --> never-say-die const _ = require('lodash') // const log = v => { // console.log(v) // return v // } const trace = _.curry((tag, v) => { console.log(tag, v) return v }) // _.split() const split = _.curry((sep, str) => _.split(str, sep)) // _.toLower() const join = _.curry((sep, array) => _.join(array, sep)) const map = _.curry((fn, array) => _.map(array, fn)) const f = _.flowRight(join('-'), trace('map 之后'), map(_.toLower), trace('split 之后'), split(' ')) console.log(f('NEVER SAY DIE'))
lodash 中的 fp(functional programming) 模块
lodash 的 fp 模块提供了实用的对函数编程友好的方法 提供了不可变 auto-curried iteratee-first data-last 的方法
// lodash 的 fp 模块 // NEVER SAY DIE --> never-say-die const fp = require('lodash/fp') const f = fp.flowRight(fp.join('-'), fp.map(fp.toLower), fp.split(' ')) console.log(f('NEVER SAY DIE'))
lodash 和 lodash/fp 模块中 map 方法的区别
// lodash 和 lodash/fp 模块中 map 方法的区别 // const _ = require('lodash') // console.log(_.map(['23', '8', '10'], parseInt)) // // parseInt('23', 0, array) // // parseInt('8', 1, array) // // parseInt('10', 2, array) const fp = require('lodash/fp') console.log(fp.map(parseInt, ['23', '8', '10']))
-
PointFree
PointFree: 我们可以把数据处理的过程定义成与数据无关的合成运算,不需要用到代表数据的那个参数,只要把简单的运算步骤合成到一起,在使用这种模式之前,我们需要定义一些辅助的基本运算函数。- 不需要指明处理的数据
- 只需要合成运算过程
- 需要定义一些辅助的基本运算函数
// point free // Hello World => hello_world const fp = require('lodash/fp') const f = fp.flowRight(fp.replace(/\s+/g, '_'), fp.toLower) console.log(f('Hello World'))
PointFree 案例:
// 把一个字符串中的首字母提取并转换成大写, 使用. 作为分隔符 // world wild web ==> W. W. W const fp = require('lodash/fp') // const firstLetterToUpper = fp.flowRight(fp.join('. '), fp.map(fp.first), fp.map(fp.toUpper), fp.split(' ')) const firstLetterToUpper = fp.flowRight(fp.join('. '), fp.map(fp.flowRight(fp.first, fp.toUpper)), fp.split(' ')) console.log(firstLetterToUpper('world wild web'))
-
-
函子 Functor
为什么要学函子
到目前为止,我们已经学习了函数式编程的一些基础,但是我们还没演示在函数式编程中如何把副作用控制在可控的范围内、异常处理、异步操作等
什么是 Functor
- 容器:包含值和值得变形关系(这个变形关系就是函数) - 函子:是一个特殊的容器,通过一个普通的对象来实现,该对象具有 map 方法,map 方法可以运行一个函数对值进行处理(变形关系)
// // Functor 函子 // class Container { // constructor (value) { // this._value = value // } // map (fn) { // return new Container(fn(this._value)) // } // } // let r = new Container(5) // .map(x => x + 1) // .map(x => x * x) // console.log(r) class Container { static of (value) { return new Container(value) } constructor (value) { this._value = value } map (fn) { return Container.of(fn(this._value)) } } // let r = Container.of(5) // .map(x => x + 2) // .map(x => x * x) // console.log(r) // 演示 null undefined 的问题,程序是会报错得,下面会讲解处理空值得问题 Container.of(null) .map(x => x.toUpperCase())
总结:
- 函数式编程的运算不直接操作值,而是由函子完成 - 函子就是一个实现了 map 契约的对象 - 我们可以把函子想象成一个盒子,这个盒子里封装了一个值 - 想要处理盒子中的值,我们需要给盒子的 map 方法传递一个处理值得函数(纯函数),由这个函数对值来进行处理 - 最终 map 方法返回一个包含新值得盒子(函子)
MayBe 函子(可能会是…)
- 我们在编程过程中可能会遇到很多错误,需要对这些错误进行相应得处理, - MayBe 函子的作用就是可以对外部的空值情况做处理(控制副作用在允许范围内)
// MayBe 函子 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 === undefined } } // let r = MayBe.of('Hello World') // .map(x => x.toUpperCase()) // console.log(r) // let r = MayBe.of(null) // .map(x => x.toUpperCase()) // console.log(r) let r = MayBe.of('hello world') .map(x => x.toUpperCase()) .map(x => null) .map(x => x.split(' ')) console.log(r)
Either 函子
- Either 两者中的任何一个,类似于 if ... else ... 的处理 - 异常会让函数变得不纯,Either 函子可以用来做异常处理
// 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)) } } // let r1 = Right.of(12).map(x => x + 2) // let r2 = Left.of(12).map(x => x + 2) // console.log(r1) // console.log(r2) function parseJSON (str) { try { return Right.of(JSON.parse(str)) } catch (e) { return Left.of({ error: e.message }) } } // let r = parseJSON('{ name: zs }') // console.log(r) let r = parseJSON('{ "name": "zs" }') .map(x => x.name.toUpperCase()) console.log(r)
IO 函子
- IO 函子中的 _value 是一个函数,这里是把函数作为值来处理 - IO 函子可以把不纯的动作存储到 _value 中,延迟执行这个不纯的操作(惰性执行),包装当前的操作为一个纯的操作 - 把不纯的操作交给调用者来处理
// IO 函子 const fp = require('lodash/fp') class IO { static of (value) { return new IO(function () { return value }) } constructor (fn) { this._value = fn } map (fn) { return new IO(fp.flowRight(fn, this._value)) } } // 调用 let r = IO.of(process).map(p => p.execPath) // console.log(r) console.log(r._value())
Folktale:Task 异步执行
· 异步任务的实现过于复杂,我们使用 folktale 中的 Task 来演示
· folktale 一个标准的函数式编程库
- 和 lodash 、 ramda 不同的是 ,他没有提供很多功能函数 - 只是提供了一些 函数式处理的操作,例如:compose、curry等,一些函子:Task、MayBe、Either 等
// folktale 中的 compose、curry const { compose, curry } = require('folktale/core/lambda') const { toUpper, first } = require('lodash/fp') // let f = curry(2, (x, y) => { // return x + y // }) // console.log(f(1, 2)) // console.log(f(1)(2)) let f = compose(toUpper, first) console.log(f(['one', 'two']))
Task 函子 异步任务
folktale 中 2.X 版本和 1.X 版本中的 Task 区别很大,这里以 2.3.2 来演示// Task 处理异步任务 const fs = require('fs') const { task } = require('folktale/concurrency/task') const { split, find } = require('lodash/fp') function readFile (filename) { return task(resolver => { fs.readFile(filename, 'utf-8', (err, data) => { if (err) resolver.reject(err) resolver.resolve(data) }) }) } readFile('package.json') .map(split('\n')) .map(find(x => x.includes('version'))) .run() .listen({ onRejected: err => { console.log(err) }, onResolved: value => { console.log(value) } })
Pointed 函子
· Pointed 函子是实现了 of 静态方法的函子,
· of 方法是为了避免使用 new 来创建对象,更深层次含义是 of 方法用来把值放到上下文 Context(把值放到容器中,使用 map 来处理)
// 只是一个概念而已,我们一直在使用的 class Container { static of (value){ return Container(value) } ... } Container.of(2) .map(x => x + 5)
Monad 函子:
先看一个 IO 函子的问题,IO(IO(x)) 嵌套的函子// IO 函子的问题 const fs = require('fs') const fp = require('lodash/fp') class IO { static of (value) { return new IO(function () { return value }) } constructor (fn) { this._value = fn } map (fn) { return new IO(fp.flowRight(fn, this._value)) } } let readFile = function (filename) { return new IO(function () { return fs.readFileSync(filename, 'utf-8') }) } let print = function (x) { return new IO(function () { console.log(x) return x }) } let cat = fp.flowRight(print, readFile) // IO(IO(x)) 嵌套的函子 let r = cat('package.json')._value()._value() console.log(r)
· Monad 函子是可以变扁的 Pointed 函子,IO(IO(x))
· 一个函子如果具有 join 和 of 两个方法并遵守一些定律,就是一个 Monad// IO Monad const fs = require('fs') const fp = require('lodash/fp') class IO { static of (value) { return new IO(function () { return value }) } constructor (fn) { this._value = fn } map (fn) { return new IO(fp.flowRight(fn, this._value)) } join () { return this._value() } flatMap (fn) { return this.map(fn).join() } } let readFile = function (filename) { return new IO(function () { return fs.readFileSync(filename, 'utf-8') }) } let print = function (x) { return new IO(function () { console.log(x) return x }) } let r = readFile('package.json') // .map(x => x.toUpperCase()) .map(fp.toUpper) .flatMap(print) // 当我们要合并的函数返回的是一个值,调用的是 map 当我们要合并的函数返回的是一个函子,这时候要调用 flatMap .join() console.log(r)
总结:
· 认识函数式编程
函数式编程是一种编程范式,或者说是一种编程的思想, 它跟面向对象编程是同一级别的,我们想要掌握一种编程思想,是需要花费很长时间的,可能是半年,或者更长的时间 函数式编程的核心思想是:把运算过程进行抽象成函数,我们在编程过程中是面向函数进行编程的
· 函数相关复习
函数是一等公民 - 函数可以储存在变量中 - 函数作为参数 - 函数作为返回值 高阶函数 - 函数作为参数或者是返回值 闭包 - 无处不在
· 函数式编程基础
lodash 纯函数 柯里化 管道 函数组合
· 函子
函子可以帮助我们控制副作用,进行异常处理,或者异步操作等等 可以把函子想象成一个盒子,这个盒子里面包裹着一个值, 我们想要对这个值进行处理的话,我们需要调用这个盒子给我们提供的 map 方法, map 方法接收一个函数类型的参数,我们传递的这个函数就是去处理值得函数 - Functor - MayBe - Either - IO - Task folktal - Monad
【学习笔记】Part1·JavaScript·深度剖析-函数式编程与 JS 异步编程、手写 Promise(课前准备)
【学习笔记】Part1·JavaScript·深度剖析-函数式编程与 JS 异步编程、手写 Promise(一、函数式编程范式)
【学习笔记】Part1·JavaScript·深度剖析-函数式编程与 JS 异步编程、手写 Promise(二、JavaScript 异步编程)
【学习笔记】Part1·JavaScript·深度剖析-函数式编程与 JS 异步编程、手写 Promise(三、手写Promise源码)
【Part1作业】https://gitee.com/zgp-qz/part01-task
【学习笔记】Part1·JavaScript·深度剖析-ES 新特性与 TypeScript、JS 性能优化(一、ECMAScript 新特性)
【学习笔记】Part1·JavaScript·深度剖析-ES 新特性与 TypeScript、JS 性能优化(二、TypeScript 语言)
【学习笔记】Part1·JavaScript·深度剖析-ES 新特性与 TypeScript、JS 性能优化(三、JavaScript 性能优化1)
【学习笔记】Part1·JavaScript·深度剖析-ES 新特性与 TypeScript、JS 性能优化(四、JavaScript 性能优化2)