JavaScript funcional: o que são funções de ordem superior e por que são necessárias?


"Funções de ordem superior" é uma daquelas frases que geralmente são espalhadas. Mas raramente alguém pode parar e explicar o que é. Você já deve saber o que é chamado de funções de ordem superior. Mas como os usamos em projetos reais? Quando e por que eles são úteis? Podemos manipular o DOM com a ajuda deles? Ou as pessoas que usam esses recursos apenas aparecem? Talvez eles sem sentido complicem o código?

Eu costumava pensar que funções de ordem superior são úteis. Agora, considero-os a propriedade mais importante do JavaScript como idioma. Mas antes de discutirmos isso, vamos primeiro descobrir o que exatamente são funções de ordem superior. E começaremos com funções como variáveis.

Funções como Objetos de Primeira Classe


Em JavaScript, há pelo menos três maneiras (há mais no total) de escrever uma nova função. Primeiro, você pode escrever uma declaração de função :

// Take a DOM element and wrap it in a list item element. function itemise(el) { const li = document.createElement('li'); li.appendChild(el); return li; } 

Espero que você entenda tudo. Além disso, você provavelmente sabe que pode escrever uma expressão de função :

 const itemise = function(el) { const li = document.createElement('li'); li.appendChild(el); return li; } 

E, finalmente, há outra maneira de escrever a mesma função - como uma função de seta :

 const itemise = (el) => { const li = document.createElement('li'); li.appendChild(el); return li; } 

Nesse caso, todos os três métodos são equivalentes. Embora isso nem sempre aconteça, na prática, cada método tem pequenas diferenças associadas ao que acontece com a magia de uma palavra-chave e rótulos específicos nos rastreamentos de pilha.

Mas observe que os dois últimos exemplos atribuem a função a uma variável. Parece um pouco. Por que não atribuir uma função a uma variável? Mas é muito importante. Funções em JavaScript pertencem à " primeira classe ". Portanto, podemos:

  • Atribua funções a variáveis.
  • Passe funções como argumentos para outras funções.
  • Retorna funções de outras funções.

Isso é maravilhoso, mas o que tudo isso tem a ver com funções de ordem superior? Preste atenção nos dois últimos pontos. Em breve retornaremos a eles, mas, por enquanto, vejamos alguns exemplos.

Vimos a atribuição de funções a variáveis. Que tal passá-los como parâmetros? Vamos escrever uma função que possa ser usada com elementos DOM. Se executarmos document.querySelectorAll() , em troca obteremos não uma matriz, mas um NodeList . NodeList não possui um método .map() , como matrizes, portanto, escrevemos isso:

 // Apply a given function to every item in a NodeList and return an array. function elListMap(transform, list) { // list might be a NodeList, which doesn't have .map(), so we convert // it to an array. return [...list].map(transform); } // Grab all the spans on the page with the class 'for-listing'. const mySpans = document.querySelectorAll('span.for-listing'); // Wrap each one inside an <li> element. We re-use the // itemise() function from earlier. const wrappedList = elListMap(itemise, mySpans); 

Aqui passamos a função itemise como argumento para a função elListMap . Mas podemos usar o elListMap não apenas para criar listas. Por exemplo, com sua ajuda, você pode adicionar uma classe a um conjunto de elementos:

 function addSpinnerClass(el) { el.classList.add('spinner'); return el; } // Find all the buttons with class 'loader' const loadButtons = document.querySelectorAll('button.loader'); // Add the spinner class to all the buttons we found. elListMap(addSpinnerClass, loadButtons); 

elLlistMap aceita outra função como parâmetro e converte. Ou seja, podemos usar o elListMap para resolver problemas diferentes.

Vimos um exemplo de passagem de funções como parâmetros. Agora vamos falar sobre retornar uma função de uma função. Como é isso?

Primeiro, escrevemos a função antiga usual. Precisamos pegar uma lista de elementos li e envolver em ul . Fácil:

 function wrapWithUl(children) { const ul = document.createElement('ul'); return [...children].reduce((listEl, child) => { listEl.appendChild(child); return listEl; }, ul); } 

E se temos um monte de elementos de parágrafo que queremos agrupar em uma div ? Não tem problema, vamos escrever mais uma função para isso:

 function wrapWithDiv(children) { const div = document.createElement('div'); return [...children].reduce((divEl, child) => { divEl.appendChild(child); return divEl; }, div); } 

Isso funciona muito bem. No entanto, essas duas funções são muito semelhantes, a única diferença está no elemento pai que criamos.

