上节课我们学习了受控组件和非受控组件,这节课我们来学习高阶函数。

回顾

  • 受控组件在输入过程中就开始取值
  • 受控组件把输入的值维护到state
  • 受控组件可以减少ref的使用

以上便是上节课的内容

概述

或许接触过python的朋友对高阶函数并不陌生。因为python中的装饰器在本质上就是高阶函数。那么什么是高阶函数呢?我们接下来开始学习这方面东西。

其实呢,按照react学习路线来说的话,这个时候应该来讲生命周期,但是为什么突然来说高阶函数呢?因为扩展了高阶函数的话,后面对代码优化是有帮助的。

我们先来看一下代码:

class Login extends React.Component {
  state = {
    username: "",
    password: ""
  };

  render() {
    return (
      <form onSubmit={this.notify}>
        Username: <input type="text" onChange={this.saveUsername} />
        Password: <input type="password" onChange={this.savePassword} />
        <button>Submit</button>
      </form>
    );
  }

  notify = e => {
    e.preventDefault();
    const { username, password } = this.state;
    alert(`{username}: {password}`);
  };

  saveUsername = e => { this.setState({ username: e.target.value }) };
  savePassword = e => { this.setState({ password: e.target.value }) };
}
ReactDOM.render(<Login />, document.getElementById("test"));

单一函数合并

我们用得还是上节课的案例和代码。大家有没有觉得这里的saveUsername方法和savePassword方法重复度太高了?几乎是一模一样。都是往state中存东西,如果这里不是登录,而且一个什么信息统计的页面,要输入姓名、年龄、性别、民族等等各种信息,那我们是不是要写好多个saveXxxx的方法?那么我们有没有办法能把这写操作全部写在一个函数里面呢?

但是我们如果把这个操作全部写在一个函数里面的话,我哪知道这个是传入的是什么呢?有人要说了那好办啊,我们来加一个参数不就行了嘛。行看看吧:

class Login extends React.Component {
  state = {...};
  render() {
    return (
      <form onSubmit={this.notify}>
        Username: <input type="text" onChange={this.saveFormData("username")} />
        Password: <input type="password" onChange={this.saveFormData("password")} />
        <button>Submit</button>
      </form>
    );
  }
  notify = e => {...};            
  saveFormData = e => {this.setState({name:e.target.value});};
}

这样对吗?那怎么可能对呢。我们说了好多遍了在绑定事件监听函数的时候不要加()加了就是直接调用了。我们看一下效果吧

image-20211228151704040

页面都没渲染出来,而且还报了错。咱也不去纠结报了什么错了。反正就是这个方法不对,大方向错了而不是代码中的bug。至于为什么说大方向错了,如果说有不明白为什么的,那么说明当初onClick事件绑定那一节的内容你已经忘了,赶紧回去复习,那一节里面详细讲解了这里报错的原因。

高阶函数

那么我们应该怎么写?我们再来分析一下刚才的代码:<input type="text" onChange={this.saveFormData("username")} />这是什么意思啊,我觉得大家应该没有任何迟疑地就回答出来,这是不是把this.saveFormData("username")的返回值作为onChange事件的回调?但是我们要求的是什么?我们要求的是onChange一定要是一个函数。那么如果this.saveFormData("username")的返回值是一个函数呢?

什么意思?我刚才代码那么写是把this.saveFormData("username")的返回值作为onChange的回调,那么这个方法返回的是个函数的话,onChange接收到的是不是就是一个函数?那就符合规则了。

那么我们来测一下吧:

class Login extends React.Component {
  state = {...};
  render() {return ...}
  notify = e => {...};

  saveFormData = e => () => { console.log("@") };
}

我们看上面这段代码,其他的都不变,saveFormData方法接收参数e,返回了一个箭头函数,在返回的箭头函数中打印@。那么onChange={this.saveFormData("username")}是不是和onChange={ () => { console.log("@") }}一样,这个时候onChange是不是就是这个箭头函数?,那么我们在input栏输入信息的话,控制台是不是就会打印@?我们来看一下效果:

