sexta-feira, 27 de janeiro de 2017

Escopo e Hoisting no Javascript



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!