Contexto Léxico
Em termos mais simplistas, o escopo léxico define como os nomes de variáveis são resolvidos dentro de funções aninhadas.
Que nada mais são do que funções internas que contêm o escopo das funções pai, independentemente se a função pai tenha algum tipo de retorno.
A melhor forma de se entender o contexto léxico é fazendo isso na prática.
Vamos supor que no início de um código/arquivo em Javascript, você tenha declarado uma variável global chamada valor:
const valor = 'Global';
Toda e qualquer função ou estrutura de bloco que vier após a variável acima, conseguirá acessa-la, isso é fato e já vimos esse comportamento antes.
const valor = 'Global';
function minhaFuncao(){
console.log(valor);
}
minhaFuncao();//'Global'
Apesar do console.log estar chamando a variável valor dentro de um outro contexto, o Javascript consegue acessa-la, pois a variável é global.
Agora observe o exemplo abaixo:
const valor = 'Global';
function minhaFuncao(){
console.log(valor);
}
function minhaFuncaoDois(){
const valor = 'Local';
minhaFuncao();
}
minhaFuncaoDois();//'Global'
Apesar de estarmos redeclarando a variável valor dentro da minhaFuncaoDois(), quando chamamos a minhaFuncao(), por conta do contexto onde a função minhaFuncao() foi declarada, ela irá selecionar o 'Global'.
Isso porque quando declaramos uma função em Javascript, ela tem consciência do local onde foi definida, por isso que ela busca pela variável 'Global' em vez da 'Local'.
Isso é contexto léxico!
É importante ressaltar que existe a consciência do local onde ela foi definida, e do local onde ela foi executada, se o contexto léxico do JS considerasse o contexto do local onde ela foi executada, aí sim, o resultado seria 'Local' em vez de 'Global'.
Closures
Entrando agora em termos mais técnicos dentro do universo das funções, você precisará entender o que é uma CLOSURE.
Uma closure nada mais é do que o escopo criado quando uma função é declarada.
Esse escopo permite que você acesse as variáveis que foram declaradas internamente dentro da própria função, além das variáveis que foram declaradas fora daquela função.
Pense na closure como uma interseção, ou seja, algo que envolve, que se conecta, que compartilha coisas entre si.
Pense na closure como uma combinação de uma função com o ambiente léxico dentro da mesma função que o ambiente léxico foi declarado.
Para entendermos seu contexto na prática, vamos iniciar criando uma constante global:
const a = 'Global';
Em seguida vamos criar uma nova função chamada de funcaoFora(), que vai declarar a mesma constante com um valor diferente.
const a = 'Global';
function funcaoFora(){
const a = 'Local';
console.log(a);
}
funcaoFora();//'Local'
Perceba que o console.log() existente dentro da função, retornou o valor 'Local', pois como estamos dentro de um escopo diferente, o JS aceitou a reescrita daquela variável.
Caso não tivéssemos feito isso, o console.log iria retornar a constante declarada fora da função, vejamos:
const a = 'Global';
function funcaoFora(){
console.log(a);
}
funcaoFora();//'Global'
E se tivermos uma funcaoDentro() dentro da funcaoFora() que retorna a constante a? Será que o JS vai retornar essa constante com o valor 'Global' ou será que ele vai retornar o 'Local'?
const a = 'Global';
function funcaoFora(){
const a = 'Local';
function funcaoDentro(){
return a;
}
return funcaoDentro();
}
console.log(funcaoFora());//'Local'
Obviamente que retornou a 'Local'.
No caso das funções em JS, elas são um closure, pois elas possuem memória do local onde foram definidas.
Uso prático das closures
Ela ajuda durante a associação de alguns dados a uma função que opera nesses dados.
Além disso, ela permite que os objetos realizem a associação de algumas propriedades de objetos a um ou mais métodos diferentes.
Ela também é muito útil quando você cria soluções baseadas em eventos, onde você define algum comportamento, e o anexa ao evento que é acionado pelo usuário.
Onde esse comportamento do código, será anexado como um callback que é executado em resposta ao evento.
Para que possamos entender melhor, vamos recapitular os 3 tipos de escopos principais:
Escopo Global: A variável declarada como global é a mesma variável que está disponível para acesso em todo o seu código.
Escopo da Função: A variável declarada dentro de uma função, só pode ser acessada dentro da própria função.
Escopo do bloco: A variável declarada dentro de um bloco, só pode ser acessada dentro desse bloco.
Funções Factory
Um outro padrão de escrita de funções comumente usado pela comunidade do Javascript, são as funções do tipo factory.
Factory significa "Fábrica", e representa na verdade um padrão de projeto muito difundido em outras linguagens de programação.
Factory Functions nada mais é do que uma função cuja responsabilidade é retornar um objeto (uma nova instancia de um objeto).
Durante o desenvolvimento, você pode pensar em criar diversos objetos que possuem chaves repetidas, como por exemplo:
const pessoaUm = {
nome: 'Micilini',
idade: 26
}
const pessoaDois = {
nome: 'Gabriel',
idade: 29
}
const pessoaUm = {
nome: 'Maycon',
idade: 76
}
Se você fosse usar o conceito de factory, você criaria uma função de modo a retornar um objeto do tipo pessoa contendo todas as chaves que você precisa passando os valores por parâmetros.
function criarPessoa(nome, idade){
return {
nome,
idade,
humano: true
}
}
console.log(criarPessoa('Micilini', 28));//{ humano: true, idade: 28, nome: "Micilini" }
console.log(criarPessoa('João Vitor', 20));//{ humano: true, idade: 20, nome: "João Vitor" }
Veja que não precisamos fazer a seguinte definição nome: nome dentro do objeto que esta sendo retornado dentro do Javascript, pois isso é feito automaticamente por ele.
Mas nada impede que você faça isso usando a definição nome: nome, uma vez que esse modo de escrita ainda funcionará perfeitamente.
function criarPessoa(nome, idade){
return {
nome: nome,
idade: idade,
humano: true
}
}
console.log(criarPessoa('Micilini', 28));//{ humano: true, idade: 28, nome: "Micilini" }
console.log(criarPessoa('João Vitor', 20));//{ humano: true, idade: 20, nome: "João Vitor" }
Observação: Você também pode ter uma factory que retorna funções ou classes.
IIFE
Ainda dentro do universo das funções do Javascript, nós temos um outro conceito conhecido como Funções Auto Invocadas (IIFE).
IIFE é a sigla para Immediately invoked function expression.
Até então nós estamos trabalhando com funções nas quais precisávamos chamar manualmente da seguinte forma:
function minhaFuncao(){
console.log('Olá Mundo!');
}
minhaFuncao();//'Olá Mundo'
Agora nós iremos aprender sobre as funções que são executadas de maneira automática, isto é, sempre quando JS se depara com elas durante a execução do código.
A sua sintaxe é muito similar as funções anônimas:
(function(){
console.log('Estou dentro da função IIFE');
})();//O último parêntesis (antes do ponto e vírgula) diz para o JS executar imeditamente essa função
A ideia principal das funções do tipo IIFE, é quando você deseja fugir do escopo global do projeto.
Nesse caso tudo o que criar ou definir dentro dessas funções, será considerado como escopo local daquela função.
Isso evita que seus elementos sejam manipulados diretamente no escopo global do navegador, e isso é bom, pois se algo é compartilhado com toda a sua aplicação (principalmente se tratando de algo "variável"), isso evitaria que seu código apresente bugs.
E quando me refiro a bugs, estou me referindo a resultados de variáveis ou de funções que estão retornando valores errados, e isso acontece porque em algum ponto do projeto, essas variáveis acabaram sendo redefinidas ou tendo seus valores alterados.
Vejamos mais exemplos do IIFE em ação:
(function () {
var nome = "Micilini";
})();
// A variável nome não é acessível fora do escopo da expressão
console.log(nome); // gerará o erro "Uncaught ReferenceError: nome is not defined"
Já quando atribuímos uma função do tipo IIFE dentro de uma variável, o retorno será um valor em vez de uma função:
var resultado = (function () {
var nome = "Micilini";
return nome;
})();
// Imediatamente gera a saída:
console.log(resultado); // "Micilini"
Construtores de uma Função
No Javascript, existe um método chamado construtor, cujo objetivo é usado para criar objetos dentro de uma função sempre que ela for chamada.
Em outras linguagens de programação, e até mesmo dentro das classes do próprio JS, vemos que a linguagem dispõe de um método construtor.
Cuja missão é receber qualquer tipo de parâmetro que esteja sendo enviado para uma classe, e salvar em seus respectivos atributos.
No mundo das funções, esse tipo de coisa também é possível, só que para isso, precisamos entender dois conceitos diferentes, o conceito dos comandos new e this.
O Operador New
Para que possamos usar os métodos construtores presentes nas funções do JS, precisamos em primeiro lugar, executar a função em conjunto com o operador new.
const pessoa = new funcaoPessoa();
O operador new permite que os desenvolvedores criem uma instância de um tipo de objeto definido pelo usuário, ou de um dos tipos de objeto integrados que possuem uma função de construtor.
Criar uma instancia é como ligar o motor de um carro, ou seja, o ato de girar a chave para mantê-lo funcionando.
Quando "ligamos o motor de uma função" usando o operador new, o JS possibilita enviar parâmetros para dentro dessa função, para que estes sejam armazenados em variáveis que existirão dentro do escopo daquela função.
Só que para fazer esse armazenamento é necessário usar o operador this.
O Operador This
O operador this é usado para selecionar métodos, variáveis, funções, chaves que existem dentro de um escopo, como por exemplo:
function Pessoa () {
this.nome = 'Rafael';
this.idade = 33;
}
const pessoa = new Pessoa();
Observe que no código acima nós estamos instanciando uma função usando o operador new, ao mesmo tempo que dentro do escopo daquela função, estamos utilizando o comando this para fazer referência a duas variáveis lá existentes, que são nome e idade.
Mas espere aí!!! Essas variáveis não existem dentro da função, então como é que o Javascript consegue fazer referência a elas?
Simples, quando isso acontece, o JS se encarrega de criar por de baixo dos panos essas variáveis, de modo que você como desenvolvedor não precise se preocupar muito com isso.
Tanto que conseguimos seleciona-las normalmente mais tarde:
function Pessoa () {
this.nome = 'Rafael';
this.idade = 33;
}
const pessoa = new Pessoa();
console.log(pessoa.nome);//Rafael
console.log(pessoa.idade);//33
Método Construtor com Parâmetros
Nos exemplos anteriores nós vimos como utilizar o método construtor de uma função sem enviar parâmetros, agora vamos ver como ele se comporta quando enviamos alguns parâmetros:
function Pessoa (nome, idade) {
this.nome = nome;
this.idade = idade;
}
const pessoaUm = new Pessoa('Micilini', 23);
console.log(pessoaUm.nome);//Micilini
console.log(pessoaUm.idade);//33
O processo é simples, basta apenas que você envie os parâmetros para dentro da função normalmente, e em seguida usar o comando this junto ao nome da variável para fazer a associação dos parâmetros recebidos.
Existe a possibilidade de criar uma função dentro dessa função de modo que ela tenha acesso as variáveis que foram criadas de maneira automática?
Sim, mas para isso precisamos criar uma variável que contenha uma função anônima, da seguinte forma:
function Pessoa (nome, idade) {
this.nome = nome,
this.idade = idade,
this.mostraMensagem = function () {
console.log('Nome: ' + this.nome + ', Idade: ' + this.idade);
}
}
const pessoaUm = new Pessoa('Micilini', 23);
pessoaUm.mostraMensagem();//'Nome: Micilini, Idade: 23'
Call & Apply & Bind
Existem três formas de se chamar uma função no Javascript, e como as funções são considerados objetos, é fácil de entender que ela possui alguns métodos disponíveis de forma padrão que podemos utilizar.
Call()
O primeiro método que é usado para chamar uma função é o método call().
O método call() é usado para chamar uma função passando argumentos para dentro dessa função, de modo que tais argumentos possam ser acessados pelo comando this de dentro da função.
Call significa chamar, e é um método usado para chamar uma função de modo a passar parâmetros que podem ser acessados pelo comando this.
function mostraNome(){
console.log('Meu nome é: ' + this.nome);
}
let pessoa = {
nome: 'Micilini'
}
mostraNome.call(pessoa);
Quanto mais parâmetros existirem dentro do objeto pessoa, mais argumentos podemos chamar dentro da função por meio do comando this.
Observe outro exemplo, onde a função conta com argumentos comuns:
function mostraNome(idade){
console.log('Meu nome é: ' + this.nome + ', Idade é: ' + idade);
}
let pessoa = {
nome: 'Micilini'
}
mostraNome.call(pessoa, 25);
Veja que no comando acima, além de eu passar o objeto para dentro daquele contexto, estou enviando o argumento idade de forma padrão também.
No caso do comando acima, sempre passamos o contexto em primeiro lugar (que foi o objeto pessoa) seguido dos parâmetros da função (idade).
Isso significa que podemos chamar qualquer função, especificando explicitamente a referência que deve ser referenciada na função de chamada.
Observe um exemplo do comando call(), onde ele se comporta de forma bem parecida (se não similar) com o conceito de heranças que vimos em classes:
function Produto(nome, preco){
this.nome = nome;
this.preco = preco;
}
function Brinquedo(nome, preco){
Produto.call(this, nome, preco);
this.categoria = 'brinquedos';
}
let brinquedo = new Brinquedo('Boneco do Batman', 99.90);
console.log(brinquedo.nome);//"Boneco do Batman"
console.log(brinquedo.preco);//99.9
console.log(brinquedo.categoria);//"brinquedos"
Para que você possa entender o que o método call() acabou de fazer, vamos destrinchar o código passo a passo.
Primeiro, nós criamos uma instancia da função Brinquedo, passando dois parâmetros:
- nome (Boneco do Batman)
- preco (99.90)
Fazendo com que função Brinquedo receba dois parâmetros, e os salve em suas respectivas variáveis que são nome e preco.
Em segundo lugar, estamos fazendo referência a função Produto por meio do método call(), que por sua vez, instancia a função Produto enviando os parâmetros que recebemos na função Brinquedo.
De modo que esses parâmetros passam a existir dentro do contexto da função Produto, e sejam trazidos de volta para a função Brinquedo, como se agora essas duas variáveis (nome e idade) existissem na função Brinquedo.
Ou seja, é como se a função Brinquedo tivesse herdado as variáveis da função Produto.
No final das contas é como se tivéssemos criado tudo isso dessa forma:
function Brinquedo(nome, preco){
this.nome = nome;
this.preco = preco;
this.categoria = 'brinquedos';
}
let brinquedo = new Brinquedo('Boneco do Batman', 99.90);
console.log(brinquedo.nome);//"Boneco do Batman"
console.log(brinquedo.preco);//99.9
console.log(brinquedo.categoria);//"brinquedos"
Vejamos agora outro exemplo onde injetamos um objeto para dentro de uma função existente dentro de outro objeto:
const pessoa = {
mostraNome: function(idade, level) {
console.log('Primeiro Nome: ' + this.nome);
console.log('Segundo Nome: ' + this.sobrenome);
console.log('Idade: ' + idade);
console.log('level: ' + level);
}
}
const pessoaUm = {
nome: "Micilini",
sobrenome: "Roll"
}
pessoa.mostraNome.call(pessoaUm, 26, 10);
Observe que no código acima estamos injetando um objeto dentro de outro por meio do comando call, de modo que dentro da função mostrarNome ele consiga ter acesso aos chaves existentes em pessoaUm por meio do comando this.
Apply()
O segundo método bastante utilizado para chamar uma função, é o método apply().
A diferença é que devemos passar os argumentos da função em forma de array em vez de elementos soltos como vínhamos fazendo no método call().
function mostraNome(idade){
console.log('Meu nome é: ' + this.nome + ', Idade é: ' + idade);
}
let pessoa = {
nome: 'Micilini'
}
mostraNome.apply(pessoa, [25]);
Observe que mudamos somente o método para apply e passamos a idade em formato de array.
Veja um exemplo do método apply sendo executado em conjunto com o método push do array:
let meuArray = ['a', 'b', 'c'];
let elementos = [1, 2, 3, 4, 5];
meuArray.push.apply(meuArray, elementos);
console.log(meuArray);//["a", "b", "c", 1, 2, 3, 4, 5]
Veja agora um outro exemplo onde o método apply está sendo executado em conjunto com o método Max do Math:
let numeros = [23, 77, 987, 432];
console.log(Math.max.apply(null, numeros));//Precisamos passar null pois o método 'max' não precisa de contexto.
//O resultado será: 987
Bind()
O método bind() retorna a copia de uma função, onde dentro dessa nova função existem argumentos que podem ser acessados pelo comando this.
function mostraNome(){
console.log('Nome: ' + this.nome);
}
let pessoa = {
nome: 'Micilini'
}
let funcaoNova = mostraNome.bind(pessoa);
funcaoNova();//'Nome: Micilini'
Diferente dos comandos call() e apply() que aplicam um contexto para dentro de uma função, o bind() ele cria uma nova função com esse novo contexto e a retorna, para que mais tarde possamos chamá-la.
De acordo com as especificações do ECMAScript 5, a função retornada por bind é um tipo especial de objeto de função exótica chamado de função Bound (BF).
Funções Prototype
As funções do tipo prototype é um artifício muito interessante que auxiliam os desenvolvedores, a criar mais interatividade em seus códigos.
Essas funções, nada mais são do que notações que são aplicadas a objetos/estruturas definidas pelo Javascript, com o objetivo de atribuir métodos de execução a esses objetos.
Esses novos objetos podem ser criados por meio de notações do tipo JSON (Java Script Object Notation).
Sendo assim, o Prototype é considerado um método ou um construtor de classes, feito para executar códigos ou até mesmo criar novos objetos.
Onde a partir dele podemos atribuir funções e métodos o classes/objetos já existentes a partir de um outro objeto.
Por exemplo, em conteúdos anteriores, nós vimos diversos tipos de classes e objetos que a linguagem JS nos oferece por padrão.
Como é o caso dos métodos presentes no array, onde temos diversas possibilidades de se trabalhar com índices.
Seja utilizando a propriedade length, ou usando quaisquer um dos métodos como at(), every(), fill(), find()...
Mas e se eu te contasse, que é possível criar um novo método para ser usado em conjunto com os arrays?
Sim, e isso é possível de ser feito usando as funções prototype!
Trabalhando com Prototype
Observe o comando abaixo, e note que o prototype adiciona uma nova função dentro da função Pessoa, que retorna uma mensagem:
function Pessoa(){
this.nome = "Micilini";
this.site = "https://micilini.com";
}
Pessoa.prototype.RetornarMensagem = function(){
return 'Nome: ' + this.nome + ', Site: ' + this.site;
}
const pessoaF = new Pessoa();
const mensagem = pessoaF.RetornarMensagem();//Uma nova função chamada 'RetornarMensagem' foi criada na função Pessoa()
console.log(mensagem);//"Nome: Micilini, Site: https://micilini.com"
Veja que no código acima, foi adicionado uma nova função dentro de Pessoa(), que retorna uma mensagem e faz uso direto das propriedades daquela função.
No próximo código, veja que também é possível adicionar novas propriedades a partir do prototype:
Pessoa.prototype.idade = 28;
Pessoa.prototype.secure = true;
Como dito anteriormente, podemos usar o prototype para modificar métodos do próprio JS, como é o caso de criar um novo método específico para os arrays:
Array.prototype.mostrarConsole = function(){
console.log(this);//Vai mostrar
}
var frutas = ['Maçã', 'Banana'];
frutas.mostrarConsole();//['Maçã', 'Banana']
No caso do comando acima, criamos um novo método chamado mostrarConsole que funciona em conjunto com o Array que criamos.
Apesar dessa estratégia ser bem legal, a própria documentação do JS nos diz: Apenas modifique seus próprios protótipos. Nunca modifique os protótipos de objetos padrão do próprio JavaScript.
Portanto, atenção redobrada quando for fazer o uso do prototype em objetos padrão da linguagem.
Conclusão
Neste conteúdo aprendemos assuntos avançados relacionados a funções em JS.
Aprendemos bastante coisa sobre contextos, operadores new, comando this, uso de objetos e criação de contextos por meio do apply, call e bind.
Te aguardo no próximo conteúdo 😄