文章内容输出来源:拉勾教育大前端高薪训练营
函数式编程(Function Programming,FP)是编程范式之一。常说的编程范式还有面向过程编程、面向对象编程。
// 非函数式
let a = 1;
let b = 2;
let sum = a+b;
console.log(sum);
// 函数式
function add(a,b){
return a+b
}
let sum1 = add(1,2)
console.log(sum1);
体现在:mdn 头等函数
let fn = function(){
console.log('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
}
// 高阶函数 函数作为参数
function forEach(arr,fn) {
for (let i = 0; i < arr.length; i++) {
fn(arr[i])
}
}
// 函数作为返回值
function makeFn(){
let msg = 'hello'
return function(){
console.log(msg);
}
}
// 模拟常用高阶函数
// map
const map = (arr,fn)=>{
let res =[];
for (let i = 0; i < arr.length; i++) {
res.push(fn(arr[i]))
}
return res;
}
// let arr = [1,2,3,4]
// let res = map(arr,v=>v*v)
// console.log(res);
//every
const every = (array,fn)=>{
let res = true;
for (let i = 0; i < array.length; i++) {
res = fn(array[i])
if(!res) break
}
return res
}
// let arr = [3,4,5]
// let res = every(arr,v=>v>2)
// console.log(res);
// some
const some = (array,fn)=>{
let res = false;
for (const value of array) {
res = fn(value)
if(res) break
}
return res;
}
let arr = [1,4,5,7,9]
let res = some(arr,v=>v%2===0)
console.log(res);
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(18)
pay(18)
pay(18)
pay(18)
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(4));
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));
// 演示lodash
// first / last / toUpper / reverse / each / includes / find / findIndex
const _ = require('lodash')
const array = ['jack','tom','kate','lucy']
// console.log(_.first(array));
// console.log(_.last(array));
// console.log(_.toUpper(_.first(array)));
// console.log(_.reverse(array));
const r = _.each(array,(item,index)=>{
console.log(item,index);
})
console.log(r);
// 纯函数和不纯的函数
// slice splice
let arr = [1,2,3,4,5,6,7]
// 纯函数
console.log(arr.slice(0,3));//[ 1, 2, 3 ]
console.log(arr.slice(0,3));//[ 1, 2, 3 ]
console.log(arr.slice(0,3));//[ 1, 2, 3 ]
// 不纯的函数
console.log(arr.splice(0,3));//[ 1, 2, 3 ]
console.log(arr.splice(0,3));//[ 4, 5, 6 ]
console.log(arr.splice(0,3));//[ 7 ]
// 纯函数
function getSum(a,b){
return a+b
}
console.log(getSum(1,2));
console.log(getSum(1,2));
console.log(getSum(1,2));
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));
// 模拟实现memoize
function memoize(f){
const 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));
// 不纯的
let min = 18;
function checkAge(age){
return age>= min
}
// 纯的 (有硬编码)
function checkAge2(age){
let min = 18;
return age>=min
}
所有的外部交互都有可能带来副作用,副作用也使得方法通用性下降不适合扩展和可重用性,同时副作用会给程序中带来安全隐患,但是副作用不可能完全禁止,尽可能控制他们在可控范围内发生。
// function checkAge(age){
// let min = 18;
// return age >= min;
// }
// 普通的纯函数
// function checkAge(min,age){
// return age >= min
// }
// console.log(
// checkAge(18,24),
// checkAge(18,20),
// checkAge(20,30)
// );
// function checkAge(min) {
// return function (age) {
// return age >= min
// }
// }
const checkAge = min => (age => age >= min)
let checkAge18 = checkAge(18)
let checkAge20 = checkAge(20)
console.log(checkAge18(20));
console.log(checkAge20(24));
// 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));
// 柯里化案例
const match = _.curry(function(reg,str){
return str.match(reg)
})
const haveSpace = match(/\s+/g)
const haveNumber = match(/\d+/g)
const filter = _.curry((func,array)=>{
return array.filter(func)
})
const findSpace = filter(haveSpace)
// console.log(haveSpace('hello world'));
// console.log(haveNumber('abc123'));
console.log(filter(haveSpace,['John Connor','John_Donne']));
console.log(findSpace(['John Connor','John_Donne']));
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));
function curry(func){
return function curriedFn(...args){
if(args.length<func.length){
return function(){
return curriedFn(...args.concat(Array.from(arguments)))
}
}
return func(...args)
}
}
_.toUpper(_.first(_.reverse(arr)))
下图便是程序中使用函数处理数据的过程,给fn函数输入参数a,返回结果b。可以想象a数据通过一个管道得到了b数据。
当fn函数比较复杂的时候,可以把fn拆分成多个小函数,此时多了中间运算过程中产生的m和n。
下图可以想象把fn这个管道拆分成3个管道f1,f2,f3,数据a通过管道f3得到结果m,m再通过管道f2得到结果n,n通过管道f1得到最终结果b.
fn = compose(f1,f2,f3)
b = fn(a)
// 函数组合
function compose(f,g){
return function(value){
return f(g(value))
}
}
function reverse(arr){
return arr.reverse();
}
function first(arr){
return arr[0]
}
let last = compose(first,reverse)
console.log(last([1,2,3,4]));
// 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((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 f1 = _.flowRight(_.flowRight(_.toUpper,_.first),_.reverse)
const f2 = _.flowRight(_.toUpper,_.flowRight(_.first,_.reverse))
console.log(f(['one','two','three']));
console.log(f1(['one','two','three']));
console.log(f2(['one','two','three']));
const _ = require('lodash')
const trace = _.curry((tag,v)=>{
console.log(tag,v);
return v
})
const split = _.curry((sep,str)=>_.split(str,sep))
const jion = _.curry((sep,arr)=>_.join(arr,sep))
const map = _.curry((fn,arr)=>_.map(arr,fn))
const f = _.flowRight(jion('-'),trace('map之后'),map(_.toLower),trace('split之后'),split(' '))
console.log(f('never say die'));
const fp = require('lodash/fp')
const _ = require('lodash')
const f = fp.flowRight(fp.join('-'),fp.map(fp.toLower),fp.split(' '))
console.log(f('NEVER SAY DIE'));
console.log(_.map(['23','8','10'],parseInt));
console.log(fp.map(parseInt,['23','8','10']));
我们可以把数据处理的过程定义成与数据无关的合成运算,不需要用到代表数据的那个参数,只是把简单的运算步骤合成到一起,在使用这种模式之前我们需要定义一些辅助的基本运算函数。
const f = fp.flowRight(fp.join('-'),fp.map(_.toLower),fp.split(' '))
// 非 Point Free模式
// function f(word){
// return word.toLowerCase().replace(/\s+/g,'_')
// }
// console.log(f('Hello World'));
// Point Free
const fp = require('lodash/fp')
const f = fp.flowRight(fp.replace(/\s+/g,'_'),fp.toLower)
console.log(f('Hello World'));
// 实用Point Free,模式 将单词中的首字母提取出来,并转换为大些
const firstLetterToUpper = fp.flowRight(fp.join('. '), fp.map(fp.flowRight(fp.first, fp.toUpper)), fp.split(' '))
console.log(firstLetterToUpper('world wild web'));
在函数式编程中如何把副作用控制在可控的范围内、异常处理、异步操作等。
// class Contains{
// constructor(value){
// this._value = value
// }
// map(fn){
// return new Contains(fn(this._value))
// }
// }
// let r = new Contains(5).map(x=>x+1).map(x=>x*x)
// console.log(r); // 36
class Contains{
static of(val){
return new Contains(val)
}
constructor(value){
this._value = value
}
map(fn){
return Contains.of(fn(this._value))
}
}
let r = new Contains(5).map(x=>x+2).map(x=>x*x)
console.log(r); // 49
// 演示 null undefined的问题 如果不小心传入了空值
Contains.of(null).map(x=>x.toUpperCase())
对错误做响应的处理。
MayBe的作用就是对外部的空值情况做处理(控制副作用的允许的范围)
在MayBe函子中,很难确认是哪一步产生的空值问题。
class MayBe{
static of(val){
return new MayBe(val)
}
constructor(val){
this._val = val
}
map(fn){
return this.isNothing()?MayBe.of(null):MayBe.of(fn(this._val))
}
isNothing(){
return this._val === null || this._val === undefined
}
}
let r = MayBe.of('hello lala').map(x=>x.toUpperCase())
// console.log(r);
let r1 = MayBe.of(null).map(x=>x.toUpperCase())
// console.log(r1);
let r2 = MayBe.of('hello lala')
.map(x=>x.toUpperCase())
.map(x=>null)
.map(x=>x.split(' '))
console.log(r2);
两者中的任何一个,类似if…else…的处理
异常会让函数变得不纯,Either函子可以用来做异常处理
class Left{
static of(val){
return new Left(val)
}
constructor(val){
this._val = val
}
map(fn){
return this
}
}
class Right{
static of(val){
return new Right(val)
}
constructor(val){
this._val = val
}
map(fn){
return Right.of(fn(this._val))
}
}
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({err:e.message})
}
}
let r = parseJson('{"name":"zs"}').map(x=>x.name.toUpperCase())
console.log(r);
IO函子中的_val是一个函数,这里是把函数作为值来处理。
IO函子可以把不纯的动作存储到_val中,延迟执行这个不纯的操作(惰性执行),把不纯的操作交给调用者来处理。
const fp = require('lodash/fp')
class IO{
static of(val){
return new IO(function(){
return val
})
}
constructor(val){
this._val = val
}
map(fn){
return new IO(fp.flowRight(fn,this._val))
}
}
let r = IO.of(process).map(p=>p.execPath)
console.log(r);
console.log(r._val());
使用folktale中的Task来演示。
folktale是一个标准的函数式编程库。
只提供了一些函数式处理操作,如compose、curry等 一些函子Task、Either、MayBe等
const {compose,curry} = require('folktale/core/lambda')
const {toUpper,first} = require('lodash/fp')
let f = curry(2,(x,y)=>x+y)
console.log(f(1,2));
console.log(f(1)(2));
let f1 = compose(toUpper,first)
console.log(f1(['one','two']));
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函子是实现了of静态方法的函子
of方法是为了避免使用new来创建对象,更深层的含义是of方法来把值放到上下文Context(把值放到容器中,使用map来处理)
class Contains{
static of(val){
return new Contains(val)
}
...
}
Contains.of(2).map(x=>x+1)
一个函子如果具有join和of两个方法并遵守一些定律就是一个Monad
const fs = require('fs')
const fp = require('lodash/fp')
class IO{
static of(val){
return new IO(()=>val)
}
constructor(val){
this._val = val
}
map(fn){
return new IO(fp.flowRight(fn,this._val))
}
}
let readFile = (filename)=>{
return new IO(()=>fs.readFileSync(filename,'utf-8'))
}
let print = (x)=>{
return new IO(()=>{
console.log(x);
return x
})
}
let cat = fp.flowRight(print,readFile)
let r = cat('../../package.json')._val()._val()
console.log(r);
const fs = require('fs')
const fp = require('lodash/fp')
class IO{
static of(val){
return new IO(()=>val)
}
constructor(fn){
this._val = fn
}
map(fn){
return new IO(fp.flowRight(fn,this._val))
}
join(){
return this._val()
}
flatMap(fn){
return this.map(fn).join()
}
}
let readFile = (filename)=>{
return new IO(()=>fs.readFileSync(filename,'utf-8'))
}
let print = (x)=>{
return new IO(()=>{
console.log(x);
return x
})
}
let r = readFile('../../package.json')
.map(fp.toUpper)
.flatMap(print)
.join()
console.log(r);
参考资料:
函数式编程指北
函数式编程入门
Pointfree 编程风格指南
图解 Monad
Functors
同步
console.log('start');
function bar(){
console.log('bar');
}
function foo(){
console.log('foo');
bar()
}
foo();
console.log('end');
异步
console.log('start');
setTimeout(()=>{
console.log('timer1');
},1800)
setTimeout(()=>{
console.log('timer2');
setTimeout(()=>{
console.log('inner');
},1000)
},1000)
console.log('end');
// start
// end
// timer2
// timer1
// inner
function foo(cb) {
setTimeout(()=>{
cb()
},1000)
}
foo(()=>{
console.log('回调');
console.log('调用者定义,执行者执行');
console.log('就是调用者告诉执行者异步任务结束后应该做什么');
})
// 回调地狱
promise基本示例
const promise = new Promise((resolve,reject)=>{
resolve(100)
})
promise.then(value=>{
console.log(value);
},err=>{
console.log(err);
})
console.log('end');
promise-ajax
function ajax(url) {
return new Promise((resolve,reject)=>{
var xhr = new XMLHttpRequest();
xhr.open('GET',url);
xhr.responseType = 'json'
xhr.onload = function(){
if(this.status === 200){
resolve(this.response)
}else{
reject(new Error(this.statusText))
}
}
xhr.send()
})
}
ajax('./api/users.json').then(res=>{
console.log(res);
},err=>{
console.log(err);
})
// 嵌套使用 Promise 是最常见的误区
ajax('/api/urls.json').then(function (urls) {
ajax(urls.users).then(function (users) {
ajax(urls.users).then(function (users) {
ajax(urls.users).then(function (users) {
ajax(urls.users).then(function (users) {
})
})
})
})
})
promise链式调用
function ajax(url) {
return new Promise((resolve,reject)=>{
var xhr = new XMLHttpRequest();
xhr.open('GET',url);
xhr.responseType = 'json'
xhr.onload = function(){
if(this.status === 200){
resolve(this.response)
}else{
reject(new Error(this.statusText))
}
}
xhr.send()
})
}
ajax('./api/users.json')
.then(res=>{
console.log(res);
return ajax('./api/users.json')
})
.then(res=>{
console.log(res);
return ajax('./api/users.json')
})
.then(res=>{
console.log(res);
return ajax('./api/users.json')
})
.then(res=>{
console.log(res);
return 'foo'
})
.then(res=>{
console.log(res);
})
promise异常处理
function ajax(url) {
return new Promise((resolve,reject)=>{
var xhr = new XMLHttpRequest();
xhr.open('GET',url);
xhr.responseType = 'json'
xhr.onload = function(){
if(this.status === 200){
resolve(this.response)
}else{
reject(new Error(this.statusText))
}
}
xhr.send()
})
}
// 使用catch注册失败回调
ajax('./api/users.json')
.then(res=>{
console.log(res);
return ajax('./api/users.json')
})
.catch(err=>{
console.log(err);
})
// then(onRejected) === then(undefined,onRejected)
ajax('./api/users.json')
.then(res=>{
console.log(res);
return ajax('./api/users.json')
}).then(undefined,(err)=>{
console.log(err);
})
// 全局捕获 promise异常
window.addEventListener('unhandlerejection',event=>{
const {reason,promise} = event
console.log(reason,promise);
event.preventDefault();
},false)
// node中的方式
process.on('unhandledRejection',(reason,promise)=>{
console.log(reason,promise);
})
微任务
console.log('start');
setTimeout(()=>{
console.log('setTimeout');
},0)
Promise.resolve().then(()=>{
console.log('promise');
}).then(()=>{
console.log('promise1');
})
console.log('end');
function * foo(){
console.log('start');
try {
const res = yield 'foo'
console.log(res);
} catch (error) {
console.log(error);
}
}
const generator = foo();
const res = generator.next();
console.log(res);
// generator.throw(new Error('xxx error'))
function * main () {
try {
const users = yield ajax('/api/users.json')
console.log(users)
const posts = yield ajax('/api/posts.json')
console.log(posts)
const urls = yield ajax('/api/urls11.json')
console.log(urls)
} catch (e) {
console.log(e)
}
}
function co (generator) {
const g = generator()
function handleResult (result) {
if (result.done) return // 生成器函数结束
result.value.then(data => {
handleResult(g.next(data))
}, error => {
g.throw(error)
})
}
handleResult(g.next())
}
co(main)
async function main () {
try {
const users = await ajax('/api/users.json')
console.log(users)
const posts = await ajax('/api/posts.json')
console.log(posts)
const urls = await ajax('/api/urls.json')
console.log(urls)
} catch (e) {
console.log(e)
}
}
// co(main)
const promise = main()