[关闭]
@hpp157 2016-09-17T00:03:10.000000Z 字数 16606 阅读 2564

React学习手册chap3

第三章 js函数式编程

React javascript


函数式编程最近引起了一些争议,完整的函数式编程语言像Haskell,Clojure,和Scala 等在被google,Facebook,Linkedin,和Netflix这些科技巨头们广泛的使用。
虽然流行的编程语言,像Javascript,Python,和ruby支持函数式编程,但是它们并不被看作是完整的函数式编程语言。许多有影响力的编程语言都支持函数式编程,java和C++用lambdas来支持函数式编程。

如果你好奇函数式编程什么时候开始流行的,答案是在20世纪30年代,lambda演算的发明。历史上,从17世纪诞生以来,函数就是表达式的一部分。函数可以作为一个参数传到另一个函数中或者把函数作为一个结果,返回。更复杂的高阶函数可以操作函数本身,既可以做参数,也可以作为结果来返回。20世纪30年代,阿隆佐·邱奇(Alonzo Church)在普林斯顿研究lambda演算或者叫λ-lambda时搞出了这些高阶函数。
有趣的 Scala 语言: 函数成了一等公民

20世纪50年代,约翰·麦卡锡(John McCarthy)把从lambda演算的理念应用到了一个新的编程语言中:Lisp。lisp并不全是函数式的,仍旧和Javascript以及Python一样属于函数式编程范式。Lisp实践了高阶函数以及函数是一等公民的理念。函数可以被看作是第一等的公民,它可以被声明为一个变量以及作为函数的参数进行传递。这些函数甚至可以返回函数。