image-20211228155251753

我们在input栏输入了 5 个数字,相应的,控制台中也打印了 5 次@。那么问题来了,当onChange被触发,调用的是哪个函数?是不是saveFormData返回的那个箭头函数?那么onChangeevent是不是也就传给saveFormData返回的函数了?那么我们就得接收这个event啊,我们还指望用event来取到输入的值呢。那么saveFormDate接的参数是什么?saveFormData是不是在页面渲染之前就已经被调用了啊?不调用怎么把函数返回给onChange呢?那么这个函数已经被调用了,他接收到什么参数是不是取决于我们在调用时传入的值啊?那么如果我们把saveFormDate函数改一下:

class Login extends React.Component {
  state = {...};
  render() {return ...}
  notify = e => {...};

  saveFormData = dataType => e => { this.setState({ dataType: e.target.value }) };
}

上面,saveFormData接收参数是我们手动传的,是我们传的这些信息的类型而不是event,所以我们用dataType来接收,但是当onChange触发,event要被传入saveFormData返回的函数中,所以我们用e来接收event,那获取到的event中输入的值要通过event.target.value来获取,然后存入到state里,我们这个值是谁的值,对应的就是saveFormData接收到的dataType。那我们来测一下吧:

image-20211228160819449

好像并不对,这是为什么呢?我们对象中的属性名其实都是字符串,所以说{dataType: value}这里并没有读取dataType变量,而是认为这是一个dataType属性。所以说我们并没有成功得到dataType的值。那么我们应该怎么写呢?这样改一下:

class Login extends React.Component {
  state = {...};
  render() {return ...}
  notify = e => {...};

  saveFormData = dataType => e => { this.setState({ [dataType]: e.target.value }) };
}

我们先看一下效果:

image-20211228161423174

诶,这样就没有问题了。可能有人说,没懂,这是啥意思啊?怎么用[]括起来就读取变量了呢?我们又到了复习原生js基础知识的时候了。

复习关于对象的知识

我们就简单地举一个例子吧:

let a = "name";
let obj = {};

看上面这段代码是什么意思啊?我定义了一个变量a,这个变量是一个值为name的字符串。然后有定义了一个变量obj是一个空对象。

那么如果我们现在希望obj变成{name: "jingxun"}应该怎么处理呢?有人要说,这还不简单嘛,直接这样obj.name = "jingxun"就可以了。当然了这样写是没毛病的,但是如果我的目的是让你这么写我还问你干什么呢?我这边的变量a不是白定义了嘛。

我们先来思考一个问题,我们在使用对象的时候,要想获取到对象中的某个属性有几种方式?最常见的是不是obj.attrName这一种?那么还有一种是不是obj[attrName]?那么第二种方法通常在什么时候会使用到呢?是不是我们在不确定要获取的属性的属性名是什么,然后通过变量传入从而来获取与变量值对应的属性?

当然更多关于对象的知识我这边是没法带着大家一一回顾的,大家有遗忘的抓紧去复习一下

回归高阶函数的内容

那么刚才{[datatype]: e.target.value}是不是也就不难理解了?那么我们把之前的流程从头捋一遍:

我们onChange={this.saveFormData("username")},这里我们是不是自己调用的saveFormData方法?而且也是我们自己决定的我现在要存username,然后saveFormData方法接收到了我们传进去的dataType。然后返回一个值作为onChange的回调。但是saveFormData的返回值是什么啊?是一个函数对不对?那么也就是说onChange的回调就是saveFormData返回出来的函数。当我们onChange被触发的时候是不是就会自动去调用saveFormData返回的函数而不是去调用saveFormData方法自身。

所以说大家还记不记得我们之前说给onXxxx事件绑定回调一定不要加(),那时候我们还没有提到高阶函数,为了方便大家理解所以那么说。但是这种说法是不严谨的。但是唯一不变的原则是onXxxx事件绑定的回调一定得是一个函数。

