O que significam várias funções de seta em javascript?

472

Eu tenho lido um monte de reactcódigo e vejo coisas assim que não entendo:

handleChange = field => e => {
  e.preventDefault();
  /// Do something here
}
jhamm
fonte
11
Apenas por diversão, Kyle Simpson colocou todos os caminhos de decisão para as setas neste fluxograma . Fonte: Seu comentário em um post no blog Mozilla Hacks direito ES6 Em Profundidade: Seta funções
gfullam
Como existem ótimas respostas e agora uma recompensa. Você pode, por favor, elaborar o que você não entende que as respostas abaixo não cobrem.
Michael Warner
5
O URL do fluxograma das funções de seta agora está quebrado porque há uma nova edição do livro. URL de trabalho está em raw.githubusercontent.com/getify/You-Dont-Know-JS/1st-ed/…
Dhiraj Gupta

Respostas:

832

Essa é uma função ao curry

Primeiro, examine esta função com dois parâmetros…

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

Aqui está novamente em forma de curry…

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

Aqui está o mesmo código 1 sem funções de seta…

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

Focar em return

Pode ajudar a visualizá-lo de outra maneira. Sabemos que as funções de seta funcionam assim - vamos prestar especial atenção ao valor de retorno .

const f = someParam => returnValue

Portanto, nossa addfunção retorna uma função - podemos usar parênteses para maior clareza. O texto em negrito é o valor de retorno da nossa funçãoadd

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

Em outras palavras, addde algum número retorna uma função

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

Chamando funções com caril

Então, para usar nossa função ao curry, precisamos chamá-la de um jeito diferente…

add(2)(3)  // returns 5

Isso ocorre porque a primeira chamada de função (externa) retorna uma segunda função (interna). Somente depois que chamamos a segunda função é que obtemos o resultado. Isso é mais evidente se separarmos as chamadas em duas linhas ...

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

Aplicando nosso novo entendimento ao seu código

relacionados: "Qual é a diferença entre encadernação, aplicação parcial e curry?"

OK, agora que entendemos como isso funciona, vejamos seu código

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

Começaremos por representá-lo sem usar as funções de seta ...

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

No entanto, porque as funções de seta lexically ligamento this, seria realmente se parecem mais com isso ...

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

Talvez agora possamos ver o que isso está fazendo com mais clareza. A handleChangefunção está criando uma função para um especificado field. Esta é uma técnica útil do React, pois você precisa configurar seus próprios ouvintes em cada entrada para atualizar o estado dos aplicativos. Usando a handleChangefunção, podemos eliminar todo o código duplicado que resultaria na configuração de changelisteners para cada campo. Legal!

1 Aqui não tive que ligar lexicamente thisporque a addfunção original não usa nenhum contexto; portanto, não é importante preservá-la neste caso.


Ainda mais flechas

Mais de duas funções de seta podem ser sequenciadas, se necessário -

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

Funções curry são capazes de coisas surpreendentes. Abaixo, vemos $definida como uma função ao curry com dois parâmetros, mas no site da chamada, parece que podemos fornecer qualquer número de argumentos. Currying é a abstração da aridade -

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

Aplicação parcial

Aplicação parcial é um conceito relacionado. Ele nos permite aplicar parcialmente funções, semelhantes ao curry, exceto que a função não precisa ser definida na forma de caril -

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

Aqui está uma demonstração funcional sobre a qual partialvocê pode jogar em seu próprio navegador -

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

Obrigado
fonte
2
Isto é excelente! Quantas vezes alguém realmente atribui o '$'? Ou é um apelido para isso em reagir? Perdoe minha ignorância por último, apenas curioso, porque não vejo um símbolo recebendo uma tarefa com muita frequência em outros idiomas.
Caperneoignis
7
O @Caperneoignis $foi usado para demonstrar o conceito, mas você pode chamá- lo como quiser. Coincidentemente, mas completamente independente, $ tem sido usado em bibliotecas populares como o jQuery, onde $é uma espécie de ponto de entrada global para toda a biblioteca de funções. Eu acho que tem sido usado em outros também. Outro que você verá é _popularizado em bibliotecas como sublinhado e lodash. Nenhum símbolo é mais significativo que outro; você atribui o significado ao seu programa. É simplesmente válido JavaScript: D
Obrigado
1
Santo Frijoli, bela resposta. gostaria que o op aceitas #
2626 mtyson
2
@ Blake Você pode entender melhor $olhando para como é usado. Se você está perguntando sobre a implementação em si, $é uma função que recebe um valor xe retorna uma nova função k => .... Olhando para o corpo da função retornada, vemos k (x)que ktambém sabemos que deve ser uma função, e qualquer que seja o resultado k (x)é devolvido ao $ (...)que sabemos que devolve outra k => ..., e continua ... Se você ainda está ficar preso, me avise.
Obrigado
2
enquanto essa resposta explica como funciona e quais padrões existem com essa técnica. Eu sinto que não há nada específico sobre por que essa é realmente uma solução melhor em qualquer cenário. Em que situação, abc(1,2,3)é menos do que o ideal abc(1)(2)(3). É mais difícil argumentar sobre a lógica do código, é difícil ler a função abc e é mais difícil ler a chamada de função. Antes que você só precisasse saber o que o abc faz, agora você não tem certeza do que as funções não nomeadas que o abc está retornando fazem e duas vezes.
Muhammad Umer
57

