Os últimos anos mostraram-nos que o desenvolvimento Web está a mudar. À medida que muitas funcionalidades e APIs foram sendo adicionadas aos browsers, tivemos de as utilizar da forma correta. A linguagem a que devemos esta honra foi a JavaScript.
Inicialmente, os programadores não estavam convencidos da forma como foi concebido e tiveram, na sua maioria, impressões negativas ao utilizarem este script. Com o tempo, verificou-se que esta linguagem tem um grande potencial e as normas ECMAScript subsequentes tornaram alguns dos mecanismos mais humanos e, simplesmente, melhores. Neste artigo, damos uma vista de olhos a algumas delas.
Tipos de valores em JS
A verdade bem conhecida sobre JavaScript é que tudo aqui é um objeto. A sério, tudo: matrizes, funções, cadeias de caracteres, números e até booleanos. Todos os tipos de valores são representados por objectos e têm os seus próprios métodos e campos. No entanto, podemos dividi-los em duas categorias: primitivos e estruturais. Os valores da primeira categoria são imutáveis, o que significa que podemos reatribuir uma variável com um novo valor, mas não podemos modificar o valor existente. A segunda representa valores que podem ser alterados, pelo que devem ser interpretados como colecções de propriedades que podemos substituir ou simplesmente chamar os métodos concebidos para o fazer.
Âmbito das variáveis declaradas
Antes de nos aprofundarmos, vamos explicar o que significa o âmbito. Podemos dizer que o âmbito é a única área onde podemos utilizar as variáveis declaradas. Antes do padrão ES6, podíamos declarar variáveis com a instrução var e dar a elas escopo global ou local. O primeiro é um domínio que permite nós para aceder a algumas variáveis em qualquer local da aplicação, o segundo é apenas dedicado a uma área específica - principalmente uma função.
Desde a norma ES2015, JavaScript tem três maneiras de declarar variáveis que diferem com a palavra-chave. A primeira é descrita anteriormente: as variáveis declaradas pela palavra-chave var têm o âmbito do corpo da função atual. A norma ES6 permitiu-nos declarar variáveis de formas mais humanas - ao contrário das declarações var, as variáveis declaradas pelas declarações const e let têm apenas o âmbito do bloco. No entanto, o JS trata a declaração const de forma bastante invulgar se comparada com outras declarações linguagens de programação - em vez de um valor persistente, mantém uma referência persistente para o valor. Em suma, podemos modificar as propriedades de um objeto declarado com uma declaração const, mas não podemos substituir a referência desta variável. Há quem diga que a alternativa var no ES6 é, na verdade, uma declaração let. Não, não é, e a declaração var não é e provavelmente nunca será retirada. Uma boa prática é evitar a utilização de declarações var, porque a maior parte das vezes dão-nos mais problemas. Por sua vez, temos de abusar das declarações const, até termos de modificar a sua referência - então devemos usar let.
Exemplo de comportamento inesperado do âmbito
Comecemos pelo seguinte código:
(() => {
for (var i = 0; i {
console.log(`Valor de "i": ${i}`);
}, 1000);
}
})();
Quando olhamos para ele, parece que o loop for itera o valor i e, após um segundo, regista os valores do iterador: 1, 2, 3, 4, 5. Bem, isso não acontece. Como mencionámos acima, a instrução var tem como objetivo manter o valor de uma variável durante todo o corpo da função; isto significa que na segunda, terceira e assim sucessivamente, o valor da variável i será substituído por um valor seguinte. Finalmente, o ciclo termina e os ticks de timeout mostram-nos o seguinte: 5, 5, 5, 5, 5, 5. A melhor forma de manter um valor atual do iterador é utilizar a instrução let:
(() => {
for (let i = 0; i {
console.log(`Valor de "i": ${i}`);
}, 1000);
}
})();
No exemplo acima, mantemos o âmbito do valor i no bloco de iteração atual, é o único domínio onde podemos utilizar esta variável e nada a pode substituir fora desta área. O resultado neste caso é o esperado: 1 2 3 4 5. Vamos ver como lidar com esta situação com uma declaração var:
(() => {
for (var i = 0; i {
setTimeout(() => {
console.log(`Valor de "j": ${j}`);
}, 1000);
})(i);
}
})();
Uma vez que a declaração var se destina a manter o valor dentro do bloco de funções, temos de chamar uma função definida que recebe um argumento - o valor do estado atual do iterador - e depois fazer algo. Nada fora da função declarada irá sobrepor-se ao valor j.
Exemplos de expectativas erradas de valores de objectos
O crime mais frequente que notei diz respeito a ignorar o poder das estruturas e alterar as suas propriedades que também são modificadas noutras partes do código. Dê uma olhadela rápida:
const DEFAULT_VALUE = {
favoriteBand: 'The Weeknd'
};
const currentValue = DEFAULT_VALUE;
const bandInput = document.querySelector('#favorite-band');
const restoreDefaultButton = document.querySelector('#restore-button');
bandInput.addEventListener('input', () => {
currentValue.favoriteBand = bandInput.value;
}, false);
restoreDefaultButton.addEventListener('click', () => {
currentValue = DEFAULT_VALUE;
}, false);
Desde o início: vamos assumir que temos um modelo com propriedades predefinidas, armazenadas como um objeto. Queremos ter um botão que restaure os valores de entrada para os valores predefinidos. Depois de preencher a entrada com alguns valores, actualizamos o modelo. Passado um momento, pensamos que a escolha por defeito era simplesmente melhor, por isso queremos restaurá-la. Clicamos no botão... e nada acontece. Porquê? Porque ignoramos o poder dos valores referenciados.
Esta parte: const currentValue = DEFAULTVALUE está a dizer ao JS o seguinte: pegue na referência para a variável DEFAULTVALUE e atribui a variável currentValue a esse valor. O valor real é armazenado na memória apenas uma vez e ambas as variáveis estão a apontar para ele. Modificar algumas propriedades num local significa modificá-las noutro. Temos algumas formas de evitar situações como esta. Uma que satisfaz as nossas necessidades é um operador de propagação. Vamos corrigir o nosso código:
const DEFAULT_VALUE = {
favoriteBand: 'The Weeknd'
};
const currentValue = { ...DEFAULT_VALUE };
const bandInput = document.querySelector('#favorite-band');
const restoreDefaultButton = document.querySelector('#restore-button');
bandInput.addEventListener('input', () => {
currentValue.favoriteBand = bandInput.value;
}, false);
restoreDefaultButton.addEventListener('click', () => {
currentValue = { ...DEFAULT_VALUE };
}, false);
Neste caso, o operador de dispersão funciona da seguinte forma: pega em todas as propriedades de um objeto e cria um novo objeto preenchido com elas. Graças a isto, os valores em currentValue e DEFAULT_VALUE já não estão a apontar para o mesmo local na memória e todas as alterações aplicadas a um deles não afectarão os outros.
Ok, então a questão é: trata-se apenas de utilizar o operador mágico de dispersão? Neste caso - sim, mas os nossos modelos podem exigir mais complexidade do que este exemplo. No caso de utilizarmos objectos aninhados, arrays ou quaisquer outras estruturas, o operador de dispersão do valor referenciado de nível superior só afectará o nível superior e as propriedades referenciadas continuarão a partilhar o mesmo lugar na memória. Há muitas soluções para lidar com este problema, tudo depende das suas necessidades. Podemos clonar objectos em todos os níveis de profundidade ou, em operações mais complexas, utilizar ferramentas como o immer, que nos permite escrever código imutável de forma quase indolor.
Misturar tudo
É possível utilizar uma mistura de conhecimentos sobre âmbitos de aplicação e tipos de valores? Claro que sim! Vamos construir algo que utilize ambos:
const useValue = (defaultValue) => {
const value = [...defaultValue];
const setValue = (newValue) => {
value.length = 0; // maneira complicada de limpar o array
newValue.forEach((item, index) => {
value[index] = item;
});
// faz algumas outras coisas
};
retorna [valor, setValue];
};
const [animals, setAnimals] = useValue(['cat', 'dog']);
console.log(animals); // ['cat', 'dog']
setAnimals(['horse', 'cow']);
consola.log(animais); // ['cavalo', 'vaca']);
Vamos explicar como este código funciona linha a linha. Bem, a função useValue está a criar uma matriz com base no argumento defaultValue; cria uma variável e outra função, o seu modificador. Este modificador recebe um novo valor que é, de uma forma complicada, aplicado ao valor existente. No final da função, devolvemos o valor e o seu modificador como valores da matriz. De seguida, utilizamos a função criada - declaramos animais e setAnimals como valores devolvidos. Utilize o seu modificador para verificar se a função afecta a variável animal - sim, funciona!
Mas espere, o que é que este código tem de especial? A referência mantém todos os novos valores e pode injetar a sua própria lógica neste modificador, como por exemplo algumas APIs ou uma parte do ecossistema que alimenta seu fluxo de dados sem nenhum esforço. Esse padrão complicado é frequentemente usado em bibliotecas JS mais modernas, onde o paradigma funcional na programação nos permite manter o código menos complexo e mais fácil de ler por outros programadores.
Resumo
A compreensão de como a mecânica da linguagem funciona nos bastidores permite-nos escrever código mais consciente e leve. Mesmo que JS não seja uma linguagem de baixo nível e nos obrigue a ter algum conhecimento sobre como a memória é atribuída e armazenada, ainda temos de estar atentos a comportamentos inesperados quando modificamos objectos. Por outro lado, abusar de clones de valores nem sempre é o caminho certo e a utilização incorrecta tem mais contras do que vantagens. A forma correta de planear o fluxo de dados é considerar o que precisa e os possíveis obstáculos que pode encontrar ao implementar a lógica da aplicação.
Ler mais: