JavaScript函数式编程学习笔记
一、函数式编程概念简介
函数与方法的区别
函数是一段可以通过其名称被调用的代码
方法是一段必须通过其名称及其关联对象的名称被调用的代码
var bar = (a) => {return a}bar(1)
var obj = { bar: (a) => {return a}}obj.bar(1)
函数的引用透明性
所有的函数对于相同的输入都会返回相同的值
可以用值替换函数
const double = x => x * 2;console.log(4 === double(2));
console.log(Math.max(1, 2, 3, 4) === 4);
函数式编程主张声明式编程和编写抽象的代码
命令式遍历数组
声明式遍历数组
纯函数
纯函数产生可测试的代码
给定输入就会返回相同输出的函数,遵循引用透明性;不应该依赖外部变量,也不应该改变外部变量
管道与组合
纯函数应该只做一件事情
举一个linux命令的例子:我们想要在一个文件中找到一个字符串并统计它的出现次数:
cat jsBook | grep –i “composing” | wc
上面的命令通过组合多个函数解决了这个问题,组合不是linux命令独有的,但他是函数式编程的核心,也叫作函数式组合(Funtional Composition)
二、高阶函数
高阶函数是接收函数作为参数并且/或者返回函数作为输出的函数(Higher-Order functions HOC)
三、闭包和高阶函数
var fn = (arg) => { let outer = "Visible" let innerFn = () => { console.log(outer) console.log(arg) } return innerFn}var closureFn = fn(5)
当innerFn被return的时候为他设置作用域, 返回函数的引用被存储在closureFn里面
四、柯里化和偏应用
柯里化:吧一个多参数函数转换成一个嵌套的一元函数的过程
处理两个参数的柯里化函数
const add = (x,y) => x + y;const curry = (binaryFn) => { return function (firstArg) { return function (secondArg) { return binaryFn(firstArg, secondArg); }; };};let autoCurriedAdd = curry(add)autoCurriedAdd(2)(2)=> 4
柯里化的使用场景/优点如下
- 参数复用:柯里化可以让我们固定一部分参数,生成一个新的函数,这样子后续调用这个新函数就不用再次提供这些参数了。
- 延迟计算:通过柯里化,我们可以创建一系列的函数调用链,每一步返回一个新的函数,直至所有的参数被提供之后,才执行原始函数的计算。
- 功能分解:柯里化有助于将复杂的函数分解为能够处理一部分功能的简单函数,这些简单函数易于测试和维护。
- 动态生成函数:可以根据柯里化得到的中间函数生成具有特定功能的函数,而无需对每种情况都编写独立的函数定义。
柯里化实战:在数组中寻找数字
let match = curry(function(expr, str) { return str.match(expr);});
let hasNumber = match(/[0-9]+/)
let filter = curry(function(f, ary) { return ary.filter(f);});
let findNumbersInArray = filter(hasNumber)
findNumbersInArray(["js","number1"])
=> ["number1"]
Partial Application 偏应用,颠倒参数的传递顺序
将回调函数和时间参数调换
setTimeout(() => console.log("Do X task"),10);setTimeout(() => console.log("Do Y task"),10);//柯里化之后const setTimeoutWrapper = (time,fn) => {setTimeout(fn,time);}const delayTenMs = curry(setTimeoutWrapper)(10)delayTenMs(() => console.log("Do X task"))delayTenMs(() => console.log("Do Y task"))
偏函数定义:
const partial = function (fn,...partialArgs){ let args = partialArgs; return function(...fullArguments) { let arg = 0; for (let i = 0; i < args.length && arg < fullArguments.length; i++) { if (args[i] === undefined) { args[i] = fullArguments[arg++]; } } return fn.apply(null, args); };};
五、组合(函数组合)和管道(Composition and Pipelines)
UNIX哲学:
- 每个程序只做好一件事情,如果要添加新功能,请重新构建而不是在复杂的旧程序中添加新功能
- 每个程序的输出应该是另一个尚未可知程序的输入
接收两个参数的compose函数的实现:
const compose = (a, b) => (c) => a(b(c))
接收n个参数:
const compose = (...fns) => (value) => reduce(fns.reverse(), (accumulator, fn) => fn(accumulator), value)
六、函子Functor
函子是一个普通对象,它实现了map函数,用map操作对象值的时候生成一个新对象
换句话说:函子是一个实现了map方法的对象
用Maybe和Either函子处理错误
Maybe函子
const MayBe = function(val) { this.value = val}MayBe.of = function(val) { return new MayBe(val)}MayBe.prototype.isNothing = function() { return (this.value === null || this.value === undefined);}MayBe.prototype.map = function(fn) { return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this.value));}
MayBe.of("George").map((x) => x.toUpperCase()).map((x) => "Mr. " + x)//MayBe { value: 'Mr. GEORGE' }
MayBe.of("George").map(() => undefined).map((x) => "Mr. " + x)//MayBe { value: null }
let getTopTenSubRedditData = (type) => { let response = getTopTenSubRedditPosts(type); return MayBe.of(response).map((arr) => arr['data']) .map((arr) => arr['children']) .map((arr) => arrayUtils.map(arr, (x) => { return { title : x['data'].title, url : x['data'].url } } ))}
不足:不知道map函数在哪一步出错了
Either函子
const Nothing = function(val) { this.value = val;};Nothing.of = function(val) { return new Nothing(val);};Nothing.prototype.map = function(f) { return this;};const Some = function(val) { this.value = val;};Some.of = function(val) { return new Some(val);};Some.prototype.map = function(fn) { return Some.of(fn(this.value));}const Either = { Some : Some, Nothing: Nothing}let getTopTenSubRedditPostsEither = (type) => { let response //对成功和失败用不同的函数处理,就可以捕获错误的信息 try{ response = Some.of(JSON.parse(request('GET',"https://www.reddit.com/r/subreddits/" + type + ".json?limit=10").getBody('utf8'))) }catch(err) { response = Nothing.of({ message: "Something went wrong" , errorCode: err['statusCode'] }) } return response}let getTopTenSubRedditDataEither = (type) => { let response = getTopTenSubRedditPostsEither(type); return response.map((arr) => arr['data']) .map((arr) => arr['children']) .map((arr) => arrayUtils.map(arr, (x) => { return { title : x['data'].title, url : x['data'].url } } ))}getTopTenSubRedditDataEither('wrong')Nothing { value: { message: 'Something went wrong', errorCode: 404 } }
七、单子Monad
单子也是一种函子
const MayBe = function(val) { this.value = val}MayBe.of = function(val) { return new MayBe(val)}MayBe.prototype.isNothing = function() { return (this.value === null || this.value === undefined);}MayBe.prototype.map = function(fn) { return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this.value));}MayBe.prototype.join = function() { return this.isNothing() ? MayBe.of(null) : this.value;}MayBe.prototype.chain = function(f){ return this.map(f).join()}
通过join返回maybe对象里面的value值,脱掉了Maybe对象的外衣
八、异步编程Generator
Generator基础
创建一个Generator
function* gen() { return 'first generator';}
使用它:
let generatorResult = gen()console.log(generatorResult)gen {[[GeneratorStatus]]: "suspended", [[GeneratorReceiver]]: Window}
那么如何得到里面的value呢?可以使用next()方法
generator.next().value=> 'first generator'
如果我们调用两次next方法呢?
let generatorResult = gen()//第一次generatorResult.next().value=> 'first generator'//第二次generatorResult.next().value=> undefined
yield关键字
function* generatorSequence() { yield 'first'; yield 'second'; yield 'third';}
let generatorSequence = generatorSequence();generatorSequence.next().value=> firstgeneratorSequence.next().value=> second
带有yield关键字的G都会以惰性求值的方式进行,也就是说,代码直到调用的时候才会执行
Generator的值:
Generator里面有很多值,我们如何知道何时停止调用next方法呢?
每次对next函数的调用都会返回一个对象
{value: 'value', done: false}
done是一个可以判断Generator序列是否被完全消费的属性
//get generator instance variablelet generatorSequenceResult = generatorSequence();console.log('done value for the first time',generatorSequenceResult.next())console.log('done value for the second time',generatorSequenceResult.next())console.log('done value for the third time',generatorSequenceResult.next())
done value for the first time { value: 'first', done: false }done value for the second time { value: 'second', done: false }done value for the third time { value: 'third', done: false }
console.log(generatorSequenceResult.next())=> { value: undefined, done: true }
用循环遍历Generator
for(let value of generatorSequence())console.log("for of value of generatorSequence is",value)which is going to print:for of value of generatorSequence is firstfor of value of generatorSequence is secondfor of value of generatorSequence is third
给 Generator传递值
function* sayFullName() { var firstName = yield; var secondName = yield; console.log(firstName + secondName);}let fullName = sayFullName()fullName.next() //代码会暂停在这一行var firstName = yield;fullName.next('anto') //yield会被auto取代,并赋值给firstname// yield 会暂停在 var secondName = yield 这一步fullName.next('aravinth')=> anto aravinth
使用setTimeout的例子
let generator;let getDataOne = () => { setTimeout(function(){ //call the generator and //pass data via next console.log(333) generator.next('dummy data one') }, 1000);}let getDataTwo = () => { setTimeout(function(){ //call the generator and //pass data via next generator.next('dummy data two') }, 1000);}function* main() { console.log('1111') let dataOne = yield getDataOne(); console.log('2222') let dataTwo = yield getDataTwo(); console.log("data one",dataOne) console.log("data two",dataTwo) console.log('4444')}generator = main()generator.next()
generator = main()generator.next()
调用main函数,并遇到第一个yield语句,进入了暂停模式,但在暂停之前,他调用了getDataOne函数,
一秒钟之后setTimeout的回调会执行generator.next(‘dummy data one’),yield语句将会返回’dummy data one’,dataOneb变成’dummy data one’
然后会执行
let dataTwo = yield getDataTwo();
执行方式和之前一样
真实的例子:
let https = require('https');function httpGetAsync(url,callback) { return https.get(url, function(response) { var body = ''; response.on('data', function(d) { body += d; }); response.on('end', function() { let parsed = JSON.parse(body) callback(parsed) }) } );}
httpGetAsync('https://www.reddit.com/r/pics/.json',(data)=> { console.log(data)}){ modhash: '', children: [ { kind: 't3', data: [Object] }, { kind: 't3', data: [Object] }, { kind: 't3', data: [Object] }, . . . { kind: 't3', data: [Object] } ], after: 't3_5bzyli', before: null}
httpGetAsync('https://www.reddit.com/r/pics/.json',(picJson)=> { httpGetAsync(picJson.data.children[0].data.url+". json",(firstPicRedditData) => { console.log(firstPicRedditData) })})
上面的例子会造成回调地狱,用Generator来改写可以简化一下:
let generatorfunction request(url) { httpGetAsync( url, function(response){ generator.next( response ); });}
function *main() { let picturesJson = yield request( "https://www.reddit.com/r/pics/.json" ); let firstPictureData = yield request(picturesJson.data.children[0].data. url+".json") console.log(firstPictureData)}generator = main()generator.next()