“Quando você faz em fração de segundo o que os outros levariam horas para fazer, tudo parece mágica.” ― Steve Jobs
Escopos de bloco e de função
Observe o exemplo retirado do site do Mozilla:
function varBlockTest() {
var x = 31;
if (true) {
var x = 71; // same variable!
console.log(x); // 71
}
console.log(x); // 71
}
Qualquer programador que tenha trabalhado com linguagens com escopo de bloco esperaria que o segundo retorno fosse 31. O Javascript, entretanto, possui, além do global, apenas escopo de função. Isso significa que qualquer variável só terá uma outra referência quando declarada dentro de uma outra função. Vejamos:
function varClosureTest() {
var x = 31;
(function(){
if (true) {
var x = 71; // different variable!
console.log(x); // 71
}
})();
console.log(x); // 31
}
Em outras palavras, o que define um novo escopo em javascript é a abertura de chaves{ } de uma função e não a abertura de chaves de cláusulas condicionais ou loops, como acontece em linguagens como C.
Se você não entendeu o exemplo acima não se preocupe, vamos explicar melhor escopo de função e IIFE(Immediatly Invoked Functions Expression) quando virmos o conceito de closure.
The Keyword "let"
Ciente de simplificar esse tipo de necessidade o ECMAScript6 propõe a criação e uso da palavra reservada "let", conforme o exemplo abaixo:
function letTest() {
let x = 31;
if (true) {
let x = 71; // different variable
console.log(x); // 71
}
console.log(x); // 31
}
Assim, quando todos os browsers implementarem o "let", você poderá usá-lo sem problemas. Por enquanto, porém, ainda está sofrendo com bugs e não é suportado por todos os browsers. Entretanto, você pode testar essa e outras implementações da ECMA6 aqui.
Escopo e Loops
Próximo exemplo interessante é de escopo dentro de loops. Esse talvez seja o equívoco mais clássico entre programadores iniciantes em Javascript. Quem nunca se enganou da maneira abaixo?
O que acontece é simples, mas "tricky". Note que a variável "j" é declarada dentro do escopo da função externa e usada dentro da função interna sem uma nova declaração.
Como não existe uma nova declaração da variável "j" dentro da função interna, "j" continua recebendo o valor da variável i a qual, por sua vez, alterna de valor dentro do loop.
O resultado prático é que apesar de no momento da criação do primeiro evento click, "j" ser igual a 1, quando acontece o segundo loop, "j" se torna 2 e o evento do primeiro botão que guarda a referência do "j" da função externa também muda de valor. E, assim, acontece sucessivamente até chegar no último valor que no nosso caso é 5 para a variável "j" e 6 para a variável "i".
- Exercício: Experimente trocar a variável "j" por "i" na função interna para ver o que acontece.
Qual seria a solução para esse problema então? Uma das alternativas, eu deixo abaixo:
Note que a função "setClickEvent" é invocada somente uma vez para cada valor de i. Assim, o "valor" existente dentro da função interna é único e igual ao valor de i na ocasião da chamada da função setClickEvent.
- Exercício 2: Experimente o trecho abaixo em seu console e verifique o que acontece.
for(var i=0;i<5;i++) {
for(var i=0;i<5;i++) {
console.log(i);
}
}
Chamada de função
Entenda o mecanismo de chamada de uma função:
var x = 1;
//x assume valor global dentro de teste1
function teste1() {x = 2;};
teste1();
console.log(x); //retorna 2
var x = 1;
//existe uma declaração de x implícita dentro de teste2
function teste2(x) {x = 2;};
teste2(x);
console.log(x); //retorna 1
var x = 1;
//Equivale ao teste2, mas de maneira explícita
// para melhor entendimento do mecanismo implícito
// que ocorre ao invocar uma função com parâmetros.
function teste3(x) {
var x = x;
x = 2;
};
teste3(x);
console.log(x); //retorna 1
Hoisting
Hoisting é uma palavra em inglês que significa "elevar" ou "içar". O termo é usado em Javascript, pois quando você declara uma variável usando a keyword var dentro de uma função, não importa onde você o faça, essa declaração vai "elevar-se" até o topo da função que a contém. É, por isso, que um dos padrões recomendados é que você declare logo no início da função todas as variáveis locais que usará.
Obs: Se você achar essa prática estranha, porque descontextualiza sua declaração de variáveis é porque provavelmente sua função está muito extensa e precisa ser refatorada.
Vejamos o exemplo abaixo:
function hoistingExample() {
var x = 10;
function hoisting() {
x = 4;
if(true){
var x = 3;
}
console.log(x); //3
}
hoisting();
console.log(x); //10
}
function hoistingExplained() {
var x;
x = 10;
function hoisting() {
var x;
x = 4;
if(true){
x = 3;
}
console.log(x); //3
}
hoisting();
console.log(x); //10
}
function noHoisting() {
var x;
x = 10;
function hoisting() {
x = 4; //external variable
console.log(x); //4
}
hoisting();
console.log(x); //4
}
hoistingExample();
hoistingExplained();
noHoisting();
O primeiro exemplo mostra um exemplo de hoisting atuando. Se ele não ocorresse, você poderia esperar que a segunda chamada de console.log(x) fosse 4, mas o resultado é 10.
O segundo exemplo é a réplica do primeiro, mas com o mecanismo do hoisting evidenciado. Deixando claro o resultado do primeiro exemplo e explicando porque não existe escopo de bloco em Javascript.
O terceiro exemplo mostra que na ausência do uso de "var" dentro da função hoisting, a variável x continua com a referência externa. Assim, dentro da função hoisting nenhuma "elevação" está de fato ocorrendo.
Próximos Passos
Infelizmente, por Javascript ser uma linguagem com conceitos muito interdependentes, nem sempre é tão fácil separar os temas. Assim, muito do explicado aqui envolveu mais de um conceito, por exemplo, closures. Portanto, se você não entendeu tudo que foi exposto, continue acompanhando as próximas postagens que tudo ficará mais claro e não parecerá apenas "mágica".