
O JavaScript sempre me surpreendeu, antes de tudo, porque provavelmente como nenhuma outra linguagem difundida suporta os dois paradigmas ao mesmo tempo: programação normal e anormal. E se quase tudo foi lido sobre as melhores práticas e modelos adequados, o maravilhoso mundo de como não escrever código, mas é possível, permanece apenas um pouco entreaberto.
Neste artigo, analisaremos outra tarefa artificial que exige abuso indesculpável de uma solução normal.
Tarefa anterior:
Implemente uma função decoradora que conte o número de chamadas para a função passada e ofereça a capacidade de obter esse número sob demanda. A solução não usa colchetes e variáveis globais.
O contador de chamadas é apenas uma desculpa, porque existe console.count () . A conclusão é que nossa função acumula alguns dados ao chamar uma função agrupada e fornece uma certa interface para acessá-la. Pode estar salvando todos os resultados de uma chamada, coletando logs e algum tipo de memorização. Apenas um contra-primitivo e compreensível para todos.
Toda a complexidade está em uma restrição anormal. Você não pode usar chaves, o que significa que você deve reconsiderar as práticas cotidianas e a sintaxe comum.
Solução habitual
Primeiro você precisa escolher um ponto de partida. Normalmente, se o idioma ou sua extensão não fornecer a função de decoração necessária, implementaremos algum contêiner por conta própria: uma função agrupada, dados acumulados e uma interface para acessá-los. Isso geralmente é uma classe:
class CountFunction { constructor(f) { this.calls = 0; this.f = f; } invoke() { this.calls += 1; return this.f(...arguments); } } const csum = new CountFunction((x, y) => x + y); csum.invoke(3, 7);
Isso não nos convém imediatamente, pois:
- Em JavaScript, você não pode implementar uma propriedade privada dessa maneira: podemos ler as chamadas da instância (de que precisamos) e escrever o valor de fora (de que NÃO precisamos). Obviamente, podemos usar o fechamento no construtor , mas qual é o significado da classe? E eu ainda teria medo de usar campos particulares frescos sem o babel 7 .
- A linguagem suporta um paradigma funcional, e criar uma instância por meio de novas parece não ser a melhor solução aqui. É melhor escrever uma função que retorne outra função. Sim
- Finalmente, a sintaxe de ClassDeclaration e MethodDefinition não nos permitirá livrar-nos de todos os chavetas.
Mas temos um maravilhoso padrão de módulo que implementa a privacidade usando o fechamento:
function count(f) { let calls = 0; return { invoke: function() { calls += 1; return f(...arguments); }, getCalls: function() { return calls; } }; } const csum = count((x, y) => x + y); csum.invoke(3, 7);
Você já pode trabalhar com isso.
Decisão divertida
Por que os aparelhos são usados aqui? Estes são 4 casos diferentes:
- Definindo o corpo de uma função de contagem ( FunctionDeclaration )
- Inicialização do objeto retornado
- A definição do corpo da função de chamada ( FunctionExpression ) com duas expressões
- Definindo o corpo de uma função getCalls ( FunctionExpression ) com uma única expressão
Vamos começar com o segundo parágrafo. De fato, não precisamos retornar um novo objeto, enquanto complicamos a invocação da função final por meio da invocação . Podemos tirar proveito do fato de que uma função em JavaScript é um objeto, o que significa que ela pode conter seus próprios campos e métodos. Vamos criar nossa função return df e adicionar o método getCalls , que, através do fechamento, terá acesso às chamadas como antes:
function count(f) { let calls = 0; function df() { calls += 1; return f(...arguments); } df.getCalls = function() { return calls; } return df; }
É mais agradável trabalhar com isso:
const csum = count((x, y) => x + y); csum(3, 7);
O quarto ponto é claro: apenas substituímos FunctionExpression por ArrowFunction . A ausência de chaves nos fornecerá um breve registro da função de seta no caso de uma única expressão em seu corpo:
function count(f) { let calls = 0; function df() { calls += 1; return f(...arguments); } df.getCalls = () => calls; return df; }
Com o terceiro - tudo é mais complicado. Lembre-se de que a primeira coisa que fizemos foi substituir FunctionExpression por funções de chamada por FunctionDeclaration df . Para reescrever isso no ArrowFunction, dois problemas terão que ser resolvidos: não perder o acesso aos argumentos (agora é uma pseudo-matriz de argumentos ) e evitar o corpo da função de duas expressões.
O primeiro problema nos ajudará a lidar com o especificado explicitamente para o parâmetro de função args com o operador spread . E para combinar duas expressões em uma, você pode usar AND lógico . Diferentemente do operador de conjunção lógica clássica que retorna Boolean, ele calcula operandos da esquerda para a direita para o primeiro "false" e o retorna, e se todos são "true" - então o último valor. O primeiro incremento do contador nos dará 1, o que significa que essa subexpressão sempre será convertida em true. A redutibilidade à "verdade" do resultado da chamada de função na segunda subexpressão não nos interessa: em qualquer caso, a calculadora para nela. Agora podemos usar o ArrowFunction :
function count(f) { let calls = 0; let df = (...args) => (calls += 1) && f(...args); df.getCalls = () => calls; return df; }
Você pode decorar um registro um pouco usando o incremento do prefixo:
function count(f) { let calls = 0; let df = (...args) => ++calls && f(...args); df.getCalls = () => calls; return df; }
A solução para o primeiro e mais difícil ponto começará substituindo FunctionDeclaration por ArrowFunction . Mas ainda temos o corpo em chaves:
const count = f => { let calls = 0; let df = (...args) => ++calls && f(...args); df.getCalls = () => calls; return df; };
Se quisermos nos livrar de chaves, enquadrando o corpo da função, teremos que evitar declarar e inicializar variáveis através de let . E nós temos duas variáveis inteiras: calls e df .
Primeiro, vamos lidar com o balcão. Podemos criar uma variável local, definindo-a na lista de parâmetros de função, e transferir o valor inicial chamando-o usando IIFE (Expressão de Função Invocada Imediatamente):
const count = f => (calls => { let df = (...args) => ++calls && f(...args); df.getCalls = () => calls; return df; })(0);
Resta concatenar as três expressões em uma. Como temos todas as três expressões que representam funções que são sempre redutíveis em true, também podemos usar AND lógico :
const count = f => (calls => (df = (...args) => ++calls && f(...args)) && (df.getCalls = () => calls) && df)(0);
Mas há outra opção para concatenar expressões: usando o operador vírgula . É preferível, pois não lida com transformações lógicas desnecessárias e requer menos colchetes. Os operandos são avaliados da esquerda para a direita e o resultado é o valor do último:
const count = f => (calls => (df = (...args) => ++calls && f(...args), df.getCalls = () => calls, df))(0);
Acho que consegui enganar você? Nós nos livramos ousadamente da declaração da variável df e deixamos apenas a atribuição da nossa função de seta. Nesse caso, essa variável será declarada globalmente, o que é inaceitável! Para df , repetimos a inicialização da variável local nos parâmetros de nossa função IIFE, mas não passaremos nenhum valor inicial:
const count = f => ((calls, df) => (df = (...args) => ++calls && f(...args), df.getCalls = () => calls, df))(0);
Assim, o objetivo é alcançado.
Variações sobre um tema
Curiosamente, fomos capazes de evitar a criação e a inicialização de variáveis locais, várias expressões em blocos de funções e a criação de um objeto literal. Ao mesmo tempo, a solução original foi mantida limpa: a ausência de variáveis globais, contra-privacidade, acesso aos argumentos da função que está sendo quebrada.
Em geral, você pode tomar qualquer implementação e tentar fazer algo semelhante. Por exemplo, o polyfill para a função bind a esse respeito é bastante simples:
const bind = (f, ctx, ...a) => (...args) => f.apply(ctx, a.concat(args));
No entanto, se o argumento f não for uma função, devemos lançar uma exceção de uma maneira positiva. E a exceção de lançamento não pode ser lançada no contexto da expressão. Você pode esperar pelas expressões de lançamento (estágio 2) e tentar novamente. Ou alguém já tem pensamentos agora?
Ou considere uma classe que descreve as coordenadas de um ponto:
class Point { constructor(x, y) { this.x = x; this.y = y; } toString() { return `(${this.x}, ${this.y})`; } }
O que pode ser representado por uma função:
const point = (x, y) => (p => (px = x, py = y, p.toString = () => ['(', x, ', ', y, ')'].join(''), p))(new Object);
Somente aqui perdemos a herança do protótipo: toString é uma propriedade do objeto de protótipo Point , não um objeto criado separadamente. Isso pode ser evitado se você se esforçar?
Nos resultados das transformações, obtemos uma mistura prejudicial de programação funcional com hacks imperativos e alguns recursos da própria linguagem. Se você pensar bem, isso pode se tornar um ofuscador interessante (mas não prático) do código-fonte. Você pode criar sua própria versão da tarefa "ofuscamento de bracketing" e divertir colegas e amigos de JavaScript em seu tempo livre com um trabalho útil.
Conclusão
A questão é: para quem é útil e por que é necessário? Isso é completamente prejudicial para iniciantes, pois forma uma idéia falsa da excessiva complexidade e desvio do idioma. Mas pode ser útil para os profissionais, pois permite que você observe os recursos do idioma do outro lado: a ligação não deve ser evitada e a ligação é a tentativa de evitar no futuro.