Você não sabe Javascript e eu posso provar!
Motivação
Depois de ter realizado um curso técnico em eletrônica e ter entrado em contato com diversas tecnologias, percebi que possuia maior afinidade com o software do que com o hardware. Por isso, iniciei meus estudos sobre linguagens de programação em 2019 e me apaixonei pela programação Web e pelo Javascript.
Imagem: Universo Racionalista
Porém, como já mostra o gráfico da confiança pela competência, meu pouco conhecimento sobre a linguagem e a facilidade que o aprendizado de sua lógica me proporcionava para aprender outras me colocou no início do eixo horizontal: eu acreditava saber muito, infelizmente.
O ensino médio e os preparativos para o Enem me afastaram dos estudos sobre o desenvolvimento Web, mas eu ainda produzia alguns pojetos pessoais esporádicos e participava de alguns eventos online quando tinha tempo. Mas, mesmo assim, acreditava já estar preparado para lidar com bibliotecas, frameworks, ambientes e todo o tipo de implementação que o Javscript poderia ter no momento em que pudesse voltar a usá-lo com mais frequência. Isso até eu ganhar um livro de padrões de projeto em JS, acreditando ser capaz de entendê-lo por completo, mas quando tentei lê-lo: BOOM! Eu não estava entendendo nada. Closures, AJAX, Prototype, Delegação, Coerção, "Callback Hell" e uma chuva de outros termos que fizeram minha ficha cair: Eu não sabia Javascript. Eu não estava nem perto de entender. Foi aí que passei a buscar materiais sobre a linguagem para me inteirar sobre as reais capacidades da linguagem.
Por esse motivo atualmente estudo Javascript por meio da série You Don't Know JS, produzida por Kyle Simpson, e gostaria de elencar alguns conceitos interessantes que aprendi durante minha leitura para encorajar as pessoas a estudarem esse material.
Closures
Esse é um dos termos que mais me causou confusão depois de miseravelmente ter tentado ler aquele livo de padrões de projeto em JS. Por sorte, o tempo e o estudo me fizeram compreender melhor seu significado.
Para entender closures é importante entender como o Javascript organiza a memória alocada para o programa: Garbage Collection. Basicamente essa é uma maneira de liberar espaço da memória do programa quando o conteúdo nela não está mais sendo utilizado. Então, imagine uma variável x que pertence ao escopo de uma função foo da seguinte maneira:
function foo(){
var x = 2;
return x * 2
}
console.log(foo()) // 4
O que acontece com x depois do término da execução da função foo? O Garbage Collection desaloca o espaço reservado para ela da memória para liberar espaço de armazenamento, pois ela não será mais utilizada.
Mas o que acontece se eu fizer isso:
function soma(x){
function adicionar(y){
return x + y
}
return adicionar
}
var adicionaUm = soma(1);
var res = adicionaUm(2)
console.log(res) // ??
Minha mente depois de ter entendido o Garbage Collection me fez pensar que esse código resultaria em algum erro, pois a varável x, que é retornada pela função soma deveria ser desalocada e a busca pelo seu valor em x + y dentro da função adiciona atribuida a adicionaUm falharia, mas o resultado me surpreendeu:
function soma(x){
function adicionar(y){
return x + y
}
return adicionar
}
var adicionaUm = soma(1);
var res = adicionaUm(2)
console.log(res) // 3
Oi? O valor de x foi encontrado? Sim, foi. Isso é uma Closure.
Vou explicar melhor: o Garbage Collection, para saber se um escopo específico e suas variáveis estão sendo utilizadas, confere se existe alguma referência apontando para aquele escopo. Nesse caso, a função adiciona possui uma referência para a variável x no escopo da função soma. Por esse motivo, adiciona possui uma closure (referência) para o escopo da função soma, impedindo que aquele contexto seja desalocado pelo Garbage Collection.
Esse conceito pode ser melhor explicado em: Escopos e Closures
Javascript NÃO possui Classes!
"Ah, tá! Se JS não tem Classes, me explica isso aqui:"
class User {
constructor(nome, idade){
this.user_nome = nome
this.user_idade = idade
}
showInfos(){
console.log(`Meu nome é ${this.user_nome} e tenho ${this.user_idade} anos`)
}
}
var person = new User("Tadomicari", 19);
person.showInfos(); // "Meu nome é Tadomicari e tenho 19 anos"
"Claramente está sendo criada uma classe User que é instanciada em um objeto person através do construtor User(). Acho que você nunca estudou Orientação a Objetos."
Aparentemente é isso que está acontecendo. Porém, é apenas uma boa forma de imitar o comportamento de classes. Se você conhece bem a POO deve saber dos seus quatro pilares: abstração, herança, polimorfismo e encapsulamento. Vamos testar uma coisa:
class Carro {
constructor(){
this.rodas = 4;
}
run(){
console.log("Acelerar")
}
}
console.log(typeof Carro) // "function"
Se Carro é uma classe, por qual motivo seu tipo é dado como function? O pior disso é descobrir que, em JS, funções não são tipos primitivos, mas sim derivações do tipo primitivo object, ou seja, essa "classe" é na realidade um objeto!!
Por "baixo dos panos" o Javascript realiza uma série de procedimentos para que o uso da palavra reservada class, chamada sugar syntax, imite os comportamentos de uma classe de verdade, como a abstração e a herança. Porém, é possível demonstrar que não é bem isso que está acontecendo:
function Carro(){
console.log("Acelerar")
}
var fusca = new Carro();
typeof fusca; // "object"
Carro.prototype.isPrototypeOf(fusca) // true
Aqui temos muitas coisas para notar:
- Carro é declarado explicitamente como uma função;
- A variável fusca recebe a função Carro como se fosse um construtor. Como assim? O que acontece aqui é o seguinte: em JS não existem "funções construtoras" que precisam ser chamadas com a palavra reservada new. Na verdade, essas "funções construtoras" são apenas funções normais. O trabalho de new é apenas produzir um novo objeto a partir da chamada daquela função e retorná-lo como resultado da expressão. É por isso que o resultado de typeof fusca é "object";
- Toda função, quando declarada, é automaticamente linkada a um objeto com o nome da função seguido por .prototype. Objetos criados a partir dessa função serão ligados a esse objeto .prototype através da cadeia de protótpos do JS. Por esse motivo, o resultado de Carro.prototype.isPrototypeOf(fusca) é true (verdadeiro), pois fusca é um objeto que faz parte da cadeia de protótipos derivada de Carro;
A cadeia de protótipos inclusive torna o mecanismo de "herança" do Javascript, algo mais próximo do que Kyle chama de "delegação de comportamento" do que da "herança" propriamente dita da POO.
Isso pode ser melhor explicado em: Confundindo objetos com classes
this: O que é isso?
A palavra reservada this está presente em muitas linguagens de programação e, por esse motivo, gera grandes confusões dentro do Javascript por apresentar comportamentos um pouco diferentes do esperado. Veja isso:
var obj = {
a: 1,
show(){
console.log(this.a)
}
}
obj.show(); // 1
Certo. Tivemos um comportamento próximo do esperado: this se referiu ao objeto dentro do qual ele foi declarado e buscou pela propriedade a presente.
Se eu fizer isso em uma função, sabendo que funções são derivações do tipo object, é de se esperar que o this de uma função vai se referir para dentro de seu escopo. Certo?
function testThis(){
var a = 2;
console.log(this.a);
}
var a = 1;
testThis(); // 1 (COMO ASSIM?????????)
Afinal de contas: ao que this se refere no Javascript? Ao objeto em que ele está ou ao escopo global? (Guarde essa pergunta)
Se você pesquisar na documentação da MDN vai encontrar que existem contextos diferentes que vão mudar a referência para qual this está apontando. Seriam esses:
-
Chamada simples: nesse caso, uma função é chamada dentro do escopo global. Portanto, o this vai se referir ao objeto global que, no caso dos navegadores, é window. (Como as variáveis globais se tornam propriedades desse objeto, a chamada this.a é o mesmo que window.a e corresponde à variável global a. Por isso o console mostra "1" como resposta da função testThis no segundo exemplo);
-
Chamada simples em modo estrito: se o modo estrito da linguagem for declarado no documento ou no escopo em que this está, ele permanece indefinido caso não seja declarado em contexto de execução:
function testThis(){
"use strict"
var a = 2;
// this é undefined
console.log(this.a);
}
var a = 1;
testThis(); // Uncaught TypeError: Cannot read properties of undefined (reading 'a')
- Arrow Functions: não permite mudanças da referência do this em tempo de execução. Se a função é declarada no escopo global, this sempre vai se referir ao objeto global:
var func = () => {console.log(this)}
var obj = {
testThis: func
}
obj.testThis(); // Window {...}
-
Método de objeto: caso a função seja chamada como método de um objeto (como no primeiro exemplo com obj), this se refere a esse objeto mesmo se sua assinatura/declaração estiver fora dele;
-
Função construtora: caso uma função seja chamada como "função construtora", usando new, um novo objeto é criado e o this da função vai se referir a esse novo objeto;
Esses não são todos os casos possíveis. Recomendo a leitura da documentação.
Porém, tem algo que ocorre aqui e resume todos esses casos em uma explicação. Lembra da pergunta que pedi para você guardar: "Afinal de contas: ao que this se refere no Javascript? Ao objeto em que ele está ou ao escopo global?". Então, a resposta é: a nenhum deles! Isso porque o this, por padrão, é definido em tempo de execução. Portanto, a referência de this é mudada enquanto o programa é executado (exceto em alguns casos, como a documentação da MDN mostrou).
Mas o que exatamente dentro da execução define a referência de this?
Quando uma função é chamada, ela é adicionada a uma pilha conhecida como "call-stack" (pilha de chamadas). A última função imediatamente antes da função que está em cima da pilha possui um escopo chamado "call-site" e ele pode alterar o contexto ao ao qual this é aplicado, os exatos mesmos contextos que a documentação da MDN nos mostra. Como essa pilha muda em tempo de execução, o valor de this em uma função vai depender de como essa pilha está e por isso é dito que "this é definido em tempo de execução".
Para ilustrar isso, veja:
function ultima(){
// Código
}
funstion primeira(){
ultima() // Call-site de ultima() é a função primeira()
}
primeira();
Nesse código, a função primeira é chamada e adicionada à call-stack. Como essa função precisa do retorno/execução da função ultima para ser terminada, ultima é adicionada acima dela na call-stack. Portanto, primeira é a call-site de ultima.
Porém, o contexto das arrow functions altera esse comportamento do this. Quando declarado dentro de uma delas, this é declarado lexicalmente, o que significa que é definido em tempo de escrita e não será alterado pela execução do programa.
Melhores explicações sobre isso podem ser vistas em this Agora tudo faz sentido!
Conclusão
Esses pontos são apenas uma pequena fração do que está presente no material produzido por Kyle Simpson em You Don't Know JS, por isso, recomendo fortemente que leiam esse material caso queiram se aprofundar nos mecanismos que o Javascript tem para oferecer.
Conhecer essa série me mostrou o como eu ainda tenho muito a aprender e quem sabe tenha me colocado um pouco mais à direita no gráfico da Confiança X Competência. Espero que sintam o mesmo durante seus estudos e peço que apontem qualquer erro presente no meu entendimento sobre esses conceitos.