Este artigo é destinado a uma pessoa que dá seus primeiros passos tímidos no caminho difícil de aprender JavaScript. Apesar de, em 2018, eu uso a sintaxe ES5 para que o artigo possa ser entendido pelos jovens padawans que estão participando do curso de nível 1 do JavaScript na HTML Academy.Um dos recursos que distingue JS de muitas outras linguagens de programação é que nessa linguagem uma função é um "objeto de primeira classe". Ou, em russo, uma função é um significado. Igual a um número, sequência ou objeto. Podemos escrever uma função em uma variável, podemos colocá-la em uma matriz ou em uma propriedade de objeto. Podemos até somar duas funções. De fato, nada significativo virá disso, mas como um fato - nós podemos!
function hello(){}; function world(){}; console.log(hello + world);
O mais interessante é que podemos criar funções que operam em outras funções - aceitando-as como argumentos ou retornando-as como um valor. Tais funções são chamadas
funções de ordem superior . E hoje nós, meninas e meninos, falaremos sobre como adaptar essa oportunidade às necessidades da economia nacional. Ao longo do caminho, você aprenderá mais sobre alguns recursos úteis de funções em JS.
Pipeline
Digamos que temos algo com o qual você precisa fazer muitas peças. Digamos que um usuário tenha carregado um arquivo de texto que armazena dados no formato JSON, e queremos processar seu conteúdo. Primeiro, precisamos aparar os caracteres extras de espaço em branco, que podem "crescer" nas bordas como resultado de ações do usuário ou do sistema operacional. Em seguida, verifique se não há código malicioso no texto (quem sabe, esses usuários). Em seguida, passe do texto para o objeto usando o método
JSON.parse
. Em seguida, remova os dados que precisamos desse objeto. E no final - envie esses dados para o servidor. Você obtém algo assim:
function trim(){}; function sanitize(){}; function parse(){}; function extractData(){}; function send(){}; var textFromFile = getTextFromFile(); send(extractData(parse(sanitize(trim(testFromFile))));
Parece tão de acordo. Além disso, você provavelmente não percebeu a falta de um colchete de fechamento. Obviamente, o IDE diria isso, mas ainda há um problema. Para resolvê-lo, um
novo operador |> foi proposto recentemente. De fato, não é novo, mas honestamente emprestado de linguagens funcionais, mas esse não é o ponto. Usando este operador, a última linha pode ser reescrita da seguinte maneira:
textFromFile |> trim |> sanitize |> parse |> extractData |> send;
O operador |> pega seu operando esquerdo e o passa para o operando direito como argumento. Por exemplo,
"Hello" |> console.log
equivalente a
console.log("Hello")
. Isso é muito conveniente exatamente para os casos em que várias funções são chamadas ao longo da cadeia. No entanto, antes da introdução desse operador, muito tempo passará (se essa proposta for aceita), mas você precisa viver de alguma forma agora. Portanto, podemos escrever para nossa
bicicleta uma função que simula esse comportamento:
function pipe(){ var args = Array.from(arguments); var result = args.shift(); while(args.length){ var f = args.shift(); result = f(result); } return result; } pipe( textFromFile, trim, sanitize, parse, extractData, send );
Se você é um javascriptist iniciante (javascript? Javascript?), A primeira linha da função pode parecer incompreensível para você. É simples: dentro da função, usamos a palavra-chave
argumentos para acessar um objeto semelhante a um array contendo todos os argumentos passados para a função. Isso é muito conveniente quando não sabemos antecipadamente quantos argumentos ela terá. Um objeto maciço é como uma matriz, mas não exatamente. Portanto, nós o convertemos em uma matriz normal usando o método
Array.from
. O código adicional, espero, já é bastante legível: começamos da esquerda para a direita para extrair elementos da matriz e aplicá-los um ao outro da mesma maneira que o operador |> faria.
Registo
Aqui está outro exemplo próximo à vida real. Suponha que já tenhamos uma função
f
que faça ... algo útil. E, no processo de testar nosso código, queremos saber mais sobre exatamente como ele faz isso. Em que momentos é chamado, quais argumentos são passados, quais valores são retornados.
Obviamente, com cada chamada de função, podemos escrever o seguinte:
var result = f(a, b); console.log(" f " + a + " " + b + " " + result); console.log(" : " + Date.now());
Mas, em primeiro lugar, é bastante complicado. E segundo, é muito fácil esquecer isso. Um dia, simplesmente escreveremos
f(a, b)
e, desde então, a escuridão da ignorância se instalará em nossas mentes. Ele se expandirá a cada novo desafio
f
, do qual nada sabemos.
Idealmente, eu gostaria que o log acontecesse automaticamente. Para que toda vez que você chame
f
, todas as coisas que precisamos sejam gravadas no console. E, felizmente, temos uma maneira de fazer isso. Conheça o novo recurso de ordem superior!
function addLogger(f){ return function(){ var args = Array.from(arguments); var result = f.apply(null, args); console.log(" " + f.name + " " + args.join() + " " + result + "\n" + " : " + Date.now()); return result; } } function sum(a, b){ return a + b; } var sumWithLogging = addLogger(sum); sum(1, 2);
Uma função pega uma função e retorna uma função que chama a função passada para a função quando a função foi criada. Desculpe, não consegui parar de escrever isso. Agora em russo: a função
addLogger
cria um
addLogger
torno da função transmitida a ela como argumento. A embalagem também é uma função. Quando chamado, ele coleta uma matriz de seus argumentos da mesma maneira que fizemos no exemplo anterior. Em seguida, usando o método
apply , ele chama uma função agrupada com os mesmos argumentos e lembra o resultado. Depois disso, o wrapper grava tudo no console.
Aqui temos o caso clássico de ataque do tipo homem do meio. Se você usar um wrapper em vez de
f
, do ponto de vista do código que o utiliza, praticamente não haverá diferença. O código pode assumir que ele se comunica diretamente com
f
. Enquanto isso, o invólucro informa tudo ao camarada major.
Eins, zwei, drei, vier ...
E mais uma tarefa próxima à prática. Suponha que precisamos numerar algumas entidades. Cada vez que uma nova entidade aparece, obtemos um novo número, mais um que o anterior. Para fazer isso, iniciamos uma função do seguinte formato:
var lastNumber = 0; function getNewNumber(){ return lastNumber++; }
E então temos um novo tipo de entidade. Digamos, antes disso, numeramos os coelhos e agora também existem coelhos. Se você usar uma função para esses e outros, cada número emitido para coelhos fará um "buraco" na série de números emitidos para coelhos. Então, precisamos da segunda função e, com ela, a segunda variável:
var lastHareNumber = 0; function getNewHareNumber(){ return lastHareNumber++; } var lastRabbitNumber = 0; function getNewRabbitNumber(){ return lastRabbitNumber++; }
Você sente que esse código cheira mal? Eu gostaria de ter algo melhor. Em primeiro lugar, eu gostaria de poder declarar essas funções menos detalhadas, sem duplicar o código. E, em segundo lugar, gostaria de "empacotar" a variável que a função usa na própria função para não entupir o espaço de nome mais uma vez.
E então um homem explode, familiarizado com o conceito de POO, e diz:"Elementar, Watson." É necessário fazer geradores de números não objetos, mas objetos. Os objetos são projetados apenas para armazenar funções que funcionam com dados, juntamente com esses mesmos dados. Então poderíamos escrever algo como:
var numberGenerator = new NumberGenerator(); var n = numberGenerator.get();
A que responderei:
- Para ser sincero, concordo plenamente com você. E, em princípio, esta é uma abordagem mais correta do que o que vou oferecer agora. Mas aqui temos um artigo sobre funções, não sobre OOP. Então você poderia ficar quieto por um tempo e me deixar terminar?
Aqui (surpresa!) A função de ordem superior nos ajudará novamente.
function createNumberGenerator(){ var n = 0; return function(){ return n++; } } var getNewHareNumber = createNumberGenerator(); var getNewRabbitNumber = createNumberGenerator(); console.log( getNewHareNumber(), getNewHareNumber(), getNewHareNumber(), getNewRabbitNumber(), getNewRabbitNumber(), );
E aqui algumas pessoas podem ter uma pergunta, talvez até de forma obscena: que diabos está acontecendo? Por que estamos criando uma variável que não é usada na própria função? Como uma função interna acessa se uma função externa concluiu sua execução há muito tempo? Por que duas funções criadas referentes à mesma variável obtêm resultados diferentes? Uma resposta para todas essas perguntas é o
encerramento .
Cada vez que a função
createNumberGenerator
é
createNumberGenerator
, o interpretador JS cria uma coisa mágica chamada "contexto de execução". Grosso modo, esse é um objeto no qual todas as variáveis declaradas nessa função são armazenadas. Não podemos acessá-lo como um objeto javascript comum, mas mesmo assim é.
Se a função era “simples” (digamos, adicionando números), depois do final de seu trabalho, o contexto de execução acaba sendo inútil. Você sabe o que acontece com dados desnecessários em JS? Eles são devorados por um demônio insaciável chamado Garbage Collector. No entanto, se a função foi "complicada", pode acontecer que alguém ainda precise de seu contexto, mesmo após a execução dessa função. Nesse caso, o Garbage Collector o poupa, e ele permanece pendurado em algum lugar em sua memória, para que aqueles que precisam dele ainda possam ter acesso a ele.
Assim, a função retornada por
createNumberGenerator
sempre terá acesso a sua própria cópia da variável
n
. Você pode pensar nisso como Bag of Holding do D&D. Você coloca a mão na sua bolsa e se encontra em um "bolso" interdimensional pessoal, onde pode guardar tudo o que quiser.
Debounce
Existe algo como "eliminar o ressalto". É quando não queremos que alguma função seja chamada com muita frequência. Suponha que exista um certo botão, clicando no botão que inicia um processo "caro" (demorado ou com muita memória, ou com a Internet ou sacrificando virgens). Pode acontecer que um usuário impaciente comece a clicar nesse botão com uma frequência superior a dez hertz. Além disso, o processo mencionado acima tem uma natureza que não faz sentido executá-lo dez vezes seguidas, porque o resultado final não muda. É então que aplicamos a "eliminação de conversas".
Sua essência é muito simples: executamos a função não imediatamente, mas depois de algum tempo. Se antes desse período, a função foi chamada novamente, "redefinimos o timer". Assim, o usuário pode clicar no botão pelo menos mil vezes - apenas uma é necessária para o sacrifício. No entanto, menos palavras, mais código:
function debounce(f, delay){ var lastTimeout; return function(){ if(lastTimeout){ clearTimeout(lastTimeout); } var args = Array.from(arguments); lastTimeout = setTimeout(function(){ f.apply(null, args); }, delay); } } function sacrifice(name){ console.log(name + " * *"); } function sacrificeDebounced = debounce(sacrifice, 500); sacrificeDebounced(""); sacrificeDebounced(""); sacrificeDebounced("");
Em meio segundo, Lena será sacrificada e Katya e Sveta sobreviverão graças à nossa função mágica.
Se você ler atentamente os exemplos anteriores, deve entender bem como tudo funciona aqui. A função de wrapper criada pelo
debounce
aciona a execução atrasada da função original usando
setTimeout . Nesse caso, o identificador de tempo limite é armazenado na variável lastTimeout, que é acessível ao wrapper devido ao fechamento. Se o identificador de tempo limite já estiver nessa variável, o wrapper cancelará esse tempo limite com
clearTimeout . Se o tempo limite anterior já foi concluído, nada acontece. Se não, tanto pior para ele.
Nisto, talvez, eu terminarei. Espero que hoje você tenha aprendido muitas coisas novas e, o mais importante, que tenha entendido tudo o que aprendeu. Vejo você de novo.