Cosa significano le funzioni con più frecce in javascript?


472

Ho letto un sacco di reactcodice e vedo cose come questa che non capisco:

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

11
Solo per divertimento, Kyle Simpson ha inserito tutti i percorsi decisionali per le frecce in questo diagramma di flusso . Fonte: il suo commento su un post sul blog di Mozilla Hacks intitolato ES6
Approfondimento

Poiché ci sono grandi risposte e ora una generosità. Puoi per favore approfondire ciò che non capisci che le risposte di seguito non coprono.
Michael Warner,

5
L'URL per il diagramma di flusso delle funzioni freccia ora è rotto perché c'è una nuova edizione del libro. L'URL di lavoro è disponibile su raw.githubusercontent.com/getify/You-Dont-Know-JS/1st-ed/…
Dhiraj Gupta

Risposte:


833

Questa è una funzione al curry

Innanzitutto, esamina questa funzione con due parametri ...

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

Eccolo di nuovo in forma al curry ...

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

Ecco lo stesso 1 codice senza funzioni freccia ...

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

Concentrarsi su return

Potrebbe aiutare a visualizzarlo in un altro modo. Sappiamo che le funzioni delle frecce funzionano così: prestiamo particolare attenzione al valore di ritorno .

const f = someParam => returnValue

Quindi la nostra addfunzione restituisce una funzione : possiamo usare le parentesi per maggiore chiarezza. Il testo in grassetto è il valore di ritorno della nostra funzioneadd

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

In altre parole, un addcerto numero restituisce una funzione

add(2) // returns (y => 2 + y)

Chiamare le funzioni al curry

Quindi, per usare la nostra funzione al curry, dobbiamo chiamarla in modo leggermente diverso ...

add(2)(3)  // returns 5

Questo perché la prima chiamata di funzione (esterna) restituisce una seconda funzione (interna). Solo dopo aver chiamato la seconda funzione otteniamo effettivamente il risultato. Ciò è più evidente se separiamo le chiamate su due linee ...

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

Applicando la nostra nuova comprensione al tuo codice

correlati: "Qual è la differenza tra associazione, applicazione parziale e curry?"

OK, ora che capiamo come funziona, diamo un'occhiata al tuo codice

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

Inizieremo rappresentandolo senza usare le funzioni freccia ...

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

Tuttavia, poiché le funzioni della freccia si legano in modo lessicale this, sembrerebbe in realtà più simile a questo ...

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

Forse ora possiamo vedere cosa sta facendo più chiaramente. La handleChangefunzione sta creando una funzione per un determinato field. Questa è una pratica tecnica React perché è necessario impostare i propri listener su ciascun input per aggiornare lo stato delle applicazioni. Usando la handleChangefunzione, possiamo eliminare tutto il codice duplicato che comporterebbe la configurazione di changelistener per ciascun campo. Freddo!

1 Qui non ho dovuto legarmi lessicamente thisperché la addfunzione originale non utilizza alcun contesto, quindi non è importante preservarla in questo caso.


Ancora più frecce

Più di due funzioni freccia possono essere sequenziate, se necessario -

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

Le funzioni al curry sono in grado di sorprendere. Di seguito vediamo $definita una funzione curry con due parametri, ma nel sito della chiamata sembra che possiamo fornire un numero qualsiasi di argomenti. Currying è l'astrazione dell'arità -

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

Applicazione parziale

L'applicazione parziale è un concetto correlato. Ci consente di applicare parzialmente funzioni, simili al curry, tranne per il fatto che la funzione non deve essere definita in forma curry -

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

Ecco una demo funzionante con partialcui puoi giocare nel tuo browser -

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">


2
Questo è eccezionale! Tuttavia, quanto spesso qualcuno assegna effettivamente i $? O è un alias per questo nel reagire? Perdonatemi la mia ignoranza all'ultimo, solo curioso perché non vedo un simbolo ottenere un incarico troppo spesso in altre lingue.
Caperneoignis,

7
@Caperneoignis è $stato usato per demo il concetto, ma puoi chiamarlo come vuoi. Per coincidenza, ma completamente non correlato, $ è stato utilizzato in librerie popolari come jQuery, dove si $trova una sorta di punto di accesso globale all'intera libreria di funzioni. Penso che sia stato usato anche in altri. Un altro che vedrai è _, reso popolare in biblioteche come underscore e lodash. Nessun simbolo è più significativo di un altro; si assegna il significato al proprio programma. È semplicemente valido JavaScript: D
Grazie

1
santo frijoli, bella risposta. vorrei che l'operazione accettasse
mtyson il

2
@ Blake Puoi capire meglio $come viene usato. Se stai chiedendo dell'implementazione stessa, $è una funzione che riceve un valore xe restituisce una nuova funzione k => .... Guardando il corpo della funzione restituita, vediamo k (x)quindi che kdobbiamo anche essere una funzione, e qualunque sia il risultato di k (x)viene rimandato a $ (...), che sappiamo restituisce un'altra k => ..., e continua ... Se sei ancora rimanere bloccato, fammi sapere.
Grazie

2
mentre questa risposta ha spiegato come funziona e quali schemi ci sono con questa tecnica. Sento che non c'è nulla di specifico sul perché questa sia in realtà una soluzione migliore in qualsiasi scenario. In quale situazione, abc(1,2,3)è meno che ideale di abc(1)(2)(3). È più difficile ragionare sulla logica del codice ed è difficile leggere la funzione abc ed è più difficile leggere la chiamata della funzione. Prima avevi solo bisogno di sapere cosa fa abc, ora non sei sicuro di quali funzioni senza nome abc stia tornando e due volte.
Muhammad Umer,

