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!