"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 :
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:
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; }
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) {
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');
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) {
Em vez de entender o código, vamos primeiro ver como ele pode ser aplicado. Vejamos a função
elListMap()
:
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);
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; return -1; }
Agora classifique a matriz:
let nums = [7, 3, 1, 5, 8, 9, 6, 4, 2]; nums.sort(compareNumbers); console.log(nums);
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);
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 .