函数式编程主要基于数学函数和它的思想。javascript
函数是一段能够经过其名称被调用的代码,能够传递参数并返回值。
方法是一段必须经过其名称及其关联对象的名称被调用的代码。php
//函数 var func = (a)=>{return a} func(5) //用其名称调用 //方法 var obj = {simple:(a)=>{return a}} obj.simple(5) //用其名称及其关联对象调用
全部函数对于相同的输入都将返回相同的值(函数只依赖参数的输入,不依赖于其余全局数据,即函数内部没有全局引用),这使并行代码和缓存(用值直接替换函数的结果)成为可能。css
命令式主张告诉编译器“如何”作html
var array = [1,2,3] for(let i=0; o<array.length;i++){ console.log(array[i]) }
声明式告诉编译器“作什么”,如何作的部分(得到数组长度,循环遍历每一项)被抽象到高阶函数中,forEach就是这样一个内置函数,本书中咱们都将建立这样的内置函数。java
var array = [1,2,3] array.forEach(elememt=>console.log(elememt))
好处就是编写纯函数。纯函数是对相同输入返回相同输出的函数,不依赖(包含)任何外部变量,因此也不会产生改变外部环境变量的反作用。web
纯函数容许咱们并行执行代码,由于纯函数不会改变它的环境,因此不须要担忧同步问题。固然,js并无真正的多线程支持并行,但若是你的项目使用了webworker来模拟多线程并行执行任务,这种时候就须要用纯函数来代替非纯函数。编程
let global = 'something'; let function1 = (input) => { global = "somethingElse" } let function2 = ()=>{ if(global === 'something'){ //业务逻辑 } }
若是咱们要两个线程并行执行function1和function2,因为两个函数都依赖全局变量global,并行执行就会引发不良的影响(两个函数的执行顺序不一样会有不一样的结果),如今把它们改成纯函数。c#
let function1 = (input,global)=>{ global = "somethingElse" } let function2 = (global)=>{ if(global === "something"){ //业务逻辑 } }
咱们移动了global变量,把它做为两个函数的参数,使他们变成纯函数。如今并行执行不会有任何问题,因为函数不依赖于外部环境变量,没必要担忧线程的执行顺序。数组
根据纯函数对于给定输入老是返回相同的输出,咱们能够缓存函数的输出,减小屡次的输入来反复调用函数。浏览器
var longRunningFnBookKeeper = {2:3,4:5...} longRunningFnBookKeeper.hasOwnProperty(ip)?longRunningFnBookKeeper[ip]:longRunningFunction(ip)
纯函数应该被设计为:只作一件事。实现多个功能经过函数的组合来实现。
UNIX/LINUX中,在一个文件中找到一个特定的名称并统计它的出现次数:
cat jsBook | grep -i "composing" | wc
组合不是命令行特有的,它是函数式编程的核心。
js是一门面对对象的语言,不是一种纯函数语言,更像是一种多范式语言,可是很是适合函数式编程。
今天不少浏览器还不支持ES6,咱们能够经过转换编译器babel,将ES6转换为ES5代码。
能够看到,箭头函数的this通过编译后为undefined,转换后的代码运行在严格模式下,严格模式是js的受限变体。
"use strict" a = 1 // -> Uncaught ReferenceError: a is not defined; 此处直接报错
在函数内部若是用var声明变量和不用时有很大差异,用var声明的是局部变量,在函数外部访问这个变量是访问不到的,没var声明的是全局变量。在函数外部是能够访问到的。
若是你不使用var命令指定,在全局状态下定一个变量。在严格模式下这段代码会报错,由于全局变量在js中很是有害。
高阶函数(HOC):
知足以上三个之一的函数就是高阶函数。
由于函数也是js中的一种数据类型,能够被赋值给变量,做为参数传递,也可被其余函数返回。
let fn = () => {} //fn就是一个指向函数数据类型的变量,即函数的引用 fn() //调用函数,即执行fn指向的函数
var tellType = arg =>{ if(typeof arg==='function'){ arg() //若是传入的是函数就执行 }else{ console.log(arg) //不然就输出数据 } } var fn = () => {console.log('i am a function')} tellType(fn) //函数做为参数传入
String是js的内置函数,注意:只返回了函数的引用,并无执行函数
let crazy = () =>{ return String } crazy() // String() { [native code] } crazy()('HOC') // "HOC"
高阶函数就是定义抽象
forEach实现遍历数组
const forEach = (array,fn)=>{ for(let i=0;i<array.length;i++){ fn(array[i]) } }
forEachObject实现遍历对象
const forEachObject = (obj,fn)=>{ for(var property in obj){ if(obj.hasOwnProperty(properity){ fn(property,obj[property]) }) } }
注意:forEach和forEachObject都是高阶函数,他们使开发者专一于任务,而抽象出遍历的部分。
unless函数:若是predicate为false,则调用fn
const unless = (predicate,fn)=>{ if(!predicate) fn() }
查找一个列表中的偶数
forEach([1,2,3,4,6,7],(number)=>{ unless((number%2),()=>{ console.log(number,"is even") }) })
若是咱们操做的是一个Number而不是array
const times = (time,fn)=>{ for(var i=0;i<time;i++){ fn(i) } } times(100,function(n){ unless(n%2,function(){ console.log(n, "is even") }) })
a.every(function(element, index, array))
every是全部函数的每一个回调函数都返回true的时候才会返回true,当遇到false的时候终止执行,返回false。
a.some(function(element, index, array))
some函数是“存在”有一个回调函数返回true的时候终止执行并返回true,不然返回false
在空数组上调用every返回true,some返回false。
every函数接受两个参数:一个数组和一个函数。它使用传入的函数检查数组的全部元素是否为true, 都为true才返回true
const every = (arr,fn)=>{ let result = true; for(let i =0;i<arr.length;i++){ result = result&&fn(arr[i]) } return true; } every([NaN,NaN,NaN],isNaN)
for..of循环:ES6中用于遍历数组元素的方法,重写every方法
const every = (arr,fn)=>{ let result = true; for(const element of arr){ result = result&&fn(element) } return true; }
some函数接受两个参数:一个数组和一个函数。它使用传入的函数检查数组的全部元素是否为true, 只要有一个为true就返回true
const some = (arr,fn)=>{ let result = false; for(const element of arr){ result = result||fn(element) } return true; } some([5,NaN,NaN],isNaN)
sort函数是一个高阶函数,它接受一个函数做为参数,该函数帮助sort函数决定排序逻辑, 是一个改变原数组的方法。
arr.sort([compareFunc])
compareFunc是可选的,若是compareFunc未提供,元素将被转换为字符串并按Unicode编码点顺序排列。
compareFunc应该实现下面的逻辑
function compareFunc(a,b){ if(根据某种排序标准a<b){ return -1 } if(根据某种排序标准a>b){ return 1 } return 0; }
具体例子
var friends = [{name: 'John', age: 30}, {name: 'Ana', age: 20}, {name: 'Chris', age: 25}];
function compareFunc(a,b){ return (a.age<b.age)?-1:(a.age>b.age)?1:0 }
写成如下也ok,按照age升序排列
function compareFunc(a,b){ return a.age>b.age }
friends.sort(compareFunc)
若是要比较不一样的属性,咱们须要重复编写比较代码。下面新建一个sortBy函数,容许用户基于传入的属性对对象数组排序。
const sortBy = (property)=>{ return (a,b) => { return (a[property]<b[property])?-1:(a[property]>b[property])?1:0 } }
var friends = [{name: 'John', age: 30}, {name: 'Ana', age: 20}, {name: 'Chris', age: 25}];
friends.sort(sortBy('age'))
注意:sortBy函数接受一个属性冰返回另外一个函数,这个返回的函数就做为compareFunc传递给sort函数,持有property参数值的返回函数之因此可以运行是由于js支持闭包。
简言之,闭包是一个内部函数,它是在一个函数内部的函数。
function outer(){ function inner(){ } }
函数inner称为闭包函数,闭包如此强大的缘由在于它对做用域链的访问。
闭包有3个能够访问的做用域:
1.闭包函数内声明的变量
2.对全局变量的访问
3.对外部函数变量的访问!!!!
let global = 'global';//2 function outer(){ let outer = 'outer'; function inner(){ let a=5;//1 console.log(outer) //3.闭包可以访问外部函数变量 } return inner } outer()()//"outer"
var fn = (arg)=>{ let outer = 'outer'; let innerFn = () =>{ console.log(outer) console.log(arg) } return innerFn } var closeureFn = fn(5) closeureFn()//outer 5
当执行var closeureFn = fn(5)
时,函数innerFn被返回,js执行引擎视innerFn为一个闭包,并相应的设置了它的做用域。3个做用域层级在innerFn返回时都被设置了。
如此,closeureFn()经过做用域链被调用时就记住了arg、outer的值。
咱们回到sortBy
const sortBy = (property)=>{ return (a,b) => { return (a[property]<b[property])?-1:(a[property]>b[property])?1:0 } }
当咱们以以下形式调用时
sortBy('age')
发生下面的事情:
sortBy函数返回了一个接受两个参数的新函数,这个新函数就是一个闭包
(a,b)=>{/*实现*/}
根据闭包能访问做用域层级的特色,它能在它的上下文中持有property的值,因此它将在合适而且须要的时候使用返回值。
这在开发过程当中很常见,例如只想设置一次第三方库,初始化一次支付设置。
const once = (fn)=>{ let done = false; return function(){ return done?undefined:((done=true),fn.apply(this,arguments)) } }
var dopayment = once(()=>{console.log("Payment is done")}) dopayment() //Payment is done dopayment() //undefined
js中,(exp1,exp2)的含义是执行两个参数并返回第二个表达式的结果。
注意:once函数接受一个参数fn并经过调用fn的apply方法返回结果。咱们声明了done变量,返回的函数会造成一个覆盖它的闭包做用域,检查done是否为true,若是是则返回undefined,
不然将done设为true,如此就阻止了下一次的执行。
用于为每个输入存储结果,以便于重用函数中的计算结果。
const memoized = (fn) => { const lookupTable = {}; return (arg) => lookupTable[arg] || (lookupTable[arg]=fn(arg)); }
有一个名为lookupTable的局部变量,它在返回函数的闭包上下文中。返回函数将接受一个参数并检查它是否在lookupTable中。
若是在,就返回对应的值,不然使用新的输入做为key,fn(arg)的结果为value,更新lookupTable对象。
求函数的阶乘(递归法)
var factorial = (n) => { if(n===0){ return 1; } return n*factorial(n-1) }
如今能够改成把factorial函数包裹进一个memoized函数来保留它的输出(存储结果法)
let factorial = memoized((n)=>{ if(n===0){ return 1; } return n*factorial(n-1) })
它以一样的方式运行,可是比以前快的多。
咱们使用数组来存储、操做和查找数据,以及转换(投影)数据格式。本章中使用函数式编程来改进这些操做。
本节建立的全部函数称为投影函数,把函数应用于一个值并建立一个新值的过程称为投影。
首先来看遍历数组的forEach方法
const forEach = (array,fn) => { for(const value of array) fn(value) }
map函数的实现代码以下
const map = (array,fn) => { let results= []; for(const value of array) results.push(fn(value)) return results; }
map和forEach很是相似,区别是用一个新的数组捕获告终果,并返回告终果。
let apressBooks = [ { "id": 111, "title": "c# 6.0", "author": "ANDREW JKDKS", "rating": [4], "reviews": [{good:4, excellent: 12}] }, { "id": 222, "title": "Machine Learning", "author": "ANDREW JKDKS", "rating": [3], "reviews": [{good:4, excellent: 12}] }, { "id": 333, "title": "Angularjs", "author": "ANDREW JKDKS", "rating": [5], "reviews": [{good:4, excellent: 12}] }, { "id": 444, "title": "Pro ASP.NET", "author": "ANDREW JKDKS", "rating": [4.7], "reviews": [{good:4, excellent: 12}] } ];
假设只须要获取包含title和author的字段
map(apressBooks,(book)=>{ return {title:book.title,author:book.author} })
有时咱们还想过滤数组的内容(例如获取rating>4.5的图书列表),再转换为一个新数组,所以咱们须要一个相似map的函数,它只须要在把结果放入数组前检查一个条件。
const filter = (array,fn) => { let results= []; for(const value of array) fn(value) ? results.push(value) : undefined return results; }
调用高阶函数filter
filter(apressBooks, (book)=>book.rating[0]>4.5)
返回结果
map和filter都是投影函数,所以它们老是对数组应用转换操做后再返回数据,因而咱们可以链接filter和map(注意顺序)来完成任务而不须要额外变量。
例如:从apressBooks中获取含有title和author对象且评级高于4.5的对象。
map(filter(apressBooks, (book)=>book.rating[0]>4.5),(book)=>{ return {title:book.title,author:book.author} })
咱们将后面的章节中国经过函数组合来完成一样的事情。
对apressBooks对象稍做修改,获得以下数据结构
let apressBooks = [ { name: "beginners", bookDetails: [ { "id": 111, "title": "c# 6.0", "author": "ANDREW 1", "rating": [4], "reviews": [{good:4, excellent: 12}] }, { "id": 222, "title": "Machine Learning", "author": "ANDREW 2", "rating": [3], "reviews": [{good:4, excellent: 12}] } ] }, { name: "pro", bookDetails: [ { "id": 333, "title": "Angularjs", "author": "ANDREW 3", "rating": [5], "reviews": [{good:4, excellent: 12}] }, { "id": 444, "title": "Pro ASP.NET", "author": "ANDREW 4", "rating": [4.7], "reviews": [{good:4, excellent: 12}] } ] } ];
如今回顾上一节的问题:获取含有title和author字段且评级高于4.5的图书。
map(apressBooks,(book)=>{ return book.bookDetails })
获得以下输出
如上图所示,map函数返回的数据包含了数组中的数组,若是把上面的数据传给filter将会遇到问题,由于filter不能在嵌套数组上运行。
咱们定义一个concatAll函数把全部嵌套数组链接到一个数组中,也可称concatAll为flatten方法(嵌套数组平铺)。concatAll的主要目的是将嵌套数组转换为非嵌套的单一数组。
const concatAll = (array) => { let results = []; for(const value of array){ results.push.apply(results,value) //重点!! } return results; }
使用js的apply方法,将push的上下文设置为results
concatAll(map(apressBooks,(book)=>{ return book.bookDetails }))
返回了咱们指望的结果(数组平铺)
转换为非嵌套的单一数组后就能够继续使用filter啦
filter(
concatAll(map(apressBooks,(book)=>{
return book.bookDetails })),(book) => (book.rating[0] > 4.5) )
返回结果
let arr = [[1,2,[3,4]],[4,5],77]
遍历每一项,若是还是数组的话就递归调用flatten,并将结果与result concat一下。若是不是数组就直接push该项到result。
function flatten(array){ var result = []; var toStr = Object.prototype.toString; for(var i=0;i<array.length;i++){ var element = array[i]; if(toStr.call(element) === "[object Array]"){ //Array.isArray(element) === true result = result.concat(flatten(element)); //[...result,...flatten(element)] } else{ result.push(element); } } return result; } let results = flatten(arr)
reduce为保持Javascript闭包的能力所设计。
先来看一个数组求和问题:
let useless = [2,5,6,1,10] let result = 0; forEach(useless,value=>{ result+=value; }) console.log(result) //24
对于上面的问题,咱们将数组归约为一个单一的值,从一个累加器开始(result),在遍历数组时使用它存储求和结果。
归约数组:设置累加器并遍历数组(记住累加器的上一个值)以生成一个单一元素的过程称为归约数组。
咱们将这种归约操做抽象成reduce函数。
reduce函数的第一个实现
const reduce = (array,fn)=>{ let accumlator = 0; for(const value of array){ accumlator = fn(accumlator,value); } return [accumlator] } reduce(useless,(acc,val)=>acc+val) //[24]
但若是咱们要求给定数组的乘积,reduce函数会执行失败,主要是由于咱们使用了累加器的值0。
咱们修改reduce函数,让它接受一个为累加器设置初始值的参数。
若是没有传递initialValue时,则以数组的第一个元素做为累加器的值。
const reduce = (array,fn,initialValue)=>{ let accumlator; if(initialValue != undefined) accumlator = initialValue; else accumlator = array[0]; //当initialValue未定义时,咱们须要从第二个元素开始循环数组 if(initialValue === undefined){ for(let i=1; i<array.length;i++){ accumlator = fn(accumlator,array[i]) } }else{//若是initialValue由调用者传入,咱们就须要遍历整个数组。 for(const value of array){ accumlator = fn(accumlator,value); } } return [accumlator] }
尝试经过reduce函数解决乘积问题
let useless = [2,5,6,1,10] reduce(useless,(acc,val)=>acc*val,1) //[600]
reduce使用举例
从apressBooks中统计评价为good和excellent的数量。->使用reduce
因为apressBooks包含数组中的数组,先须要使用concatAll把它转化为一个扁平的数组。
concatAll(map(apressBooks,(book)=>{ return book.bookDetails }))
咱们使用reduce解决该问题。
let bookDetails = concatAll(map(apressBooks,(book)=>{ return book.bookDetails })) reduce(bookDetails,(acc,bookDetail)=>{ let goodReviews = bookDetail.reviews[0] != undefined ? bookDetail.reviews[0].good : 0 let excellentReviews = bookDetail.reviews[0] != undefined ? bookDetail.reviews[0].good : 0 return {good:acc.good + goodReviews, excellent:acc.excellent + excellentReviews} },{good:0,excellent:0})
在reduce函数体中,咱们获取good和excellent的评价详情,将其存储在相应的变量中,名为goodReviews和excellentReviews。
再回顾一下以前数据的结构,咱们在apressBooks的bookDetails中获取reviews,并能轻松的操做它。
let apressBooks = [ { name: "beginners", bookDetails: [ { "id": 111, "title": "c# 6.0", "author": "ANDREW 1", "rating": [4], "reviews": [{good:4, excellent: 12}] }, { "id": 222, "title": "Machine Learning", "author": "ANDREW 2", "rating": [3], "reviews": [{good:4, excellent: 12}] } ] }, { name: "pro", bookDetails: [ { "id": 333, "title": "Angularjs", "author": "ANDREW 3", "rating": [5], "reviews": [{good:4, excellent: 12}] }, { "id": 444, "title": "Pro ASP.NET", "author": "ANDREW 4", "rating": [4.7], "reviews": [{good:4, excellent: 12}] } ] } ];
可是有时候数据可能被分离到不一样部分了。
let apressBooks = [ { name: "beginners", bookDetails: [ { "id": 111, "title": "c# 6.0", "author": "ANDREW 1", "rating": [4] }, { "id": 222, "title": "Machine Learning", "author": "ANDREW 2", "rating": [3], "reviews": [] } ] }, { name: "pro", bookDetails: [ { "id": 333, "title": "Angularjs", "author": "ANDREW 3", "rating": [5], "reviews": [] }, { "id": 444, "title": "Pro ASP.NET", "author": "ANDREW 4", "rating": [4.7] } ] } ];
reviews被填充到一个单独的数组中。
let reviewDetails = [
{
"id":111, "reviews":[{good:4,excellent:12}] }, { "id":222, "reviews":[] }, { "id":111, "reviews":[] }, { "id":111, "reviews":[{good:4,excellent:12}] }, ]
zip函数
const zip = (leftArr,rightArr,fn) => { let index,results=[]; for(index=0;index<Math.min(leftArr.length,rightArr.length);index++){ results.push(fn(leftArr[index],rightArr[index])); } return results; }
zip:咱们只须要遍历两个给定的数组,因为要处理两个数组详情,就须要用 Math.min 获取它们的最小长度Math.min(leftArr.length, rightArr.length)
,一旦获取了最小长度,咱们就可以用当前的leftArr值和rightArr值调用传入的高阶函数fn。
假设咱们要把两个数组的内容相加,能够采用以下方式使用zip
zip([1,2,3],[4,5,6],(x,y)=>x+y)
继续解决上一节的问题:统计Apress出版物评价为good和excellent的总数。
咱们接受bookDetails和reviewDetails数组,检查两个数组元素的id是否匹配,若是是,就从book中克隆出一个新的对象clone
//获取bookDetails let bookDetails = concatAll(map(apressBooks,(book)=>{ return book.bookDetails })) //zip results let mergedBookDetails = zip(bookDetails, reviewDetails, (book, review)=>{ if(book.id === review.id){ let clone = Object.assign({},book) clone.ratings = review //为clone添加一个ratings属性,以review对象做为其值 return clone } })
注意:Object.assign(target, ...sources)
Object.assign() 方法用于将全部可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。
let clone = Object.assign({},book)
clone获得了一份book 对象的副本,clone指向了一个独立的引用,为clone添加属性或操做不会改变真实的book引用。
只接受一个参数的函数称为一元(unary)函数。
const identity = (x) => x
接受两个参数的函数称为二元(binary)函数。
const add = (x,y) => x+y;
指函数接受的参数数量是可变的。ES5中咱们经过arguments来捕获可变数量的参数。
function variadic(a){ console.log(a) console.log(arguments) }
调用
variadic(1,2,3) 1 [1,2,3]
ES6中咱们使用扩展运算符,得到可变参数
const variadic = (a,...variadic){ console.log(a) console.log(variadic) }
调用
variadic(1,2,3) 1 [2, 3]
柯里化:把一个多参数函数转换为一个嵌套的一元函数的过程。
看个例子,假设有一个名为add的函数
const add = (x,y)=>x+y;
咱们会如此调用该函数add(1,1),获得结果2。下面是add函数的柯里化版本:
const addCurried = x => y => x+y;
若是咱们用一个单一的参数调用addCurried,
addCurried(3)
它返回一个函数,在其中x值经过闭包被捕获,fn = y => 4+y
,所以能够用以下方式调用addCurried
addCurried(3)(4) //7
相似的乘法函数
const curri = x=>y=>z=>x*y*z; curri(2)(3)(4) //24
下面展现了如何把该处理过程转换为一个名为curry的方法, curry方法将接收到的函数参数curry化
const curry = (binaryFn) => { return function(firstArg){ return function(secondArg){ return binaryFn(firstArg,secondArg) } } }
调用curry函数,curry化add。
const add = (x,y)=>x+y; let autoCurried = curry(add) autoCurried(2)(3) //5
假设咱们要编写一个建立列表的函数,建立列表tableOf二、tableOf三、tableOf4等。
const tableOf2 = (y) => 2*y; const tableOf3 = (y) => 3*y; const tableOf4 = (y) => 4*y;
如今能够把表格的概念归纳为一个单独的函数
const genericTable = (x,y) => x*y
咱们将genericTable柯里化,用2填充tableOf2的第一个参数,用3填充tableOf3的第一个参数,用4填充tableOf4的第一个参数。
const tableOf2 = curry(genericTable)(2); const tableOf3 = curry(genericTable)(3); const tableOf4 = curry(genericTable)(4);
添加规则,检查若是传入参数不是function,就会报错。
let curry = (fn) => { if(typeof fn!=='function'){ throw Error('No function provided') } }
若是有人为柯里化函数提供了全部的参数,就须要经过传递这些参数执行真正的函数,重点在于返回函数curriedFn是一个变参函数。
let curry = (fn) => { if(typeof fn!=='function'){ throw Error('No function provided') } return function curriedFn(){ //返回函数是一个变参函数 return fn(...arguments) } //采用以下写法也ok // return function curriedFn(...args){ // return fn(...args) // } }
若是咱们有一个名为multiply的函数:
const multiply = (x,y,z) => x*y*z;
能够经过以下方式调用,等价于multiply(1,2,3)
curry(multiply)(1,2,3) //6
下面回到把多参数函数转换为嵌套的一元函数(柯里化的定义)
let curry = (fn) => { if(typeof fn!=='function'){ throw Error('No function provided') } return function curriedFn(...args){ //args是一个数组 if(args.length < fn.length){ //检查...args传入的参数长度是否小于函数参数列表的长度 return function(){ args = [...args,...arguments] return curriedFn(...args) }; } return fn(...args) //不小于,就和以前同样调用整个函数 } }
args.length < fn.length
检查...args传入的参数长度是否小于函数参数列表的长度,若是是,就进入if代码块,若是不是就如以前同样调用整个函数。
args = [...args,...arguments]
用来链接一次传入的参数,把他们合并进args,并递归调用curriedFn。因为咱们将全部传入的参数 组合并递归地调用,再下一次调用中将会遇到某一个时刻if(args.length < fn.length)
条件失败,说明这时args存放的参数列表的长度和函数参数的长度相等,程序就会被调用 fn(...args)。
调用
const multiply = (x,y,z) => x*y*z; curry(multiply)(1)(2)(3) //6
开发者在写代码时候会在应用的不一样阶段编写不少日志。咱们编写以下日志函数。
在数组中查找数字,返回包含数字的数组内容。
无需柯里化时,咱们能够以下实现。
["js","number1"].filter(function(e){ return /[0-9]+/.test(e) //["number1"] })
采用柯里化的filter函数
let filter = curry((fn,ary)=>{ return ary.filter(fn) }) filter(function(str){ return /[0-9]+/.test(str); })(["js","number1"]) //["number1"]
前几章中,咱们使用map函数传入一个平凡函数来解决问题,此处能够经过curry函数以另外一种方式解决该问题。
let map = curry(function(f,ary){ return ary.map(f) }) map(x=>x*x)([1,2,3]) //[1, 4, 9]
咱们设计的柯里化函数总在最后接受数组,这是有意而为之。若是咱们但愿最后接受的参数是位于参数列表的中间某位置呢?curry就帮不了咱们了。
偏应用:部分地应用函数参数。有时填充函数的前两个参数和最后一个参数会使中间的参数处于一种未知状态,这正是偏应用发挥做用的地方,将未知状态的参数填充为undefined,以后填入其余参数调用函数。
setTimeout(()=>console.log("Do X task"),10) setTimeout(()=>console.log("Do Y task"),10)
咱们为每个setTimeout函数都传入了10,咱们但愿把10做为常量,在代码中把它隐藏。curry函数并不能帮咱们解决这个问题,缘由是curry函数应用参数列表的顺序是从最左到最右。
一个方案是把setTimeout封装一下,如此函数参数就会变成最右边的一个。
const setTimeoutWrapper = (time,fn)=>{ setTimeout(fn,time); }
而后就能经过curry函数来实现一个10ms的延迟了
const delayTenMs = curry(setTimeoutWrapper)(10) delayTenMs(()=>console.log("Do X task")) delayTenMs(()=>console.log("Do Y task"))
程序将以咱们须要的方式运行,但问题是建立了setTimeoutWrapper这个封装器,这是一种开销。
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) } }
使用该偏函数
let delayTenMs = partial(setTimeout,undefined,10); delayTenMs(()=>console.log("Do Y task"))
说明:
咱们调用
partial(setTimeout,undefined,10);
这将产生
let args = partialArgs = [undefined,10]
返回函数将记住args的值(闭包)
返回函数很是简单,它接受一个名为fullArguments的参数。因此传入()=>console.log("Do Y task")
做为参数,
在for循环中咱们执行遍历并为函数建立必需的参数数组
if(args[i]===undefined){ args[i] = fullArguments[arg++]; }
从i=0开始,
返回函数将记住args的值,返回函数很是简单,它接受一个名为fullArguments的参数。因此传入
fullArguments = [()=>console.log("Do Y task")]
在if循环内
args[0]===undefined=>true
args[0]=()=>console.log("Do Y task")
如此args就变成
[()=>console.log("Do Y task"),10]
能够看出,args指向咱们指望的setTimeout函数调用所需的数组,一旦在args中有了必要的参数,就能够经过fn.apply(null,args)调用函数了。
partial应用
注意,咱们能够将partial应用于任何含有多个参数的函数,看下面的例子。js中使用JSON.stringify() 方法将一个JavaScript值(对象或者数组)转换为一个 JSON字符串。
JSON.stringify(value[, replacer[, space]])
value:
必需, 要转换的 JavaScript 值(一般为对象或数组)。
replacer:
可选。用于转换结果的函数或数组。
若是 replacer 为函数,则 JSON.stringify 将调用该函数,并传入每一个成员的键和值。使用返回值而不是原始值。若是此函数返回 undefined,则排除成员。根对象的键是一个空字符串:""。
若是 replacer 是一个数组,则仅转换该数组中具备键值的成员。成员的转换顺序与键在数组中的顺序同样。
space:
可选,文本添加缩进、空格和换行符,若是 space 是一个数字,则返回值文本在每一个级别缩进指定数目的空格,若是 space 大于 10,则文本缩进 10 个空格。space 也可使用非数字,如:\t。
咱们调用下面的函数作JSON的美化输出。
let obj = {obj:"bar",bar:"foo"} JSON.stringify(obj,null,2); 输出: "{ "obj": "bar", "bar": "foo" }"
能够看到stringify调用的最后两个参数老是相同的“null,2”,咱们能够用partial移除样板代码
let prettyPrintJson = partial(JSON.stringify, undefined, null, 2) prettyPrintJson({obj:"bar",bar:"foo"}) 输出: "{ "obj": "bar", "bar": "foo" }"
该偏函数的小bug:
若是咱们使用一个不一样的参数再次调用prettyPrintJson,它将老是给出第一次调用的结果。
prettyPrintJson({obj:"bar",bar:"foo222"}) 输出:老是给出第一次调用的结果 "{ "obj": "bar", "bar": "foo" }"
由于咱们经过参数替换undefined值的方式修改partialArgs,而数组传递的是引用。
函数式组合:将多个函数组合在一块儿以便能构建出一个新函数。
1.每一个程序只作好一件事情。
2.每一个程序的输出应该是另外一个尚不可知的程序的输入。
|
使用Unix管道符号|
,就能够将左侧的函数输出做为右侧函数的输入。
若是想计算单词word在给定文本文件中的出现次数,该如何实现呢?
cat test.txt | grep 'world' | wc
cat用于在控制台现实文本文件的内容,它接受一个参数(文件位置)
grep在给定的文本中搜索内容
wc计算单词在给定文本中的数量
本节建立第一个compose函数,它须要接收一个函数的输出,并将其做为输入传递给另一个函数。
const compose = (a, b)=>(c)=>a(b(c))
compose 接收函数a 、b做为输入,并返回一个接收参数c的函数。当用c调用返回函数时,它将用输入c调用函数b,b的输出做为a的输入,这就是compose函数的定义。
注意:函数的调用方向是从右至左的。
例子1:对一个给定的数字四舍五入求和。
let data = parseFloat("3.56") let number = Math.round(data) //4
下面经过compose函数解决该问题:
const compose = (a, b)=>(c)=>a(b(c)) let number = compose(Math.round, parseFloat) number("3.56") //4
以上就是函数式组合,咱们将两个函数(Math.round、parseFloat)组合在一块儿以便能构造出一个新函数,注意:Math.round和parseFloat知道调用number函数时才会执行。
例子2:计算一个字符串中单词的数量
已有如下两个函数:
let splitIntoSpaces = (str) => str.split(" ") let count = (array) => array.length;
若是想用这两个函数构建一个新函数,计算一个字符串中单词的数量。
const countWords = compose(count, splitIntoSpaces)
调用
countWords("hello what's your name") // 4
以上的例子中,仅当函数接收一个参数时,咱们才能将两个函数组合。但还存在多参数函数的状况,咱们能够经过curry和partial函数来实现。
5.2中,咱们经过如下写法从apressBooks中获取含有title和author对象且评级高于4.5的对象。
map(filter(apressBooks, (book)=>book.rating[0]>4.5),(book)=>{ return {title:book.title,author:book.author} })
本节使用compose函数将map和filter组合起来。
compose只能组合接受一个参数的函数,可是map和filter都接受两个参数map(array,fn)
filter(array,fn)
(数组,操做数组的函数),不能直接将他们组合。咱们使用partial函数部分地应用map和filter的第二个参数。
咱们定义了过滤图书的小函数filterGoodBooks和投影函数projectTitleAndAuthor
let filterGoodBooks = (book)=>book.rating[0]>4.5; let projectTitleAndAuthor = (book)=>{return {title:book.title, author:book.author}}
如今使用compose和partial实现
let queryGoodBooks = partial(filter, undefined, filterGoodBooks); let mapTitleAndAuthor = partial(map, undefined, projectTitleAndAuthor); let titleAndAuthorForGoodBooks = compose(mapTitleAndAuthor, queryGoodBooks)
使用
titleAndAuthorForGoodBooks(apressBooks) 输出: 0: {title: "Angularjs", author: "ANDREW JKDKS"} 1: {title: "Pro ASP.NET", author: "ANDREW JKDKS"}
本例子使用partial和compose解决问题,也能够用curry来作一样的事情。
提示:颠倒map和filter的参数顺序。
const mapWrap = (fn,array)=>{ return map(array,fn) }
当前的compose只能组合两个给定的函数,咱们重写compose函数,使它能组合三个、四个、更多函数。
const compose = (...fns) => (value) => reduce(fns.reverse(), (acc, fn) => fn(acc), value)
reduce用于把数组归约为一个单一的值,例如求给定数组的元素乘积,累乘器初始值为1
reduce([1,2,3,4],(acc,val)=>acc*val,1) //24
此处经过fns.reverse()反转函数数组,(acc, fn) => fn(acc)以传入的acc为参数依次调用每个函数。累加器的初始值是value变量,它做为函数的第一个输入。
上一节中,咱们组合了一个函数用于计算给定字符串的单词数。
let splitIntoSpaces = (str) => str.split(" ") let count = (array) => array.length; const countWords = compose(count, splitIntoSpaces) countWords("hello what's your name") // 4
假设咱们想知道给定字符串的单词数是基数仍是偶数,而咱们已经有以下函数
let oddOrEven = (ip) => ip%2 == 0 ? "even" : "odd";
经过compose,将这三个函数组合起来
const oddOrEvenWords = compose(oddOrEven, count, splitIntoSpaces); oddOrEvenWords("hello what's your name") // ["even"]
compose的数据流是从右至左的,最右侧的函数会首先执行,将数据传递给下一个函数,以此类推...最左侧的函数最后执行。
而当咱们进行“|”操做时,Unix命令的数据流老是从左至右的,本节中,咱们将实现pipe,它和compose函数所作的事情相同,只不过交换了数据流方向。
管道/序列(pipeline/sequence):从左至右处理数据流的过程称为管道/序列。
pipe是compose的复制品,惟一修改的是数据流方向。
const pipe = (...fns) => (value) => reduce(fns, (acc, fn) => fn(acc), value)
此处没有像compose同样调用fns.reverse(),这意味着咱们将按照原有顺序执行函数。
调用pipe函数。注意,咱们改变了调用顺序,先splitIntoSpaces, 中count, 最后oddOrEven。
const oddOrEvenWords = pipe(splitIntoSpaces, count, oddOrEven); oddOrEvenWords("hello what's your name") // ["even"]
函数式组合知足结合律:
compose(compose(f,g),h) == compose(f,compose(g,h))
看一下上一节的例子
//compose(compose(f,g),h) const oddOrEvenWord1 = compose(compose(oddOrEven, count), splitIntoSpaces); oddOrEvenWord1("hello what's your name") //["even"] //compose(f,compose(g,h)) const oddOrEvenWord2 = compose(oddOrEven, compose(count, splitIntoSpaces)); oddOrEvenWord2("hello what's your name") //["even"]
真正的好处:把函数组合到各自所需的compose函数中,
let countWords = compose(count, splitIntoSpaces) let oddOrEvenWords = compose(oddOrEven, countWords)
or
let countOddOrEven= compose(oddOrEven, count) let oddOrEvenWords = compose(countOddOrEven, splitIntoSpaces)
函子:用一种纯函数式的方式进行错误处理。
函子是一个实现了map(遍历每一个对象值的时候生成一个新对象)的普通对象(在其余语言中多是一个类)。简而言之,函子是一个持有值的容器,可以持有任何传给它值,并容许使用当前容器持有的值调用任何函数。
建立Container构造函数
const Container = function(val){ this.value = val; }
不使用箭头函数的缘由是箭头函数不具有内部方法Construct和prototype属性,因此不能用new来建立一个新对象。
应用Container
let testValue = new Container(3) //Container {value: 3} let testObj = new Container({a:1}) //Container {value: {a: 1}} let testArray = new Container([1,2]) //Container {value: [1,2]}
咱们为Container建立一个of静态工具方法,用以代替new关键词使用
Container.of = function(value){ return new Container(value) }
用of方法重写上面的代码
testValue = Container.of(3) testObj = Container.of(3) testArray = Container.of([1,2])
注意:Container也能够包含嵌套的 Container
Container.of(Container.of(33))
输出:
Container { value: Container { value: 33 } }
map方法容许咱们使用当前Container持有的值调用任何函数。
即map函数从Container中取出值,将传入的函数做用于该值,再将结果放回Container。
Container.prototype.map = function(fn){ return Container.of(fn(this.value)) }
Generator是ES6中关于函数的新规范。它不是一种函数式编程技术,但它是函数的一部分。
同步:函数执行时会阻塞调用者,并在执行完后返回结果。
异步:在执行时不会阻塞调用者,一旦执行完毕就会返回结果。
处理Ajax请求时就是在处理异步调用。
同步函数
let sync = () =>{ //一些操做 //返回数据 } let sync2 = () =>{ //一些操做 //返回数据 } let sync3 = () =>{ //一些操做 //返回数据 }
同步函数调用
result = sync() result2 = sync2() result3 = sync3()
异步函数
let async = (fn)=>{ //一些异步操做 //用异步操做调用回调 fn(/*结果数据*/) } let async2 = (fn)=>{ //一些异步操做 //用异步操做调用回调 fn(/*结果数据*/) } let async3 = (fn)=>{ //一些异步操做 //用异步操做调用回调 fn(/*结果数据*/) }
异步函数调用
async(function(x){ async2(function(y){ async3(function(z){ ... }) }) })
Generator是ES6规范的一部分,被捆绑在语言层面。
function* gen(){ return 'first generator'; } gen()
返回一个Generator原始类型的实例
调用实例的next函数,从该Generator实例中获取值
gen().next() 输出: {value: "first generator", done: true} gen().next().value 输出: "first generator"
一:不能无限制地调用next从Generator中取值
let genResult = gen() //第一次调用 genResult.next().value 输出:"first generator" //第二次调用 genResult.next().value 输出:undefined
缘由是Generator如同序列,一旦序列中的值被消费,你就不能再次消费它。
本例中,genResult是一个带有"first generator"值的序列,第一次调用next后,咱们就已经从序列中消费了该值。
因为序列已为空,第二次调用它就会返回undefined。
为了可以再次消费该序列,方法是建立另外一个Generator实例
let genResult = gen() let genResult2 = gen() //第一个序列 genResult.next().value 输出:"first generator" //第二个序列 genResult2.next().value 输出:"first generator"
来看一个简单的Generator序列
function* generatorSequence(){ yield 'first'; yield 'second'; yield 'third'; }
建立实例并调用
let genSequence = generatorSequence(); genSequence.next().value //"first" genSequence.next().value //"second" genSequence.next().value //"third"
yield让Generator惰性的生成一个值的序列。(直到调用才会执行)
yield使Generator函数暂停了执行并将结果返回给调用者,而且它还准确地记住了暂停的位置。下一次调用时就从中断的地方恢复执行。
done是一个判断Generator序列已经被彻底消费的属性。当done为true时就应该中止调用Generator实例的next。
let genSequence = generatorSequence(); genSequence.next() //{value: "first", done: false} genSequence.next() //{value: "second", done: false} genSequence.next() //{value: "third", done: false} genSequence.next() //{value: undefined, done: true}
下面的for...of循环用于遍历Generator
function* generatorSequence(){ yield 'first'; yield 'second'; yield 'third'; } for(let value of generatorSequence()){ console.log(value) //first second third }
function* sayFullName(){ var firstName = yield; var secondName = yield; console.log(firstName+secondName) } let fullName = sayFullName(); fullName.next() fullName.next('xiao ') fullName.next('ming') 输出:xiao ming
分析:第一次调用fullName.next()
时,代码将返回并暂停于var firstName = yield;
第二次调用yield
被'xiao '替换,暂停在var secondName = yield;
,第三次调用yield被'ming'替换,再也不有yield。
let getDataOne = (cb) => { setTimeout(function(){ //调用函数 cb('dummy data one') },1000) } let getDataTwo = (cb) => { setTimeout(function(){ //调用函数 cb('dummy data two') },1000) }
调用
getDataOne((data)=>console.log(data)) //1000毫秒以后打印dummy data one getDataTwo((data)=>console.log(data)) //1000毫秒以后打印dummy data two
下面改造getDataOne和getDataTwo函数,使其使用Generator实例而不是回调来传送数据
let generator; let getDataOne = () => { setTimeout(function(){ //调用Generator,经过next传递数据 generator.next('dummy data one') },1000) } let getDataTwo = () => { setTimeout(function(){ //调用Generator,经过next传递数据 generator.next('dummy data two') },1000) }
将getDataOne和getDataTwo调用封装到一个单独的Generator函数中
function* main(){ let dataOne = yield getDataOne(); let dataTwo = yield getDataTwo(); console.log(dataOne) console.log(dataTwo) }
用以前声明的generator变量为main建立一个Generator实例。该Generator实例被getDataOne和getDataTwo同时用于向其调用传递数据。generator.next()
用于触发整个过程。main 函数开始执行,并遇到了第一个yield:let dataOne = yield getDataOne();
generator = main()
generator.next()
console.log("first be printed") 输出: first be printed 1000毫秒以后打印 dummy data one dummy data two
main代码看上去是在同步的调用getDataOne和getDataTwo,但其实两个调用都是异步的。
有一点须要注意:虽然yield使语句暂停了,但它不会让调用者阻塞。
generator.next() //虽然Generator为异步代码暂停了 console.log("first be printed") //console.log正常执行,说明generator.next不会阻塞执行