Agora, podemos escrever uma função que aceita dois parâmetros: o tipo do elemento pai e a lista de elementos filhos. Mas há outra opção. Podemos criar uma função que retorna uma função. Por exemplo:

 function createListWrapperFunction(elementType) { // Straight away, we return a function. return function wrap(children) { // Inside our wrap function, we can 'see' the elementType parameter. const parent = document.createElement(elementType); return [...children].reduce((parentEl, child) => { parentEl.appendChild(child); return parentEl; }, parent); } } 

Pode parecer um pouco complicado no começo, então vamos dividir o código. Criamos uma função que apenas retorna outra função. Mas essa função de retorno lembra o parâmetro elementType . E então, quando chamamos a função retornada, ela já sabe qual elemento criar. Portanto, você pode criar wrapWithUl e wrapWithDiv :

 const wrapWithUl = createListWrapperFunction('ul'); // Our wrapWithUl() function now 'remembers' that it creates a ul element. const wrapWithDiv = createListWreapperFunction('div'); // Our wrapWithDiv() function now 'remembers' that it creates a div element. 

Esse truque, quando a função retornada "lembra" algo, é chamado de encerramento . Você pode ler mais sobre eles aqui . Os fechamentos são incrivelmente convenientes, mas por enquanto não vamos pensar neles.

Então, resolvemos:

  • Atribuindo uma função a uma variável.
  • Passando uma função como um parâmetro.
  • Retornando uma função de outra função ...

Em geral, as funções da primeira classe são agradáveis. Mas o que a função de ordem superior tem a ver com isso? Vamos olhar para a definição.

O que é uma função de ordem superior?


Definição : esta é uma função que aceita uma função como argumento ou retorna uma função como resultado.

Isso é familiar? Em JavaScript, essas são funções de primeira classe. Ou seja, "funções de ordem superior" têm exatamente as mesmas vantagens. Em outras palavras, é apenas um nome fantasioso para uma idéia simples.

Exemplos de funções de ordem superior


Se você começar a procurar, começará a notar funções de ordem superior em todos os lugares. As mais comuns são funções que assumem outras funções como parâmetros.

Funções que assumem outras funções como parâmetros


Ao passar um retorno de chamada, você usa uma função de ordem superior. No desenvolvimento front-end, eles são encontrados em todos os lugares. Um dos mais comuns é o método .addEventListener() . Usamos quando queremos executar ações em resposta a alguns eventos. Por exemplo, quero criar um botão que exibe um aviso:

 function showAlert() { alert('Fallacies do not cease to be fallacies because they become fashions'); } document.body.innerHTML += `<button type="button" class="js-alertbtn"> Show alert </button>`; const btn = document.querySelector('.js-alertbtn'); btn.addEventListener('click', showAlert); 

Aqui, criamos uma função que mostra um aviso, adicionamos um botão à página e passamos a função showAlert() como argumento para btn.addEventListener() .

Também encontramos funções de ordem superior quando usamos métodos de iteração de matriz : por exemplo, .map() , .filter() e .reduce() . Como na função elListMap() :

 function elListMap(transform, list) { return [...list].map(transform); } 

Funções de ordem superior também ajudam a trabalhar com atrasos e tempo. As funções setTimeout() e setInterval() ajudam a controlar quando as funções são executadas. Por exemplo, se você precisar remover a classe de highlight após 30 segundos, poderá fazer o seguinte:

 function removeHighlights() { const highlightedElements = document.querySelectorAll('.highlighted'); elListMap(el => el.classList.remove('highlighted'), highlightedElements); } setTimeout(removeHighlights, 30000); 

Novamente, criamos uma função e a passamos para outra função como argumento.

Como você pode ver, o JavaScript geralmente possui funções que aceitam outras funções. E você provavelmente já os usa.

Funções Funções de retorno


Funções desse tipo não são encontradas com tanta frequência quanto as anteriores. Mas eles também são úteis. Um dos melhores exemplos é a função maybe () . Adaptei uma variante do livro Allongé JavaScript :

 function maybe(fn) return function _maybe(...args) { // Note that the == is deliberate. if ((args.length === 0) || args.some(a => (a == null)) { return undefined; } return fn.apply(this, args); } } 

Em vez de entender o código, vamos primeiro ver como ele pode ser aplicado. Vejamos a função elListMap() :

 // Apply a given function to every item in a NodeList and return an array. function elListMap(transform, list) { // list might be a NodeList, which doesn't have .map(), so we convert // it to an array. return [...list].map(transform); } 

O que acontece se eu passar acidentalmente um valor nulo ou indefinido para elListMap() ? Obteremos um TypeError e uma queda da operação atual, seja ela qual for. Isso pode ser evitado com a função maybe() :

 const safeElListMap = maybe(elListMap); safeElListMap(x => x, null); // ← undefined 

Em vez de cair, a função retornará undefined . E se passássemos isso para outra função protegida por maybe() , ficaríamos novamente undefined . maybe() possa proteger qualquer número de funções, é muito mais fácil escrever um bilhão de if .

Funções que retornam funções também são comuns no mundo React. Por exemplo, connect() .

Então, o que vem a seguir?


Vimos vários exemplos de uso de funções de ordem superior. Então, o que vem a seguir? O que eles podem nos dar aquilo que não podemos obter sem eles?

Para responder a essa pergunta, vejamos outro exemplo - o método de matriz .sort() . Sim, ele tem falhas. Ele altera a matriz em vez de retornar uma nova. Mas vamos esquecer isso por enquanto. O método .sort() é uma função de ordem superior; ele assume outra função como um dos parâmetros.

Como isso funciona? Se quisermos ordenar uma matriz de números, primeiro precisamos criar uma função de comparação:

 function compareNumbers(a, b) { if (a === b) return 0; if (a > b) return 1; /* else */ return -1; } 

Agora classifique a matriz:

 let nums = [7, 3, 1, 5, 8, 9, 6, 4, 2]; nums.sort(compareNumbers); console.log(nums); // 〕[1, 2, 3, 4, 5, 6, 7, 8, 9] 

Você pode classificar listas de números. Mas que bom é isso? Quantas vezes temos uma lista de números para classificar? Frequentemente não. Normalmente, eu preciso classificar uma matriz de objetos:

 let typeaheadMatches = [ { keyword: 'bogey', weight: 0.25, matchedChars: ['bog'], }, { keyword: 'bog', weight: 0.5, matchedChars: ['bog'], }, { keyword: 'boggle', weight: 0.3, matchedChars: ['bog'], }, { keyword: 'bogey', weight: 0.25, matchedChars: ['bog'], }, { keyword: 'toboggan', weight: 0.15, matchedChars: ['bog'], }, { keyword: 'bag', weight: 0.1, matchedChars: ['b', 'g'], } ]; 

Suponha que eu queira classificar essa matriz pelo peso de cada registro. Eu poderia escrever uma nova função de classificação do zero. Mas por que, se você pode criar uma nova função de comparação:

 function compareTypeaheadResult(word1, word2) { return -1 * compareNumbers(word1.weight, word2.weight); } typeaheadMatches.sort(compareTypeaheadResult); console.log(typeaheadMatches); // 〕[{keyword: "bog", weight: 0.5, matchedChars: ["bog"]}, … ] 

Você pode escrever uma função de comparação para qualquer tipo de matriz. O método .sort() nos ajuda: “Se você me der uma função de comparação, classificarei qualquer matriz. Não se preocupe com seu conteúdo. Se você der uma função de classificação, eu classificarei. Portanto, não precisamos escrever um algoritmo de classificação por conta própria, vamos nos concentrar na tarefa muito mais simples de comparar dois elementos.

Agora imagine que não estamos usando funções de ordem superior. Não podemos passar uma função para o método .sort() . Teremos que escrever uma nova função de classificação sempre que precisarmos classificar uma matriz de um tipo diferente. Ou você precisa reinventar a mesma coisa com ponteiros de função ou objetos. De qualquer forma, ficará desajeitado.

No entanto, temos funções de ordem superior que nos permitem separar a função de classificação da função de comparação. Digamos que um desenvolvedor de navegador inteligente tenha atualizado .sort() para usar um algoritmo mais rápido. Em seguida, seu código só vencerá, independentemente do que estiver dentro das matrizes classificáveis. E esse esquema é verdadeiro para todo um conjunto de funções de matrizes de ordem superior .

Isso nos leva a essa ideia. O método .sort() abstrai a tarefa de classificação do conteúdo da matriz. Isso é chamado de separação de preocupações. Funções de ordem superior permitem criar abstrações que sem elas seriam muito complicadas ou mesmo impossíveis. E a criação de abstrações é responsável por 80% do trabalho dos engenheiros de software.

Quando refatoramos o código para remover repetições, criamos abstrações. Vemos o padrão e o substituímos por uma representação abstrata. Como resultado, o código se torna mais significativo e mais fácil de entender. Pelo menos esse é o objetivo.

Funções de ordem superior são uma ferramenta poderosa para criar abstrações. E com abstrações, um ramo inteiro da matemática, a teoria das categorias, está associado. Mais precisamente, a teoria das categorias é dedicada à busca de abstrações de abstrações. Em outras palavras, estamos falando sobre encontrar padrões de padrões. E, nos últimos 70 anos, programadores inteligentes emprestaram muitas idéias de lá, que se transformaram em propriedades de linguagens e bibliotecas. Se aprendermos esses padrões, às vezes podemos substituir grandes pedaços de código. Ou simplifique problemas complexos para combinações elegantes de blocos de construção simples. Esses blocos são funções de ordem superior. Portanto, eles são tão importantes que nos fornecem uma ferramenta poderosa para combater a complexidade do nosso código.

Materiais adicionais sobre funções de ordem superior:


Você provavelmente já está usando funções de ordem superior. Isso é tão fácil no JavaScript que nem pensamos nisso. Mas é melhor saber do que as pessoas estão falando quando dizem essa frase. Isto não é difícil. Por trás de uma idéia simples, porém, há muito poder.

Se você tem experiência em programação funcional, pode notar que eu não usei funções puras e alguns ... nomes de funções detalhados. Isso não é porque eu não tenha ouvido falar de funções impuras ou dos princípios gerais da programação funcional. E eu não escrevo esse código em produção. Tentei pegar exemplos práticos que seriam claros para iniciantes. Às vezes eu tinha que me comprometer. Se estiver interessado, já escrevi sobre limpeza funcional e os princípios gerais de programação funcional .

Source: https://habr.com/ru/post/pt458896/


All Articles