A forma como o javascript lida com o escopo de variáveis e funções pode ser um tanto quanto confusa para quem vem de outras linguagens imperativas como C, Java ou C++. Enquanto nessas linguagens as variáveis possuem um escopo de bloco, no javascript as variáveis possuem escopo de função. Vamos ver um exemplo em C:
int escopo() { int idade = 12; if(1) { int idade = 20; int ano = 2017; } printf("%d", idade); print("%d", ano); // error: ‘ano’ undeclared (first use in this function) }
Que valor você acha que a variável idade terá no momento de escrever o resultado na tela? Se respondeu 12, acertou. Isso acontece no C porque blocos como if, while e for possuem seu próprio escopo dentro da função. A variável idade dentro do if é diferente da variável idade da função main. Já como a variável ano foi criada apenas dentro do bloco if, ela não existe do lado de "fora", e portanto, esse código causaria um erro.
Agora vejamos como esse mesmo código funcionaria no Javascript:
function escopo() { var idade = 12; if(true) { var idade = 20; var ano = 2017; } console.log(idade); // 20 console.log(ano); // 2017 }
Mas como pode ter funcionado? O que acontece é que no javascript blocos como o if, for e while não possuem escopos com variáveis. Todas as variáveis são sempre colocadas dentro do escopo da função. Quando você declara a variável ano por exemplo, ela será válida na função também, pois é onde fica delimitado seu escopo. Com relação à variável idade, você estaria apenas mudando o seu valor.
Mas, como o javascript faz para a função enxergar uma variável dentro de um bloco? Isso é feito por um recurso da linguagem chamado hoisting. Hoisting em inglês quer dizer içar ou erguer com um guindaste, que tem a ver com o que o Javascript realmente faz.
Não importa o lugar onde declaramos uma função ou variável e nem em qual ordem, o Javascript sempre vai movê-la sorrateiramente através do interpretador pro lugar mais alto do seu escopo. Ou seja, nos dois exemplos abaixo, as duas funções são idênticas para o interpretador do Javascript:
function hoisting() { var idade = 12 console.log(idade) if(idade > 10) { var nome = "Pedro" console.log(nome) var ano = 2017 console.log(ano) } } function hoisting2() { var idade, nome, ano idade = 12 console.log(idade) if(idade > 10) { nome = "Pedro" console.log(nome) ano = 2017 console.log(ano) } }
O que o Javascript fez aqui foi mover todas as declarações de variáveis para o ponto mais alto do escopo. É por isso que podemos utilizar variáveis declaradas dentro de blocos no escopo da função. E o mesmo acontece com funções.
No Javascript podemos declarar funções de duas maneiras:
function imprimir() { console.log("Olá Mundo") } var imprimir = function() { console.log("Olá Mundo") }
No caso de funções declaradas explicitamente, o Javascript irá movê-la inteira para o início do escopo mais próximo. Mas, quando a função é declarada dentro de uma variável, apenas a variável é movida, o que pode causar mais uma confusão. Veja o exemplo:
imprimirAno() // Funciona imprimirNome() // TypeError: imprimirNome is not a function function imprimirAno() { return 2017 } var imprimirNome = function() { return "Maria" }
Aqui, a função imprimirAno foi movida inteira para o início do escopo, enquanto que no caso de imprimirNome, apenas a declaração da variável foi movida pra cima. Enquanto a declaração de todas as variáveis são movidas, suas definições só acontecem no momento que o interpretador chega no local de execução do código. O exemplo acima na verdade foi interpretado Javascript assim:
var imprimirNome function imprimirAno() { return 2017 } imprimirAno() imprimirNome() imprimirNome = function() { return "Maria" }
O que muda com let e const
Desde o ECMAScript 6, o javascript passou a ter duas novas maneiras de declarar variáveis. Vou explicar como cada uma delas altera a forma como o javascript trabalha com escopo e hoisting de variáveis e funções:
Veja esse exemplo:
var x = 1; if(true) { let x = 10 }
Que valor você acha que o x terá no momento de mostrar o resultado na tela? Se respondeu 1, acertou!
O que acontece aqui é que variáveis declaradas com o let terão um escopo de bloco, ou seja, a variável será içada (hoisting) até o início do primeiro bloco que encontrar. A partir de agora então, não será mais preciso utilizar a técnica IIFE para isolar uma variável do resto do código.
var a = 10 let b = 20 if(a < 11) { var a = 12 let b = 50 console.log(a) console.log(b) } console.log(a) console.log(b)
Nesse exemplo, apenas a variável a foi alterada quando saiu do bloco, pois com ela o hoisting foi feito com base no escopo de função, enquanto que o b dentro do if deixa de existir assim que o bloco é finalizado.
A próxima keyword inserida no Javascript é interessante pois com ela conseguimos bloquear uma variável de ser redeclarada com outro valor. Veja um exemplo:
const a = 2 const b = "olá" const c = {x: 1, y: 2} console.log(a) console.log(b) console.log(c) a++ b = b + "mundo" c = {x: 3, y: 4)
Depois que definimos um valor pela primeira vez para uma variável declarada com o const, não poderemos redefinir esse valor novamente. Isso é útil pois impede que outras partes do programa utilize essa variável para armazenar outro valor. Note no entanto que, apesar dela bloquear a redefinição de valores, não impede que uma variável que referencia um objeto, tenha o valor de algum de seus atributos alterados. Veja que o exemplo abaixo é perfeitamente possível:
const c = {x: 1, y: 2} console.log(c) // {x: 1, y: 2} c.x = 2 c.y = 4 console.log(c) // {x: 2, y: 4}
Para fazer com que um objeto seja completamente imutável, é necessário congelá-lo. Isso pode ser feito com Object.freeze():
const c = {x: 1, y: 2} Object.freeze(c) c.x = 100 c.y = 200 console.log(c) // {x: 1, y: 2}
Ainda assim, o Object.freeze() congela apenas o primeiro nível de atributos. Caso exista uma variável que referencie a um objeto, esse objeto não será congelado pelo método. Caso se intesse em utilizar objetos imutáveis, veja a biblioteca Immutable.js. É muito interessante e vale o estudo.
Conhecendo a forma como a linguagem lida com o escopo e o conceito de hoisting, fica muito mais fácil entender problemas que até então pareceriam completamente sem sentido. No entanto, o Javascript também possui um recurso fundamental pra quem deseja se tornar um Jedi: as Closures.
Falarei sobre elas no próximo artigo. Até la!