JavaScript中多个箭头函数是什么意思?

741

我一直在阅读一些React代码,发现有些地方我不理解:





handleChange = field => e => {
  e.preventDefault();
  /// Do something here
}

19
为了好玩,Kyle Simpson将箭头的所有决策路径都放入了这个流程图中。源自于他在 Mozilla Hacks 博客文章“ES6 In Depth: Arrow functions”下的评论中提到的链接,你可以在这里找到这个流程图 - gfullam
23
箭头函数流程图的网址已经失效,因为该书有新版本。可用的工作链接在 https://raw.githubusercontent.com/getify/You-Dont-Know-JS/1st-ed/es6%20%26%20beyond/fig1.png。 - Dhiraj Gupta
8个回答

1272

这是一个柯里化函数 (Currying function)

首先,我们来看一个带有两个参数的函数……

const add = (x, y) => x + y
add(2, 3) //=> 5

这里再以柯里化形式呈现...

const add = x => y => x + y

这是同样的代码,但没有箭头函数 …

const add = function (x) {
  return function (y) {
    return x + y
  }
}

聚焦于return

另一种视角可能有所帮助。我们知道箭头函数的工作方式 - 让我们特别注意返回值

const f = someParam => <b>returnValue</b>

所以我们的add函数返回一个函数 - 为了更清晰,我们可以使用括号。 粗体文本是我们函数add的返回值。
const add = x => (y => x + y)

换句话说,对某个数字进行add操作会返回一个函数。
add(2) // returns (y => 2 + y)

调用柯里化函数

为了使用我们的柯里化函数,我们需要以稍微不同的方式来调用它...

add(2)(3)  // returns 5

这是因为第一个(外部)函数调用返回了第二个(内部)函数。只有在我们调用第二个函数后才会实际得到结果。如果我们将两个调用分开成两行,这一点就更明显了...

const add2 = add(2) // returns function(y) { return 2 + y }
add2(3)             // returns 5

将我们新的理解应用到你的代码中

相关:"绑定、部分应用和柯里化有什么区别?"

好的,既然我们现在理解了这个,让我们来看看你的代码。

handleChange = field => e => {
  e.preventDefault()
  /// Do something here
}

我们将首先表示它,而不使用箭头函数...

handleChange = function(field) {
  return function(e) {
    e.preventDefault()
    // Do something here
    // return ...
  };
};

然而,由于箭头函数在词法上绑定了this,因此它实际上看起来更像这样...

handleChange = function(field) {
  return function(e) {
    e.preventDefault()
    // Do something here
    // return ...
  }.bind(this)
}.bind(this)
或许现在我们可以更清楚地看到这段代码在做什么了。 handleChange 函数为指定的 field 创建一个函数。这是一种方便的 React 技巧,因为您需要在每个输入上设置自己的监听器以更新应用程序状态。通过使用 handleChange 函数,我们可以消除为每个字段设置 change 监听器所导致的所有重复代码。酷!
1. 在这里,我不必词法绑定“this”,因为原始的 add 函数不使用任何上下文,因此在这种情况下保留它并不重要。
更多箭头函数
如果需要,可以连续使用多个箭头函数 -
const three = a => b => c =>
  a + b + c

const four = a => b => c => d =>
  a + b + c + d

three (1) (2) (3) // 6

four (1) (2) (3) (4) // 10

科里化函数能够做出令人惊讶的事情。在下面的例子中,我们将$定义为一个有两个参数的科里化函数,但在调用时,看起来我们可以提供任意数量的参数。科里化是元数的抽象 -

const $ = x => k =>
  $ (k (x))
  
const add = x => y =>
  x + y

const mult = x => y =>
  x * y
  
$ (1)           // 1
  (add (2))     // + 2 = 3
  (mult (6))    // * 6 = 18
  (console.log) // 18
  
$ (7)            // 7
  (add (1))      // + 1 = 8
  (mult (8))     // * 8 = 64
  (mult (2))     // * 2 = 128
  (mult (2))     // * 2 = 256
  (console.log)  // 256