57

Comprendere le sintassi disponibili delle funzioni freccia ti darà una comprensione del comportamento che stanno introducendo quando sono "incatenati" come negli esempi che hai fornito.

Quando una funzione freccia viene scritta senza parentesi graffe a blocchi, con o senza parametri multipli, l'espressione che costituisce il corpo della funzione viene restituita implicitamente . Nel tuo esempio, quell'espressione è un'altra funzione freccia.

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()
  }                        |                             |    }
}                          |  }                          |  }

Un altro vantaggio della scrittura di funzioni anonime utilizzando la sintassi della freccia è che sono legati lessicamente all'ambito in cui sono definiti. Da "Funzioni freccia" su MDN :

Un'espressione di funzione freccia ha una sintassi più breve rispetto alle espressioni di funzione e associa lessicamente questo valore. Le funzioni freccia sono sempre anonime .

Questo è particolarmente pertinente nel tuo esempio considerando che è preso da a applicazione. Come sottolineato da @naomik, in React si accede spesso alle funzioni membro di un componente usando this. Per esempio:

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)             |  }

53

Un consiglio generale, se vieni confuso da una qualsiasi delle nuove sintassi JS e da come verrà compilata, puoi controllare babel . Ad esempio, copiando il codice in babel e selezionando il preset es2015 si otterrà un output come questo

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

Babele


42

Pensala in questo modo, ogni volta che vedi una freccia, la sostituisci con function.
function parameterssono definiti prima della freccia.
Quindi nel tuo esempio:

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

e poi insieme:

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

Dai documenti :

// 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

6
Non dimenticare di menzionare il limite lessicale this.
Grazie,

30

Breve e semplice 🎈

È una funzione che restituisce un'altra funzione scritta in modo breve.

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

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

Perché le persone lo fanno

Ti sei trovato di fronte quando hai bisogno di scrivere una funzione che può essere personalizzata? Oppure devi scrivere una funzione di callback con parametri fissi (argomenti), ma devi passare più variabili alla funzione evitando le variabili globali? Se la tua risposta " ", allora è il modo in cui farlo.

Ad esempio abbiamo un buttoncallback con onClick. E dobbiamo passare idalla funzione, ma onClickaccetta solo un parametro event, non possiamo passare parametri extra all'interno di questo:

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

Non funzionerà!

Pertanto creiamo una funzione che restituirà altre funzioni con il proprio ambito di variabili senza variabili globali, poiché le variabili globali sono malvagie 😈.

Sotto la funzione handleClick(props.id)}verrà chiamata e restituirà una funzione e avrà idnel suo ambito! Indipendentemente da quante volte verrà premuto, gli ID non si influenzeranno o si cambieranno a vicenda, sono totalmente isolati.

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
)

2

L'esempio nella tua domanda è quello di a curried functionche usa arrow functione ha un implicit returncome primo argomento.

La funzione freccia associa in modo lessicale ciò, cioè non hanno il proprio thisargomento ma prendono il thisvalore dall'ambito che lo racchiude

Un equivalente del codice sopra sarebbe

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

Un'altra cosa da notare sul tuo esempio è che definisci handleChangeuna const o una funzione. Probabilmente lo stai usando come parte di un metodo di classe e usa aclass fields syntax

quindi invece di associare direttamente la funzione esterna, la assoceresti al costruttore della classe

class Something{
    constructor(props) {
       super(props);
       this.handleChange = this.handleChange.bind(this);
    }
    handleChange(field) {
        return function(e) {
           e.preventDefault();
           // do something
        }
    }
}

Un'altra cosa da notare nell'esempio è la differenza tra ritorno implicito ed esplicito.

const abc = (field) => field * 2;

Sopra è un esempio di ritorno implicito, ad es. prende il campo valore come argomento e restituisce il risultato field*2che specifica esplicitamente la funzione da restituire

Per un ritorno esplicito si direbbe esplicitamente al metodo di restituire il valore

const abc = () => { return field*2; }

Un'altra cosa da notare sulle funzioni delle frecce è che non hanno le proprie argumentsma ereditano anche quelle dal campo dei genitori.

Ad esempio se si definisce semplicemente una funzione freccia come

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

In alternativa, le funzioni freccia forniscono i restanti parametri che è possibile utilizzare

const handleChange = (...args) => {
   console.log(args);
}

1

Potrebbe non essere del tutto correlato, ma poiché la domanda menzionata reagisce usando il caso (e continuo a imbattermi in questo thread SO): c'è un aspetto importante della funzione doppia freccia che non è esplicitamente menzionato qui. Solo la "prima" freccia (funzione) viene nominata (e quindi "distinguibile" dal tempo di esecuzione), tutte le seguenti frecce sono anonime e dal punto di vista di React contano come "nuovo" oggetto su ogni rendering.

Pertanto, la funzione doppia freccia farà sì che ogni PureComponent venga reso nuovamente.

Esempio

Hai un componente padre con un gestore modifiche come:

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

e con un rendering come:

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

handleChange quindi utilizzato su un input o clic. E tutto questo funziona e sembra molto bello. MA significa che qualsiasi modifica che provocherà il rendering del genitore (come un cambiamento di stato completamente non correlato) restituirà anche TUTTO il tuo MyTask, anche se sono PureComponents.

Questo può essere alleviato in molti modi come passare la freccia "più esterna" e l'oggetto con cui lo si nutre o scrivere una funzione di aggiornamento personalizzata o tornare alle basi come scrivere funzioni con nome (e associarlo manualmente ...)

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.