Javascript Hoisting

Todo desenvolvedor de Javascript deve estar ciente do conceito de elevação, que pode potencialmente produzir efeitos colaterais se for ignorado. Hoisting é sobre como o Javascript lida com o escopo para declarações var.

Basicamente, o içamento se resume a 2 regras:

1) Em Javascript, as funções são nossos delimitadores de escopo “de fato”, o que significa que os blocos usuais de loops e condicionais (como if, for, while, switch and try) NÃO delimitam o escopo, ao contrário da maioria das outras linguagens. Portanto, esses blocos terão o escopo definido na primeira função ancestral que contém esse bloco.

Desta forma, você NÃO DEVE declarar vars dentro de um bloco, mas na própria função.

2) Em tempo de execução, todas as declarações var são movidas para o início de cada função (seu escopo). Dito isso, é uma boa prática declarar todos os vars completos na primeira linha, a fim de evitar falsas expectativas com um var que foi declarado atrasado, mas por acaso tinha um valor antes. Este é um problema comum para programadores vindos de linguagens com escopo de bloco, que geralmente declaram seus vars quando estão prestes a fazer seu primeiro uso.

Por exemplo, experimente a seguinte função:

function stepSum() {
var total = 0;
for (var i = 0; i < arguments.length; i++) {
var parameter = arguments[i];
if (typeof(parameter) !== 'number') {
parameter
= parseInt(parameter);
}
setTimeout
(function() {
if (!isNaN(parameter)) {
total
+= parameter;
console
.log(i + ") adding " + parameter + ", total is now " + total);
}
}, i*1000);
}
return "the stepSum has been triggered...";
}

A função stepSum () acima obterá qualquer número de parâmetros e os somará, convertendo adequadamente cada parâmetro em número ou ignorando-o caso contrário. Existe um requisito onde esta função deve acionar a soma para acontecer passo a passo no console.log, para que o usuário veja os passos da soma a cada segundo assim como a contagem dos passos.

Então, se você tentar:

stepSum(3, 2, 1)

Você obteria como resposta:

  • 3) adicionando 1, o total agora é 1
  • 3) adicionando 1, o total agora é 2
  • 3) adicionando 1, o total agora é 3

Por que isso aconteceu? Observe que parecia somar apenas o último parâmetro por três vezes. E também a contagem de passos está sempre em 3, onde esperaríamos ver 1, 2 e 3. O que há de errado aqui?

A resposta é que devido ao içamento, o parâmetro var foi empurrado para o início da função stepSum , tornando-o disponível para toda a função. Mais do que isso, o parâmetro só está sendo definido uma vez e, em seguida, é reatribuído a cada iteração do loop for.

Dado que estamos usando chamadas setTimeout aqui, podemos esperar agora que quando esta função for executada pela primeira vez (após um segundo), o parâmetro terá agora o valor de sua última atribuição, que é da última iteração do loop for, quando o parâmetro foi definido como 1.

Como podemos arranjá-lo? Apenas fazendo bom uso de içamento, assim:

function stepSum() {
var parameter, printStep,
total
= 0,
i
= 0;

// Printstep function
printStep
= function(parameter, step) {
step
++;
setTimeout
(function() {
if (!isNaN(parameter)) {
total
+= parameter;
console
.log(step + ") adding " + parameter + ", total is now " + total);
}
}, step * 1000);
};

// Iterating
for (i = 0; i < arguments.length; i++) {
parameter
= arguments[i];
if (typeof(parameter) !== 'number') {
parameter
= parseInt(parameter);
}
printStep
(parameter, i);
}
return "the stepSum has been triggered...";
}

Aqui, a chamada setTimeout foi extraída para outra função, printStep , que tem seu próprio escopo. Daqui para frente, o vars parâmetro e eu agora são declarados no início do stepSum função, bem como a declaração de printStep função.

Agora temos:

  • 1) adicionando 3, o total agora é 3
  • 2) adicionando 2, o total agora é 5
  • 3) adicionando 1, o total agora é 6

Para obter mais informações, leia JavaScript Scoping and Hoisting e JSLint – scope