那么我再问一个问题:这里onChange拿到的函数中的this是谁?

我们来分析一下,onChange拿到的是不是saveFormData的返回出来的函数?这个函数是不是一个箭头函数?到这里大家应该也应该知道了,前面讲过,箭头函数中的this要去外层找,这里箭头函数的外层是saveFormData,而saveFormData也是箭头函数,this也要往外层找,就到了类作用域空间内了,所以this是类组件对象。

但是上面的说法不严谨。怎么说呢?我们一定要牢记一点箭头函数没有this,只有在箭头函数中使用this关键字的时候才会去函数外层找this

那么好,现在我们要开始提出概念了。我们说到现在是不是都没用说什么是高阶函数?那么我告诉大家,saveFormData就是一个高阶函数。什么意思呢?

其实,高阶函数有两个判断标准:

  1. A 函数接收的参数是一个函数
  2. A 函数的返回值是一个函数

以上两个条件只要满足其中一条就可以说这个 A 函数是一个高阶函数。那我们看一下saveFormData方法,这个方法的返回值是不是一个函数?这是不是满足上面的第二条?那么我们说saveFormData是一个高阶函数。

那么我们常见的高阶函数都有那些呢?

  • 最常见的一个是不是有Promise,当我们new Promise时,要传入一个执行器函数
  • 还有一个setTimeout这个延迟定时器是不是也要传入一个函数
  • 还有数组上有很多方法比如arr.map这个方法是不是也要传入一个函数

当然我们就简单举几个例子,还有很多,也就不一一列举了。

函数柯里化

可能大家看到这个次就觉得这是什么啊,好难啊。其实呢我们的saveFormData就已经做到了函数的柯里化。我们先来看一下概念,什么叫函数的柯里化?

通过函数调用继续返回函数的方式实现多次接收函数最后统一处理的函数形式叫做函数的柯里化。

这是什么意思呢?为什么说我们的saveFormData做到了函数的柯里化呢?我们举一个小例子:

function sum(a, b, c) {
  return a + b + c;
}
sum(1, 2, 3);

我们求和一般来说像上面这段代码是不是就可以完成了?直接定义函数接收三个参数然后求和返回出来。但是如果要用函数的柯里化的话,我们要通过函数调用返回函数,来实现多次传参。那么首先这是不是一个高阶函数?因为返回函数了啊。那么怎么写呢?

function sum(a) {
  return b => {
    return c => {
      return a + b + c;
    }
  }
}
console.log(sum(1)(2)(3));

看上面这段代码,我定义一个函数接收参数a,然后返回一个函数,这个函数接收参数b,然后再返回一个函数接收参数c,最后返回a + b + c,然后我们调用就不能像原来那样调了,我们调用sum是不是只能传一个参数?返回了一个函数,那我们得再加一个小括号继续调用返回出来的函数啊,这里返回的也还是一个函数,那我们就得再调用一次。这样最后就返回了这三个数的和。我们看一下效果

image-20211229103041203

控制台中成功答应出了三个数的和。

我们回头再看saveFormData,是不是通过函数调用并且返回函数?是不是多次传入参数?最后统一处理了这些参数。

可能在我们写求和的那个例子的时候大家觉得柯里化仿佛有点大病,又麻烦而且还没有什么优势。但是在回头看了saveFormData之后是不是就觉得如果不用函数柯里化,我要想让多个input标签共用一个saveFormData是不是有点不知道该怎么写?所以说柯里化还是有他的存在意义的。

总结

  • 高阶函数有两个评判标准,满足其中一个就可以
  • A 函数接收的参数是一个函数
  • A 函数的返回值是一个函数
  • 通过函数调用继续返回函数的方式实现多次接收函数最后统一处理的函数形式叫做函数的柯里化。

Copyright statement:The articles of this site are all original if there is no special explanation, indicate the source please when you reprint.

Link of this article:https://work.lynchow.com/article/hg_func_currying/