函数式编程并不是新流行起来的一个东西,它是一个过去的编程理念的回归。当然你也可能碰巧已经正在使用Javascript函数式编程来写应用了,只不过你还没有意识到而已。如果你已经使用过mapped或者reduced来操作一个数组,那么你正在成为一个函数式编程的程序员。React,Redux[riː'dʌks]Flux[flʌks],都适用于js的函数式编程范式(progradim),理解函数式编程的基本概念有助于更好的掌握React应用的结构。

本章,我们继续介绍如何使用js实现函数式编程以及这些技术是怎么给予了React和Flux灵感的。同时我们会 介绍Flux模式,从函数式编程中诞生的一个设计架构。

什么是函数式编程

许多工程师都在争议这样一个问题:是否一个语言只要支持函数式编程就可以叫做函数式语言,为了成为一个函数式编程的程序员,你必须在是或者不是之间做出一个选择。

完全的函数式编程语言像Haskell或者Clojure严格执行了函数式编程范式。其他的编程语言,像Javascript和Python支持函数式编程,但同时它们也支持其它的编程范式(programing paradigm),许多工程师认为,只有单遵循函数式编程范式的语言才可以看作是完全的函数式编程语言。

同样的争论在面向对象编程中也出现过。javascript并不是像Java或者C++这样的传统的面向对象编程。js还支持对象和继承,所以我们可以在我们的应用中同时使用面向对象编程设计模式,像MVC架构等。尽管js并不是一个面向对象的编程语言,但是它支持面向对象和原型继承。ES5通过Object.create和Object.defineProperties增强了这一支持。ES6通过引入类这一概念更进一步的增强了对面向对象的支持。

Javascript支持函数式编程是因为js中的函数是一等公民,意味着函数可以当作变量来使用。ES6添加了箭头函数,Promises,和扩展操作符增强了函数式编程的特性。

一等公民到底是什么意思呢?意思就是函数即变量,它们在你的程序中也可以代表数据,你可能注意到了,你可以用var关键字来声明一个函数,就像声明字符串,数字,和其他任何变量一样。

  1. var log = function(message){
  2. console.log(message)
  3. };
  4. log(in js functions are variables')

在ES6中,你可以使用箭头函数实现上面一样的效果。函数式编程程序员写许多的小函数和箭头函数,来让它变得容易,它们在变量log中存储一个函数。此外使用const关键字来声明第二个函数,用来防止log变量被重写。

  1. const log = message=>console.log(message)

因为函数就是变量,所以我们可以把函数用到对象里。

  1. const obj ={
  2. message:"they can be added to objects like variable",
  3. log(message){
  4. console.log(message)
  5. }
  6. }
  7. obj.log(obj.message)//they can be added to objects like variable

我们还可以把函数写到一个数组里边。

  1. const message =[
  2. "they can be insert into arrays", message => console.log(message),"like variables", message => console.log(message)
  3. ]
  4. messages[1](messages[0]) // message[1]是函数,里边传入了一个,message[0] 结果为They can be inserted into arrays
  5. messages[3](messages[2]) // like variables

函数可以作为另一个函数的参数传递,就像其他的变量一样。

  1. const insideFn = logger => logger("They can be sent to other functions as arguments");
  2. insideFn(message => conlole.log(message))//"They can be sent to other functions as arguments

还可以作为一个结果返回。

  1. var createScreen = function(logger){
  2. return function(message){
  3. logger(message.toUppercase()+"!!!"
  4. }
  5. }
  6. const scream = createScream(message => console.log(message))
  7. scream('functions can be returned from other functions')
  8. scream('createScream returns a function')
  9. scream('scream invokes that returned function')
  10. // FUNCTIONS CAN BE RETURNED FROM OTHER FUNCTIONS!!!
  11. // CREATESCREAM RETURNS A FUNCTION!!!
  12. // SCREAM INVOKES THAT RETURNED FUNCTION!!!

上面两个例子都是使用的高阶函数的例子,函数可以被另一个函数返回。如果使用ES6语法,我们可以使用箭头函数来简化上面的例子

  1. const createScream = logger => message =>logger(message.Uppercase()+"!!!"

这儿我们需要注意,使用箭头函数的个数,如果一个函数声明里不只用了一个箭头函数,那就说明我们使用了一个高阶函数。

我们经常说Javascript是函数式编程语言,是因为在js中,函数是一等公民。这意味着,函数就是数据。他们可以被保存,取回,就像你在应用中使用其它变量一样使用函数。

命令式编程和声明式编程

函数式编程是一个大的编程范式:声明式编程中的一个部分,声明式编程是这样一种组合应用的风格:属性用来描述应该发生什么(目标),而不是怎样发生(过程)。

为了理解声明式编程,我们和命令式编程做个对比,命令式编程只关心怎么用代码来实现结果。我们来看下这个普通的工作:如何让URL变得更友好。通常情况下,空格是让URL变的不友好的主要因素,因此我们可以通过更换所有字符串中的空格和连字符来完成这个工作,我们首先用命令式编程完成这个工作。

  1. var string = "This is mid day show with Cheryl Water "
  2. var urlFriendly =""
  3. for (var i =0;i<string.length;i++){
  4. if(string[i] == " "){
  5. urlFriendly += '-';
  6. }else{
  7. urlFriendly += sring[i]
  8. }
  9. console.log(urlFriendly)
  10. }

这个例子中,我们循环string中的每个字符,发现空格就用-连字符替换掉,程序的这个结构只关心这个工作是怎么实现的(过程),我们用了一个for循环,嵌套了一个if语句,用等号来设置value,单单看代码的话,它并没有告诉我们其它的东西了,命令式编程需要更多的描述来说明正在发生什么。

现在,使用声明式编程来完成这个工作;

  1. const string = "This is mid day show with Cheryl Water ";
  2. const urlFriendly =string.replace(/ /g,"-")
  3. console.log(urlFriendly)

这儿我们使用了字符串的replace方法和正则表达式来完成了这个工作。替换掉了目标url中的空格。它描述了应该发生什么结果,(字符串中的空格被replace掉了),至于中间怎么替换的这个过程就隐藏在replace这个函数中了。

声明式函数很容易进行推理,因为代码描述的事自身发生了什么。看下下面例子中的代码,它详细的说明了当你从一个API获取members时发生了什么。

  1. loadAndMapMembers = compose(
  2. combineWith(sessionSto
  3. rage,"members"),
  4. save(sessionStorage,"members"),
  5. scopMembers(window),
  6. logMemberInfoToConsole,
  7. logFieldsToConsole("name.first"),
  8. countMembersBy("location.state"),
  9. prepStatesForMapping,
  10. save(sessionStorage, "map"),
  11. renderUSMap
  12. );
  13. getFakeMembers(100).then(
  14. loadAndMapMembers)

声明式编程的代码有较强的可读性,因此更容易理解。每一个函数怎么实现的细节被抽离出来。这些小函数命名恰当,并且以members data从加载到保存再到打印这个顺序排列下来,这种方法不需要太多的说明。声明式编程应该用容易理解的理念来开发应用。当一个应用容易理解的时候,就很容易进行伸缩。

现在我们再考虑下如何创建一个DOM,命令式编程应该关心DOM是怎样被创建的。

  1. var target = document.getElementById('target');
  2. var wrapper = document.createElement('div');
  3. var headline = document.createElement('h1');
  4. wrapper.id = "welcome"; headline.innerText = "Hello World";
  5. wrapper.appendChild(headline); target.appendChild(wrapper);

上述代码描述了如何创建元素,设置元素,以及如何把元素添加到文档(document)对象中,这样的话,如果你将来要对这个程序作出改动,如添加一些新特性,增加一百行代码的话,会变得很困难。

那么,让我们来看下用声明式是如何做的吧,我们用React的组件来实现:

  1. const {render} = ReactDOM
  2. const Welcome = ()=> (
  3. <div id="welcome">
  4. <h1>Hello World </h1>
  5. </div>
  6. )
  7. render(
  8. <welcome />,
  9. document.getElementById('target')
  10. )

react是声明式的,这里,welcome组件是要被渲染的一个DOM组件,渲染函数使用welcome组件中的渲染说明来创建DOM ,渲染函数并没有告诉我们DOM是怎样被渲染的。我们可以很清楚的看到我们是想要把welcome组件渲染到id为target的元素中。

函数式的核心概念

既然我们以及介绍过了函数式编程,以及什么是函数式,声明式。接下来我们要介绍函数式编程的几个核心概念:不可变性,纯函数,数据抽象,高阶函数,递归,以及组合(conposition).

不变性

mutate是改变的意思,所以Immutable就是不能改变的意思,在一个函数式程序中,数据是不可变的,并且一直不变。

如果你想要在网上分享你的身份证,需要删掉私密的信息,那么你只有两种选择,一个是用大的记号笔直接描掉身份证上的隐私信息,另一种方法是找个复印机,弄个身份证复印件,用记号笔描掉复印件上的隐私信息。这样的话你的身份证不会被弄脏。

这就是不可变的数据在应用程序中的工作流程,我们不会使用原始数据,而是制作一个副本来代替原始数据。

为了理解不可变数据的工作流程,我们先来看下可变数据如何工作的

  1. let color_lawn ={
  2. title:"lawn",
  3. color:"#00ff00",
  4. rating:0
  5. }

我们会使用一个函数来改变color对象中的rating来设置颜色,

  1. function rateColor(color,rating){
  2. color.rating = rating
  3. return color
  4. }
  5. console.log(rateColor(color_lawn,5).rating) //5
  6. console.log(color_lawn.rating) //5

在Javascript中,函数的参数引用的是实际的数据(actual data),设置color的rating的过程中,将会改变或更改原始的color对象。想象一下,你给公众展示出生证明,展示之前你花钱让别人帮你处理上面的隐私信息,他们直接在你的出生证明上用黑色记号笔描黑了隐私信息。虽然目的达到了,但是你的出生证明也被描黑了。

你可能希望他们处理你的出生证明时使用复印件,而不描黑你的原件。我们重写rateColor函数,让它不改变原始数据。

  1. var rateColor = function(color,rating){
  2. return Object.assign({},color,{rating:rating})
  3. }
  4. console.log(rateColor(color_lawn,5).rating) //5
  5. console.log(color_lawn.rating) //4

这里,我们使用了Object.assign来改变color rating。Object.assign是一个复印机,它产生一个空对象,复制color到空对象,并重写color副本中的rating,现在我们就有一个新的rated color object并且没有改变原始的数据。

我们可以使用ES6的箭头函数和ES7的扩展操作符来重写上面的函数

  1. const rateColor = (color, rating) =>(
  2. {
  3. ...color,
  4. rating
  5. }
  6. )

这个函数和上边的函数是一模一样的,它把color看作一个不可变的对象,当然它的语法看起来更为简洁,括号中返回的是一个对象。使用箭头函数需要多一步,因为箭头函数不能恰好指向花括号。

我们看下用color names组成的一个数组:

  1. var list =[
  2. {title:"Rad Red"},
  3. {title:"Lawn"},
  4. {title:"Party Pink"}
  5. ]

我们可以创建一个函数使用数组的array.push()来添加color

  1. var addColor = function(title,colors){
  2. colors.push({title:title})
  3. return colors;
  4. }
  5. console.log(addColor("Glam Green",list).length) //4
  6. console.log(list.length) //4

可是,array.push不是不可变的,这个addColor函数通过给原始数组添加字段改变了原始数组list,为了保持color数组的不变性,我们必须用array.concat来代替。

  1. const addColor = (title,array) =>array.concat({title})
  2. console.log(addColor("Glam Green",list).length) //4
  3. console.log(list.length) //3

Array.concat连接数组,在这个例子中,它把一个有新color title的对象添加到原始数组的副本中。

你也可以用ES6的扩展操作符连接数组,扩展操作符可以用来拷贝对象

  1. const addColor =(title,list) =>[...list,{title}]

这个函数为初始的list数组制作一个副本,然后给副本中添加一个新对象,这个新对象中有color的title,这是不可变的。

纯函数

纯函数的返回值是基于自己的参数计算出来的,纯函数至少使用一个参数,并且总是返回一个value或者另一个函数函数,它们不会产生副作用。不会创建全局变量,或者改变一个application的state状态,它们把它们自身的参数看作不可变的数据。

为了理解纯函数,我们先来看下不纯函数是什么样子

  1. var frederic = {
  2. name:"Frederick Douglass",
  3. canRead:false,
  4. carWrite:false
  5. }
  6. function selfEducate(){
  7. frederic.canRead = true
  8. frederic.canWrite = true
  9. return frederick
  10. }
  11. selfEducate()
  12. console.log(frederick)
  13. //{name:"Frederick Douglass",canRead:false,canWrite:false}

selfEducate函数不是一个纯函数,它没有使用任何的参数,并且也没有返回值或者另一个函数,它也很不好的改变了它作用域外的一个变量frederick,一旦selfEducate函数被引用

段落引用

,就会产生副作用。

  1. var const = {
  2. name:"Frederick Douglass",
  3. canRead:false,
  4. carWrite:false
  5. }
  6. const selfEducate =(person) =>{
  7. person.canRead = true
  8. person.canWrite = true
  9. return person
  10. }
  11. console.log(selfEducate(frederick)
  12. console.log(frederick)
  13. //{name:"Frederick Douglass",canRead:false,canWrite:false}
  14. //{name:"Frederick Douglass",canRead:false,canWrite:false}

纯函数是可测试的
纯函数生来就是是可测试的,它们不改变原来的任何环境,因此并不需要复杂的测试安装或拆卸(do not required a complicated test setup or teardown),纯函数的一切都需要通过参数arguments来访问,当测试(testing)一个纯函数的时候,你控制了参数你就可以知道它要输出什么结果,11章会更详细的介绍测试。

这个selfEducate函数也不是纯函数,它也会产生副作用,引用这个selfEducate函数会改变传入它里面的对象,如果我们把传入函数的参数看作不可变的数据,那么我们就有了自己的纯函数。

我们让这个selfEducate函数使用一个参数;

  1. const frederick ={
  2. name:"Frederick Douglass",
  3. canRead:flase,
  4. canWrite:false
  5. }
  6. const selfEducate = person =>
  7. ({
  8. ...person,
  9. canRead:true,
  10. canWrite:true
  11. })
  12. console.log(selfEducate(frederick)
  13. console.log(frederick)
  14. //{name:"FrederickDouglass",canRead:false,canWrite:false}
  15. //{name:"Frederick Douglass",canRead:true,canWrite:true}

selfEducate函数的最后这个版本就是一个纯函数,它计算(改动)value基于它自身传入的参数-the person,它返回一个新的person对象,并且没有改变原始person,因此,它没有产生副作用。

现在我们看下非纯函数是怎样改变DOM的

  1. function Header(text){
  2. let h1 = document.createElement('h1');
  3. h1.innerText = text;
  4. document.body.appendChild(h1);
  5. }
  6. Header("Header() 产生了副作用");

header函数生成一个有具体文字的头元素,并且把这个生产的头元素添加到DOM中,这个函数不是纯函数,它没有返回一个函数function或者一个值value,它也产生了副作用,改变了DOM

在React中,UI就是纯函数,在这个例子中,Header是一个纯函数用来创建一个头部元素,和上面举出的例子要实现的目标是一样的,但是这个函数并没有副作用,因为它没有改变DOM,它会创建一个头部元素,程序中的其它部分使用创建出来的头部元素来改变DOM

  1. const Header = (props)=><h1>{props.title}</h1>

纯函数是函数式编程的另一个重要的核心概念,使用纯函数会让你的工作变的更轻松,因为它们不会改变你的应用中的状态,些纯函数有下面三点遵守:

  1. 函数至少要有1个参数
  2. 函数应返回一个值value或者另一个函数another function
  3. 函数不应改变自身的任何参数

数据转换

如果一个程序中数据是不可变的那么变的是什么呢,函数式编程就是关于数据从一种形式转换到另一种形式的,我们用函数来生成转换副本,这些函数可以让我们的代码更加的简单

许多函数式编程就是关于数据转换的,你并不需要用一个具体的框架来理解怎么基于另一个数据集来生产一个新的数据集(data set),Javascript已经内置了一些必要的工具来完成这些事情。但是想要提高Javascript函数式编程的效率,有两个函数是你必须要精通的,那就是array.map以及array.reduce

在本节中,我们会了解到怎样使用这两个核心函数来让数据从一种形式转换到另一种形式。

请看下面这个high schools数组

const schools =[
"Yorktown",
"Washington & Lee",
"Wskefield"
]

我们可以通过array.join函数让逗号把这个strings分开

  1. consolog.log(schools.join(","))
  2. //"Yorktown,Washington & Lee,Wakefield"

join函数是Javascript内置到数组方法,可以让我们从数组中提取出限定的string,且原始数据不变。提取的过程被隐藏起来。

如果我们想创建一个函数让它把以字母W开头的string提取出来生成一个全新的函数的话,我们可以使用array.filter方法
```
const wSchools = schools.filter(school=>school[0] ==="w")
console.log(wSchools)

//["Washington & Lee","Wakefield"]
```
Array.filter是一个js内置函数,它会基于原始数组产生一个新数组,这个函数使用predicate作为唯一参数,predicate是一个函数,它只返回布尔值的true或者false,Array.filter为数组中的每个item引用一次predicate,这些item被作为参数传入predicate来判断item是否应该被添加到新的数组之中。在本例中,array.filter函数用来检查每一个school是否以W开头

当到了要从一个数组中移除item的时候,我们应使用array.filter而不是array.pop()或者array.splice(),因为array.filter是不变的。

下一个例子中,cutSchool函数返回一个过滤掉特殊school的新数组。

  1. const schools =[
  2. "Yorktown",
  3. "Washington & Lee",
  4. "Wskefield"
  5. ]
  6. const cutSchool =(cut,list) =>
  7. list.filter(school =>school !== cut)
  8. consolog.log(cutSchool("Washington & Lee",shoools).join("*"))
  9. // "Yorktown * Wakefield"
  10. console.log(schools.join("\n"))
  11. // Yorktown
  12. // Washington & Lee
  13. // Wakefield

这个例子中,cutSchool函数返回一个不包含“Washington & Lee"的新数组,然后使用新数组的join函数创建一个星号来连接剩下的两个schools,CutSchool是一个纯函数,传入school数组,返回一个去掉了你不需要的school的新数组。此外,join函数被连接在返回的数组上面,用了生成一个*号连接剩下的schools

另一个必须要掌握的数组是array.map,和使用predicate的filter函数不同的是,array.map方法使用函数作为自己的参数。数组的每个item都会引用一次map,并且不管发生了什么,map都会添加到新数组上。

  1. const highSchools = schools.map(shool =>'${school} high School'}
  2. console.log(highSchools.join("\n"))
  3. // Yorktown High School
  4. // Washington & Lee High School
  5. // Wakefield High School
  6. console.log(schools.join("\n"))
  7. // Yorktown
  8. // Washington & Lee
  9. // Wakefield

这个例子中,map函数用来给每个school后面追加一个high school
,原来到的chools还是没有改变

最后的一个例子中,我们从一个字符串数组中生成了另一个字符串数组(array of strings),map函数可以创建对象数组,值,数组,其它函数等任意的JavaScript类型,下面例子中为每个school返回了一个对象。

  1. const highSchools = schools.map(school => ({ name: school })) console.log( highSchools )
  2. // [
  3. // { name: "Yorktown" },
  4. // { name: "Washington & Lee" },
  5. // { name: "Wakefield" }
  6. // ]

如果你想创建一个能在对象数组中改变一个对象的纯函数,你可以使用map函数,下面例子中,我们把名字为Stratford的学校改为HB Woodlawn,期间不改变schools array

  1. let shcools =[
  2. {name:"Yorktown"},
  3. {name:"Stratford"},
  4. {name:"Washington & Lee"},
  5. {name:"Wakefield"}
  6. ]
  7. let updatedSchools = editName("Stratford","HB Woodlawn",schools)
  8. console.log(updatedSchools[1]) //{name:"HB Woodlawn"}
  9. console.log(schools[1]) //{name:"Stratford"}

数组shcools是一个对象数组,updatedSchools变量调用editName函数并且发送我们想要更新的旧school名字,新school名字,还有对象数组schools,这会改变新数组,但不会改变原始数组。

  1. const editName = (oldName,name,arr) =>
  2. arr.map(item =>{
  3. if(item.name === oldName){
  4. return{
  5. ...item,
  6. name
  7. }
  8. }else{
  9. return item
  10. }
  11. })

editName中,map函数用来创建基于原始数组schools的新数组,Array.map把每个item的索引(index)添加到回调(callback)中作为第二个参数,变量i,当i和我们想编辑的item的索引不相等时,跳过去,我们 只是想把相同的item打包到一个新的数组中;当i和我们想编辑的item的索引相等时,我们在新数组中用一个新对象代替要改动的item的索引(replace the item at that index in the new array with a new object)

editName函数可以通过简写if语句写成一行,

  1. const editName = (odlName,name,arr) =>
  2. arr.map(item =>(item.name ===oldName)?
  3. ({...item,name}):item)

如果需要把数组转换成一个对象,可以用array.mp结合Object.keys,Object.keys是一个可以从对象中返回数组的keys的方法

  1. const schools ={
  2. "Yorktown":10,
  3. "Washington":2,
  4. "Wakefield":5
  5. }
  6. const schoolArray = Object.keys(schools).map(key =>
  7. ({
  8. name:key,
  9. wins:schools[key}
  10. })
  11. console.log(schoolArray)
  12. // [
  13. // {
  14. // name: "Yorktown",
  15. // wins: 10
  16. // },
  17. // {
  18. // name: "Washington & Lee",
  19. // wins: 2
  20. // },
  21. // {
  22. // name: "Wakefield",
  23. // wins: 5
  24. // }
  25. // ]

这个例子中,Object.keys返回了一个school names的数组,我们在这个数组上面施加map,来创建一个相同长度的新数组,新对象的name使用key,value使用wins

目前为止,我们已经学过了用array.map和array.filter来转换数组,把数组转换为对象等,最后一个要学习等工具就是把数组转换为primitives and other objects

reducereduceRight可以用来把数组转换为其它的任何值,可以是一个number,string,boolean,object,甚至是函数。

比如说我们要找数组中的最大值,就需要把数组转换成为一个number,因此,我们可以使用reduce来做这件事。

  1. const ages =[21,18.42,40,64,63,34]
  2. const maxAge = ages.reduce((max,age) =>{
  3. console.age('${age}>${max} =${age >max}');
  4. if(age>max){
  5. return age
  6. }else{
  7. return max
  8. }
  9. },0)
  10. console.log('maxAge',maxAge);
  11. // 21 > 0 = true
  12. // 18 > 21 = false
  13. // 42 > 21 = true
  14. // 40 > 42 = false
  15. // 64 > 42 = true
  16. // 63 > 64 = false
  17. // 34 > 64 = false
  18. // maxAge 64

ages数组已经被换算(reduced)成了一个单一值:最大年龄:64。Reduce使用两个参数,一个回调函数,一个原始值。本例中,原始值是0,将最大年龄初始化为0。每个item会引用一次callback函数,若第一次引用的年龄值21和数组中第一个值,初始值为0的max,callback函数返回两个较大的数字,21和下一次迭代循环中的最大值,每次循环都将max value和返回的两个greater较大的值比较,最后,数组中最后一个值和先前callback返回的两个值比较。

若去掉console.log语句,使用简写的if语句,我们可以用下面的语法来计算最大值。

  1. const max = ages.reduce((max,value)=>(value>max)?value:max)

array.reduceRight和array.reduce函数差不多,唯一的区别是从结尾处开始比较而不从开头。

有时我们需要把数组转为对象,下面的例子就是使用reduce把数组里的color值转换为哈希

  1. const colors =[
  2. {
  3. id:'-xekare',
  4. title:"rad red",
  5. rating:3
  6. },
  7. {
  8. id:'-jbwsof',
  9. title:"big blue",
  10. rating:2
  11. },
  12. {
  13. id:'-prigbj',
  14. title:"grizzly grey",
  15. rating:3
  16. },
  17. {
  18. id:'-ryhbhsl',
  19. title:"banana",
  20. rating:1
  21. },
  22. ]
  23. const hashColors = colors.reduce(
  24. (hash,{id,title,rating})=>{
  25. hash[id]={title,rating}
  26. return hash
  27. },
  28. {}
  29. )
  30. console.log(hashColors);
  31. // {
  32. // "-xekare": {
  33. // title:"rad red",
  34. // rating:3
  35. // },
  36. // "-jbwsof": {
  37. // title:"big blue",
  38. // rating:2
  39. // },
  40. // "-prigbj": {
  41. // title:"grizzly grey",
  42. // rating:5
  43. // },
  44. // "-ryhbhsl": {
  45. // title:"banana",
  46. // rating:1
  47. // }
  48. // }

上面的例子中,传入reduce函数的第二个参数是一个空的对象,它是我们hash的初始化值,在每一次循环期间,回调函数(callback function)用花括号{}给hash添加一个新的key。并且为key设置value,Array.reduces可以用来换算数组,把数组弄成一个简单值(single vlaue),本例中是一个对象。

我们甚至可以用reduce把数组转换成完全不同的数组下面的例子就是使用reduce的话更为合适的例子,把数组看为一个有多个instance实例的特殊值不重复的唯一 values

  1. const colors =["red","red","greem","blue","green "]
  2. const distinctColors =colors.reduce(
  3. (distinct,color) =>
  4. (distinct.indexOf(color)!==-1)?
  5. distinct:
  6. {...distinct,color},
  7. []
  8. )
  9. console.log(distinctColors)
  10. //["red","green","blue"]

这个例子中,colors数组被转换成了一个单一值(不重复)的数组,reduce函数的第二个函数是一个空数组,它是distinct value的初始化值,当color第一次出现时,会被添加到空数组中,重复时会被略过。

Map和reduce是函数式编程人员的主要武器,Javasctipt也不例外,如果你想成为一个高效率的Javascript工程师,你必须精通这些函数。把数据集从一种形式转换为另一种形式的能力也是必须的,这种能力对于任何的编程范式来说都非常有用。

高阶函数

使用高阶函数也是函数式编程必备技能,我们之前已经提过几次高阶函数,高阶函数是一种可以对函数进行操作运算对函数,它们将函数作为参数,也可以返回函数。

第一类高阶函数就是被用来作为参数的函数,我们创造一个回调函数callback function 来检测一个condition是否成立,并触发回调函数,一个回调函数用来处理condition为真,另一个回调函数用来处理false

  1. const invokeIf =(condition,fnTrue,fnFalse) =>
  2. (condition)?fnTrue():fnFalse()
  3. const showWelcome = () =>
  4. console.log("Welcome!!!")
  5. invokeIf(true,showWelcome,showUnauthorized) //"welcome"
  6. invokeIf(false,showWelcome,showUnauthorized) //"Unauthorized"

InvokeIf要求两个函数,一个true,一个false,当condition为true,显示Welcome,反之,显示Unauthorized。

返回函数的高阶函数可以让我们处理Javascript中复杂的异步,可以帮助我们方便的复用代码。

柯里化(Currying)是使用高阶函数时的一个技巧,Currying对需要一次操作完成求值的value暂不求值,直到剩下的值在稍后提供时为止。这个操作在返回一个函数时经常用到。

下面的例子就是关于currying的,userLogin函数附带一些信息-用户的名字和返回一个复用的函数,这个函数在剩下的通知(information)-message可以访问的时候复用,本例中,log message会放置到username的前面,注意我们用gerFakeMembers函数返回了一个promise

  1. const userLogs = userName =>message=>
  2. console.log('$userName}->${message}')
  3. const log = userLogs("grandpa23")
  4. log("attempted to load 20 fake members")
  5. getFakeMembers(20).then(
  6. members =>log('successfully loaded ${members.length} members'),
  7. error =>log("ancountered an error loading members")
  8. )
  9. // grandpa23 -> attempted to load 20 fake members
  10. // grandpa23 -> successfully loaded 20 members
  11. // grandpa23 -> attempted to load 20 fake members
  12. // grandpa23 -> encountered an error loading members

UserLogs是一个高阶函数,log函数从user中产生的,每次log函数运行的话,“grandpa23“就会被添加到message

递归

递归就是创建一个函数并调用自身到技术,当碰到一个循环时可以用递归来代替,比如从1数到10这个工作,我们可以用循环来处理,也可用递归来处理。下面到例子使用递归来解决

  1. const countdown =(value,fn)=>{
  2. fn(value)
  3. return (value>0)?countdown(value-1,fn):value
  4. )
  5. countdown(16,value>console.log(value));
  6. //16
  7. //9
  8. //8
  9. //7
  10. //6
  11. //5
  12. //4
  13. //3
  14. //2
  15. //1
  16. //0

Countdown使用一个number和函数作为参数,本例中,countdown函数使用了数字10和一个回调函数,当countdown执行,回调函数记录当前的值,然后countdown检查value是否比0大,若大,countdown用自减的值回调自身,最后,value为0,count返回0

递归另一个用法就是处理异步,当函数准备好了时可以调用自身(Function can recall themselves when they are reday)。

countdown函数可以修改下,让它有个延迟,修改过的版本的countdown可以用来创建一个countdown的定时表

  1. const coundown =(value,fn,delay=1000)=>{
  2. fn(value)
  3. return (value>0)?
  4. setTimeout()=>countdown(value-1,fn),delay):
  5. value
  6. }
  7. const =log=value=>console.log(value)
  8. countdown(10,log)

待续

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注