局部应用

局部应用是一个相关的概念。它允许我们对函数进行部分应用,类似于柯里化,但函数不必以柯里化形式定义 -

const partial = (f, ...a) => (...b) =>
  f (...a, ...b)

const add3 = (x, y, z) =>
  x + y + z

partial (add3) (1, 2, 3)   // 6

partial (add3, 1) (2, 3)   // 6

partial (add3, 1, 2) (3)   // 6

partial (add3, 1, 2, 3) () // 6

partial (add3, 1, 1, 1, 1) (1, 1, 1, 1, 1) // 3

这里有一个partial的可工作演示,您可以在自己的浏览器中操作 -

const partial = (f, ...a) => (...b) =>
  f (...a, ...b)
  
const preventDefault = (f, event) =>
  ( event .preventDefault ()
  , f (event)
  )
  
const logKeypress = event =>
  console .log (event.which)
  
document
  .querySelector ('input[name=foo]')
  .addEventListener ('keydown', partial (preventDefault, logKeypress))
<input name="foo" placeholder="type here to see ascii codes" size="50">


6
太棒了!但是有多少人实际上会分配“$”呢?还是在React中这只是一个别名?请原谅我对此不了解,只是好奇,因为我在其他语言中很少看到符号被分配。 - Caperneoignis
9
@Caperneoignis 使用$演示概念,但你可以用任何你想要的名称来代替它。巧合的是,但完全没有关联,$已经在像jQuery这样的流行库中使用,其中$是整个函数库的全局入口点。我认为它在其他库中也被使用过。另一个你会看到的是"_",在underscore和lodash等库中变得流行。没有一个符号比另一个更有意义;你为你的程序分配意义。它只是有效的JavaScript :D - Mulan
3
@Blake,你可以通过观察$的使用来更好地理解它。如果你正在询问实现本身,那么$是一个函数,接收一个值 x 并返回一个新的函数 k => ...。查看返回的函数体,我们看到 k (x),所以我们知道 k也必须是一个函数,并且 k(x) 的任何结果都会被放回 $ (...),我们知道这将返回另一个 k => ...,如此循环下去......如果你还有困惑,请告诉我。 - Mulan
12
虽然这个答案解释了这种技术的工作原理和模式,但我感觉它没有具体说明在任何场景下为什么这实际上是更好的解决方案。在什么情况下,abc(1,2,3) 不如 abc(1)(2)(3) 理想。这让代码逻辑更难以理解,很难读取函数abc,也更难读取函数调用。以前你只需要知道abc函数做什么,现在你不确定未命名的函数abc返回的内容是什么,而且还有两次。 - Muhammad Umer
8
@MuhammadUmer 抱歉,函数式编程风格的优点无法在短篇文章中概括,特别是涉及到一个没有实际意义的虚构函数“abc”。但我可以说一下,柯里化允许在程序时间线的不同调用位置提供不同的参数。这对于所有参数不能同时/在同一地点准备好的情况非常有用。学习替代编程风格的好处很多。如果你想知道为什么函数式语言通常使用这些技术,那就要开始学习了! - Mulan
显示剩余8条评论

110

简述

它是一种以简短方式编写并返回另一个函数的函数。

const handleChange = field => e => {
  e.preventDefault()
  // Do something here
}

// is equal to 
function handleChange(field) {
  return function(e) {
    e.preventDefault()
    // Do something here
  }
}

动机

这种技术可以在回调函数的参数固定的情况下使用,但我们需要传递额外的变量,同时避免使用全局变量。

例如,我们有一个按钮,它有一个onClick回调函数,并且我们想传递一个变量,比如id,但是onClick只接受一个参数event,因此无法将idevent一起传递。

const handleClick = (event, id) {
  event.preventDefault()
  // Dispatch some delete action by passing record `id`
}

这不会起作用。

作为解决方案,我们编写一个函数,它返回另一个函数, 其中变量作用域中包含id,而不使用任何全局变量:

const handleClick = id => event {
  event.preventDefault()
  // Dispatch some delete action by passing record `id`
}

const Confirm = props => (
  <div>
    <h1>Are you sure to delete?</h1>
    <button onClick={handleClick(props.id)}>
      Delete
    </button>
  </div
)

函数组合

多个箭头函数也被称为“柯里化函数”,用于函数组合。

import {compose} from 'redux'
import {store} from './store.js'

const pickSelectedUser = props => {
  const {selectedName, users} = props
  const foundUser = users.find(user => user.name === selectedName)
  
  return foundUser.id
}

const deleteUser = userId => event => {
  event.preventDefault()
  store.dispatch({
    type: `DELETE_USER`,
    userId,
  })
}

// The compose function creates a new function that accepts a parameter.
// The parameter will be passed throw the functions from down to top.
// Each function will change the value and pass it to the next function
// By changing value it was not meant a mutation
const handleClick = compose(
  deleteUser,
  pickSelectedUser,
)

const Confirm = props => (
  <div>
    <h1>Are you sure to delete?</h1>
    <button onClick={handleClick(props)}>
      Delete
    </button>
  </div
)

3
那么,这种写法怎么比以下这种更好呢:const handleClick = (ev, id) => {ev.preventDefault(); //do somth with id} 并在 onClick="(ev) => handleClick(ev, id);" 中使用它——这种写法看起来清晰易懂得多。在你的版本中,甚至并不清楚有关 event 的任何信息。 - Toskan
@Toskan - 是的,你是对的,版本 handleClick(ev, id) 在某些时候更明显,但它不可组合。查看此片段:https://gist.github.com/sultan99/13ef56b4089789a8d115869ee2c5ec47,您会发现柯里化函数非常适合函数组合,这是函数式编程的一个非常重要的部分。 - sultan

85

一个常见的技巧: 如果您对任何新的JavaScript语法及其编译方式感到困惑,可以查看Babel。例如,将您的代码复制到Babel并选择ES 2015预设,将会得到以下输出:

handleChange = function handleChange(field) {
  return function (e) {
    e.preventDefault();
    // Do something here
  };
};

Babel


68
了解箭头函数的可用语法将使您了解它们在像您提供的示例中“链接”时引入了什么行为。

当箭头函数不带块括号、带有或不带有多个参数时,构成函数体的表达式会被隐式返回。在您的示例中,该表达式是另一个箭头函数。

No arrow funcs              Implicitly return `e=>{…}`    Explicitly return `e=>{…}` 
---------------------------------------------------------------------------------
function (field) {         |  field => e => {            |  field => {
  return function (e) {    |                             |    return e => {
      e.preventDefault()   |    e.preventDefault()       |      e.preventDefault()
  }                        |                             |    }
}                          |  }                          |  }

箭头语法写匿名函数的另一个优点是,它们在定义时被词法绑定到作用域中。来自于 MDN 的“箭头函数”

箭头函数表达式相比函数表达式具有更短的语法,并且词法地绑定了 this 值。箭头函数始终是匿名函数

这在你的例子中尤其相关,因为它来自于一个。正如 @naomik 指出的那样,在 React 中,通常使用 this 访问组件的成员函数,例如:

Unbound                     Explicitly bound            Implicitly bound 
------------------------------------------------------------------------------
function (field) {         |  function (field) {       |  field => e => {
  return function (e) {    |    return function (e) {  |    
    this.setState(...)     |      this.setState(...)   |    this.setState(...)
  }                        |    }.bind(this)           |    
}                          |  }.bind(this)             |  }

54

想象一下,每当你看到一个箭头时,就用function替换它。
function parameters在箭头之前被定义。
因此,在你的例子中:

field => // function(field){}
e => { e.preventDefault(); } // function(e){e.preventDefault();}

然后一起:

function (field) { 
    return function (e) { 
        e.preventDefault(); 
    };
}

从文档中可以看到:

// Basic syntax:
(param1, param2, paramN) => { statements }
(param1, param2, paramN) => expression
   // equivalent to:  => { return expression; }

// Parentheses are optional when there's only one argument:
singleParam => { statements }
singleParam => expression

9
别忘了提到词汇绑定的 this - Mulan

6

虽然问题提到了React使用案例(我一直遇到这个SO线程),它可能与完全相关的内容不同,但是有一个重要的双箭头函数方面在这里没有明确提到。仅仅“第一个”箭头函数被命名(因此可以由运行时进行“区分”),任何后续的箭头函数都是匿名的,并且从React的角度来看,在每次渲染中都会计算为“新”的对象。

因此,双箭头函数将导致任何PureComponent一直重新渲染。

示例

您有一个带有更改处理程序的父组件如下:

handleChange = task => event => { ... operations which uses both task and event... };

并使用以下类似的渲染方式:

{ tasks.map(task => <MyTask handleChange={this.handleChange(task)}/> }

然后在输入或点击上使用handleChange。这一切都很好,看起来也很不错。但是,这意味着任何会导致父组件重新渲染的更改(例如完全不相关的状态更改)也会重新渲染所有的MyTask,尽管它们是PureComponent。

可以通过多种方式来减轻这种情况,例如传递最外层箭头和您将用它提供的对象,编写自定义shouldUpdate函数或回到基础知识,例如编写命名函数(并手动绑定this...)


2
你的问题中的例子是一个利用箭头函数和具有隐式返回值的柯里化函数。
箭头函数在词法上绑定了this,即它们没有自己的this参数,而是从封闭作用域中获取this的值。
以上代码的等效版本如下:
const handleChange = (field) {
  return function(e) {
     e.preventDefault();
     /// Do something here
  }.bind(this);
}.bind(this);

关于你的示例需要注意的一点是将handleChange定义为一个常量或函数。可能你正在将它作为类方法的一部分使用,并且它使用了class fields syntax
因此,你不应该直接绑定外层函数,而是在类构造函数中进行绑定。
class Something{
    constructor(props) {
       super(props);
       this.handleChange = this.handleChange.bind(this);
    }
    handleChange(field) {
        return function(e) {
           e.preventDefault();
           // do something
        }
    }
}

在这个例子中需要注意的另一件事是隐式返回和显式返回之间的区别。最初的回答中没有明确说明这一点。
const abc = (field) => field * 2;

上面是隐式返回的一个例子,即它将value字段作为参数并返回结果field*2,明确指定函数返回值。
对于显式返回,您需要明确告诉方法返回值。
const abc = () => { return field*2; }

需要注意的是,箭头函数没有自己的arguments,而是从父作用域继承。

例如,如果您只定义了一个箭头函数,如下所示:

Original Answer翻译成"最初的回答"

const handleChange = () => {
   console.log(arguments) // would give an error on running since arguments in undefined
}

作为替代,箭头函数提供了可以使用的剩余参数。最初的回答。
const handleChange = (...args) => {
   console.log(args);
}

-1

在JavaScript中,多个箭头函数表示函数的链接,其中一个函数的输出作为下一个函数的输入,依此类推。例如:

const add = (x) => (y) => x + y;
const multiply = (x) => (y) => x * y;

const result = add(2)(3); // 5
const finalResult = multiply(result)(4); // 20

在上面的示例中,addmultiply是箭头函数,它们接受一个参数并返回另一个箭头函数。第一个箭头函数接受一个数字x并返回另一个箭头函数,该函数接受另一个数字y并返回xy的总和。第二个箭头函数接受一个数字x并返回另一个箭头函数,该函数接受另一个数字y并返回xy的乘积。
要使用这些函数,您可以通过使用其参数调用一个函数,然后立即使用下一个参数调用返回的函数来链接它们。在上面的示例中,resultadd(2)(3)的输出,即5,而finalResultmultiply(result)(4)的输出,即20

很好。只是出于好奇,你有没有从ChatGPT获取这些信息? - starball

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接