Compreender as sintaxes disponíveis das funções de seta fornecerá uma compreensão de qual comportamento elas estão apresentando quando encadeadas, como nos exemplos que você forneceu.

Quando uma função de seta é gravada sem chaves, com ou sem vários parâmetros, a expressão que constitui o corpo da função é retornada implicitamente . No seu exemplo, essa expressão é outra função de seta.

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

Outra vantagem de escrever funções anônimas usando a sintaxe da seta é que elas estão ligadas lexicamente ao escopo em que são definidas. Nas 'Funções da seta' no MDN :

Uma expressão de função de seta possui uma sintaxe mais curta em comparação com expressões de função e vincula lexicamente o valor this . As funções de seta são sempre anônimas .

Isso é particularmente pertinente no seu exemplo, considerando que é retirado de um inscrição. Como apontado por @naomik, no React você costuma acessar as funções de membro de um componente usando this. Por exemplo:

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)             |  }
sdgluck
fonte
53

Uma dica geral, se você ficar confuso com alguma sintaxe JS nova e como ela será compilada, verifique babel . Por exemplo, copiar seu código em babel e selecionar a predefinição es2015 fornecerá uma saída como esta

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

babel

Rahil Ahmad
fonte
42

Pense assim, toda vez que vir uma seta, substitua-a por function.
function parameterssão definidos antes da seta.
Então, no seu exemplo:

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

e depois juntos:

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

Dos documentos :

// 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
LifeQuery
fonte
6
Não se esqueça de mencionar o lexicamente vinculado this.
Obrigado
30

Breve e simples 🎈

É uma função que retorna outra função escrita de maneira abreviada.

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

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

Por que as pessoas fazem isso ?

Você já enfrentou quando precisa escrever uma função que pode ser personalizada? Ou você precisa escrever uma função de retorno de chamada que tenha parâmetros fixos (argumentos), mas precisa passar mais variáveis ​​para a função, evitando variáveis ​​globais? Se sua resposta for " sim ", é assim que se faz.

Por exemplo, temos um buttonretorno de chamada onClick. E precisamos passar idpara a função, mas onClickaceita apenas um parâmetro event, não podemos passar parâmetros extras dentro desta:

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

Isso não vai funcionar!

Portanto, criamos uma função que retornará outra função com seu próprio escopo de variáveis ​​sem nenhuma variável global, porque as variáveis ​​globais são más 😈.

Abaixo a função handleClick(props.id)}será chamada e retornará uma função e ela terá idem seu escopo! Não importa quantas vezes seja pressionado, os IDs não se afetam ou se alteram, eles são totalmente isolados.

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
)
sultão
fonte
2

O exemplo na sua pergunta é o de um curried functionque faz uso arrow functione possui um implicit returnpara o primeiro argumento.

A função Arrow lexicamente liga isso, ou seja, eles não têm seu próprio thisargumento, mas recebem o thisvalor do escopo

Um equivalente do código acima seria

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

Mais uma coisa a observar sobre o seu exemplo é que define handleChangecomo uma const ou uma função. Provavelmente você o está usando como parte de um método de classe e ele usa umclass fields syntax

então, em vez de vincular a função externa diretamente, você a vincularia no construtor da classe

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

Outra coisa a observar no exemplo é a diferença entre retorno implícito e explícito.

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

Acima está um exemplo de retorno implícito, ie. pega o campo de valor como argumento e retorna o resultadofield*2 que especifica explicitamente a função a ser retornada

Para um retorno explícito, você explicitamente diria ao método para retornar o valor

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

Outra coisa a ser observada sobre as funções das setas é que elas não têm suas próprias arguments mas herdam isso também do escopo dos pais.

Por exemplo, se você apenas definir uma função de seta como

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

Como uma seta alternativa, as funções fornecem os demais parâmetros que você pode usar

const handleChange = (...args) => {
   console.log(args);
}
Shubham Khatri
fonte
1

Pode não estar totalmente relacionado, mas como a pergunta mencionada reage ao caso de uso (e eu continuo colidindo com esse segmento SO): Há um aspecto importante da função de seta dupla que não é explicitamente mencionado aqui. Somente a 'primeira' seta (função) é nomeada (e, portanto, 'distinguível' pelo tempo de execução); quaisquer setas a seguir são anônimas e, do ponto de vista do React, são contadas como um 'novo' objeto em cada renderização.

Portanto, a função de seta dupla fará com que qualquer PureComponent seja renderizado o tempo todo.

Exemplo

Você tem um componente pai com um manipulador de alterações como:

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

e com uma renderização como:

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

handleChange então usado em uma entrada ou clique. E tudo isso funciona e parece muito bom. MAS significa que qualquer alteração que faça com que o pai seja renderizado novamente (como uma mudança de estado completamente não relacionada) também renderizará TODAS o seu MyTask, mesmo que sejam PureComponents.

Isso pode ser aliviado de várias maneiras, como passar a seta 'mais distante' e o objeto com o qual você a alimentaria ou escrever uma função shouldUpdate personalizada ou voltar ao básico, como escrever funções nomeadas (e vincular isso manualmente ...)

Don Kartacs
fonte