Alguns programadores que começam em Javascript sempre acabam passando por problemas para entender Hoisting e Escopo. Afinal, WTF, man?
Vejamos o código abaixo:
var name = "Marquinho"; (function () { console.log(typeof family); function console.log(typeof neighborhood); // undefined family(); neighborhood(); function family() { console.log(name); // Marquinho function brother() { var name = "Marcus Vinicius"; console.log(name); return name; } function mother() { console.log(name); // undefined if (!name) { var name = "Nair Madalena"; console.log(name); // Nair Madalena } return name; } function brother(name) { console.log(name); // Marquinho var pharse = "O apelido dele é: "; console.log(pharse + name + ', o nome da mãe é ' + mother()); // O apelido dele é: Marquinho, o nome da mãe é: Nair Madalena } brother(name); } var neighborhood = function() { var placeNear = "Supermercado Sonda"; console.log("Em minha vizinhança perto tem um " + placeNear); } })();
Nele, declaramos name como uma variável global com o valor “Marquinho” e temos a chamada da função family() e neighborhood(). A diferença é que a função familytem como escopo IIFE e funciona mesmo sendo definida depois. Já neighborhood não é hoisted, apenas sua declaração.
Em JavaScript, todas as declarações de funções e variáveis são movidas para o topo de cada escopo, e isto chamamos de hoisting. Em tempo de execução, ao declarar sua variável e função, a mesma é sempre elevada ao topo.
Porém, somente sua declaração é hoisted; a sua inicialização não. Funções anônimas ou expressão de função também são hoisted e acontece a mesma coisa, por isso a função neighborhood, ao ser invocada na linha 4, nos retorna undefined.
Vamos entender como nosso código é interpretado:
var name; name = "Marquinho"; (function () { console.log(typeof family); // function console.log(typeof neighborhood); // undefined family(); neighborhood(); function family() { console.log(name); // Marquinho function brother() { var name; name = "Marcus Vinicius"; console.log(name); return name; } function mother() { console.log(name); // undefined var name; // undefined if (!name) { name = "Nair Madalena"; console.log(name); // Nair Madalena } return name; } function brother(name) { console.log(name); // Marquinho var pharse pharse = "O apelido dele é: "; console.log(pharse + name + ', o nome da mãe é ' + mother()); // O apelido dele é: Marquinho, o nome da mãe é: Nair Madalena } brother(name); } var neighborhood neighborhood = function() { var placeNear; placeNear = "Supermercado Sonda"; console.log("Em minha vizinhança perto tem um " + placeNear); } })();
Declaração e inicialização?
É muito importante entender a diferença entre declarar e inicializar uma variável em JavaScript.
Declaração:
// declara variável 'name' var name;
Inicialização:
// inicializado com o valor 'Celso' name = 'Celso' ;
No ES5, temos dois escopos para as variáveis: escopo de função e global.
O que é Escopo?
Em ES5, as funções são nosso delimitador de escopo de fato para declaração de variáveis. Isso significa que blocos usuais de loops e estruturas condicionais (como if,for, while, switch e try) NÃO delimitam escopos, diferente de muitas outras linguagens como C, C++, Java e ES2015.
Portanto, esses blocos compartilham do mesmo escopo que a função que os contém. Dessa forma, pode ser perigoso declarar variáveis dentro de blocos, já que vai parecer que a variável pertence apenas ao bloco. Esse é um problema comum para programadores vindos de linguagens com escopos em blocos.
Como entrar em um escopo?
Temos quatro maneiras:
- Definido pela linguagem: todo escopo possui o this e, caso seja uma função, também o arguments;
- Declaração de uma variável: variáveis declaradas como var name;
- Declaração de uma função: funções declaradas na forma function family() {};
- Parâmetros de uma função: caso uma função seja chamada na forma brother (name), podendo ser brother (name, mother, …), sendo name e mother entram no escopo da função.
Como poderia ficar nosso código:
var val = 1; scopeBloc(); function scopeBloc() { for(var i = 0; i < 10; i++) { var newVal = val + i; console.log(newVal); // 10 } console.log(newVal); // 10 console.log(val); // 1 }
No código acima ainda podemos acessar a varivel newVal fora do bloco do loop, bem como podemos criar um escopo dentro do loop em ES5 utilizando ‘IIFE’ (sobre esse assunto, recomento a leitura deste post, feito por meu amigo Pedro Araujo). Essa era nossa única maneira de se criar um escopo dentro de um bloco, mas com a vinda do ES2015 isto mudou.
var val = 1; scopeBloc(); function scopeBloc() { for(var i = 0; i < 10; i++) { (function() { var newVal = val + i; console.log(newVal); // 10 }()); } console.log(newVal); // ReferenceError: newVal is not defined console.log(val); // 1 }
Vamos ver se ficou claro:
var val = 1; // escopo global var text = "Estudando Escopo"; // escopo global function scopeBloc() { for(var i = 0; i < 10; i++) { // i escopo local (function() { var newVal = val + i; // newVal escopo do bloco if console.log(newVal); // vai fazer o loop de 1 a 10 console.log(text); // var fazer o loop com a string Estudando Escopo }()); } var getPhrase = function() { var val = 2; // ecopo de bloco Pharse = "I love JavaScript"; // escopo global (var não foi utilizada) console.log(val); // 2 console.log(i); // 10 console.log(text); // Estudando Escopo console.log(Pharse); // I love JavaScript }(); console.log(newVal); // ReferenceError: newVal is not defined console.log(val); // 1 } scopeBloc(); console.log(val); // 1 console.log(text); // Estudando Escopo console.log(Pharse); // I love JavaScript
E o que é this?
Aqui está uma das partes mais confusas do JavaScript. Como o this funciona? O que come? Onde vive?
Brincadeiras à parte, this é uma referência ao contexto no qual a função está sendo executada, ao objeto ao qual ela pertence. Em grande parte de outras linguagens othis é uma referência ao objeto atual instanciado. Contudo, para dar um nó na sua mente, em JavaScript o this vai depender de como sua função foi inicializada.
Algumas formas de chamar uma função:
- Diretamente
- Como um método
- Explicitamente aplicada
- Como construtor
Diretamente:
Quando uma função é chamada diretamente, this fará referência ao contexto global, que no caso dos navegadores é o objeto window.
var text = 'Escopo em Javascript' function createPhrase(textA, textB) { var textC = 'Quando uma função é chamada diretamente' console.log(this); // imprimi window console.log(textA + textB + ' - ' + textC); // Entendendo como funciona Escopo em Javascript - Quando uma função é chamada diretamente } createPhrase('Entendendo como funciona ', text);
Como um método:
São funções armazenadas dentro das propriedades de um objeto. Se uma função for chamada como um método de um objeto, então this, dentro dessa função, fará referência a esse objeto.
var text = 'Escopo em Javascript' var myObject = { textA: 'Entendendo como funciona ', textB: 'Quando uma função é chamada com um método', createPhrase: function (textC) { console.log(this.textA + textC + ' - ' + this.textB); // Entendendo como funciona Escopo em Javascript - Quando uma função é chamada com um método } }; myObject.createPhrase(text);
Explicitamente aplicada:
Uma função pode ser aplicada explicitamente a qualquer objeto, e para isso utilizamos os métodos call ou apply. Saiba mais neste artigo aqui.
Os métodos call e apply nos permitem controlar o valor de this e assim definir a qual objeto ele fará referência. A diferença entre eles é a forma que recebem os parâmetros e também a velocidade de execução que pode variar de navegador para navegador.
var text = 'Escopo em Javascript'; var myObject = { phraseC: 'Entendendo como funciona ', phraseD: 'Quando uma função é aplicada explicitamente' }; function createPhrase(phraseA, phraseB) { console.log(this); // imprimi objeto console.log(this.phraseC + phraseA + ' - ' + this.phraseD + ' => ' + phraseB); // Entendendo como funciona Escopo em Javascript - Quando uma função é aplicada explicitamente => entenderam ? } createPhrase.call(myObject, text, 'entenderam ?'); createPhrase.apply(myObject, [text, 'entenderam ?']);
Na função createPhrase faço referência ao objeto ‘myObject’. Com isso, consigo acessar os nós dele: this.phraseC que tem como valor Entendendo como funciona e this.phraseD que tem como valor Quando uma função é aplicada explicitamente.
Como um construtor:
Quando uma função é usada como um construtor (usando a palavra-chave new), thisfará referência ao novo objeto que está sendo construído:
function work() { this.startWork = '8:00'; this.finishWork = '19:00'; this.officialName = officialName(); this.getstartWork = function() { return this.startWork; }; this.getfinishWork = function() { return this.finishWork; }; this.getOfficialName = function() { return this.officialName; }; }; function officialName() { var name = 'Luiz Celso F Alves'; return name; }; var myWork1 = new work(); var myWork2 = new work(); console.log('Entrada: '+myWork1.getstartWork());// Entrada: 8:00 console.log('Saída: '+myWork1.getfinishWork()); // Saída: 19:00 console.log('Nome: '+myWork1.getOfficialName());// Nome: Luiz Celso F Alves myWork2.startWork = '10:00'; console.log('Novo horario de entrada: '+myWork2.startWork);// Novo horario de entrada: 10:00
E por hoje é só!
Ficou alguma dúvida, tem alguma observação a fazer ou algum comentário? Fique à vontade nos campos abaixo. Até a próxima!
***
Artigo publicado originalmente em: http://www.concretesolutions.com.br/2016/09/12/hoisting-e-escopo-es2015/