Alguns conceitos de JavaScript como hoisting, closure, data types, asynchronous e outros

⚠️ Atenção! Este artigo foi traduzido do inglês para o português a partir da fonte informada ao final do artigo. A tradução é fornecida apenas para fins informativos e não deve ser considerada como uma tradução oficial. Não me responsabilizo por quaisquer erros ou imprecisões que possam ter sido introduzidos na tradução. É altamente recomendável que você verifique a precisão do conteúdo com a fonte original em inglês.

Se você se candidatar a uma função de front-end ou desenvolvedor Web, poderá ser avaliado por suas habilidades em javascript. Há muitos conceitos de JS, como hoisting, closure, data types, asynchronous e outros. Aqui estão algumas das perguntas mais frequentes sobre Javascript:

1) Mutabilidade

Em Javascript, temos 7 tipos de dados primitivos (string, number, bigint, boolean, undefined, symbole null). Todos eles são imutáveis, o que significa que, uma vez atribuído um valor, não podemos modificá-lo, o que podemos fazer é reatribuí-lo a um valor diferente (ponteiro de memória diferente). Os outros tipos de dados, como Object e Function, por outro lado, são mutáveis, o que significa que podemos modificar o valor no mesmo ponteiro de memória.

//Q1
let text = 'abcde'
text[1] = 'z'
console.log(text) //ans: abcde

O código está tentando modificar uma posição específica de uma string, mas as strings em JavaScript são imutáveis, ou seja, uma vez que uma string é criada, seus valores não podem ser alterados. Portanto, o valor da variável text permanece inalterado.

String é imutável, portanto, uma vez atribuído a um valor, você não pode alterá-lo para um valor diferente. O que você pode fazer é reatribuí-lo. Lembre-se de que alterar o valor e reatribuir a outro valor é diferente.

//Q2
const arr = [1,2,3]
arr.length = 0
console.log(arr) //ans: []

Atribuir arr.lengthcomo 0 é o mesmo que redefinir ou limpar o array, portanto, agora, o array se tornará um array vazio.

//Q3
const arr = [1,2,3,4]
arr[100] = undefined
console.log(arr, arr.length) //ans: [1, 2, 3, 4, empty × 96, undefined] 101

Como um array consome um local de memória contíguo, quando atribuímos o índice 100 a um valor (inclusive undefined), o JS reservará a memória do índice 0 ao índice 100. Isso significa que o tamanho do array agora é 101.

2) Var e Hoisting

//Q4
var variable = 10;
(() => {
	variable2 = 100;
	console.log(variable);
	console.log(variable2);
	variable = 20;
	var variable2 = 50;
	console.log(variable);
})();
console.log(variable);
var variable = 30
console.log(variable2);
//ans:
//10
//100
//20
//20
//ReferenceError: variable2 is not defined

varé uma variável de escopo funcional, enquanto lete constsão variáveis com escopo de bloco. Só var pode ser içada, o que significa que a declaração da variável é sempre movida para o topo. Por causa do hoisting, você pode atribuir, chamar ou usar a variável antes mesmo de declará-la com a palavra-chave var. Porém, let e const não podem ser içadas porque habilita a TDZ (Temporal Dead Zone), ou seja, a variável fica inacessível antes de ser declarada.

No exemplo acima, a variável2 é declarada dentro de uma função com palavra-chave var que faz com que essa variável esteja disponível apenas dentro dos escopos da função. Então, quando qualquer coisa fora da função quiser usar ou chamar essa variável, referenceError é lançada.

//Q5
test() //not error
function test(){
	console.log('test')
}

test2() //error
var test2 = () => console.log('test2')

A declaração de função com palavra-chave function pode ser elevada. No entanto, uma arrow function não pode ser içada, mesmo que seja declarada com var variável.

3) Variável Global Acidental

//Q6
function foo() {
  let a = b = 0;
  a++;
  return a;
}
foo();
typeof b; // => number
typeof a; // => undefined

console.log(a) //error: ReferenceError: a is not defined

var é uma variável com escopo funcional e let é uma variável com escopo de bloco. Embora pareça que a e b são declarados usando let in let a = b = 0, a variável b é declarada como uma variável global e atribuída a um objeto window. Em outras palavras, é semelhante a

function foo() {
  window.b = 0
  let a = b;
  a++;
  return a;
}

A função "foo" declara a variável "a" e atribui o valor 0 a ela, e também atribui o valor 0 para a variável "b" sem declará-la. Dentro da função, a variável "a" é incrementada em 1. Quando a função é chamada, ela retorna o valor 1.

Ao testar o tipo de "b" fora da função, é retornado "number", pois a variável foi atribuída um valor dentro da função.

Ao testar o tipo de "a" fora da função, é retornado "undefined", pois a variável "a" foi declarada dentro da função e não está disponível fora dela.

Ao se tentar imprimir o valor de "a" fora da função daria um erro "ReferenceError: a is not defined", pois a variável "a" não foi declarada fora da função e não está disponível fora dela.

4) Closure

//Q7
const length = 4;
const fns = [];
const fns2 = [];
for (var i = 0; i < length; i++) {
	fns.push(() => console.log(i));
}
for (let i = 0; i < length; i++) {
	fns2.push(() => console.log(i));
}
fns.forEach(fn => fn()); // => 4 4 4 4
fns2.forEach(fn => fn()); // => 0 1 2 3

Closure é a preservação de um ambiente variável, mesmo que a variável já tenha sido alterada ou coletada como lixo. Na pergunta acima, a diferença aqui está na declaração da variável, onde o primeiro loop está usando var e o segundo loop está usando let. var é uma variável com escopo funcional, portanto, quando é declarada dentro de um bloco de loop for, var é considerada uma variável global em vez de uma variável de bloco interno. Por outro lado, let é uma variável com escopo de bloco, semelhante à declaração de variável em outras linguagens, como Java e C++.

Nesse caso, o fechamento (closure) está acontecendo apenas na let variável. Cada uma das funções enviadas para o array fns2 lembra o valor atual da variável, não importando se a variável for alterada no futuro. Ao contrário, fns não lembra o valor atual da variável, usa o valor futuro ou final da variável global.

Este código cria dois arrays, "fns" e "fns2", e preenche cada array com funções que imprimem o valor de "i" no console.

O primeiro loop usa a declaração "var" para criar a variável "i". Isso significa que "i" é uma variável global, e sua referência é compartilhada entre todas as funções no array "fns". Quando o loop é executado, o valor de "i" é incrementado até 4. Então, quando as funções são chamadas, elas imprimem o valor final de "i", que é 4.

O segundo loop usa a declaração "let" para criar a variável "i". Isso significa que "i" é uma variável de escopo de bloco, e cada função no array "fns2" tem sua própria cópia da referência a "i". Quando o loop é executado, cada função é armazenada com o valor de "i" no momento em que foi criada, e quando as funções são chamadas, elas imprimem seus respectivos valores de "i", que são 0, 1, 2, e 3.

5) Objeto

//Q8
var obj1 = {n: 1}
var obj2 = obj1
obj2.n = 2
console.log(obj1) //ans: {n: 2}

//Q9
function foo(obj){
 obj.n = 3
 obj.name = "test"
}
foo(obj2)
console.log(obj1) //ans: {n: 3, name: "test"}

Como sabemos, a variável do objeto contém apenas o ponteiro da localização da memória desse objeto. Então aqui obj2 e obj1 aponta para o mesmo objeto. Isso significa que, se alterarmos qualquer valor em obj2, obj1 também será afetado porque, essencialmente, é o mesmo objeto. Da mesma forma, quando atribuímos um objeto como parâmetro em uma função, o argumento passado contém apenas o ponteiro do objeto. Assim, a função pode modificar o objeto diretamente sem retornar nada. Essa técnica é chamada passada por referência

//Q10
var foo = {n: 1};
var bar = foo;
console.log(foo === bar) //true
foo.x = foo = {n: 2};

console.log(foo) //ans: {n: 2}
console.log(bar) //ans: {n: 1, x: {n: 2}}
console.log(foo === bar) //false

Como a variável de objeto contém apenas o ponteiro da localização de memória desse objeto, quando declaramos var bar = foo, ambos foo e bar apontam para o mesmo objeto.

Na próxima lógica, foo = {n:2} está executando primeiro onde foo é atribuído a um objeto diferente. Portanto, foo tem um ponteiro para um objeto diferente. Ao mesmo tempo, foo.x = foo está em execução, foo aqui ainda contém o ponteiro antigo. Então a lógica é parecida com

foo = {n: 2}
bar.x = foo

então bar.x = {n: 2}. Finalmente, o valor de foo é {n: 2}, enquanto bar é {n:1, x: {n: 2 }}

6) This

//Q11
const obj = {
  name: "test",
  prop: {
      name: "prop name",
      print: function(){console.log(this.name)},
  },
  print: function(){ console.log(this.name) },
  print2: () => console.log(this.name, this)
}

obj.print() //ans: test
obj.prop.print() //ans: prop name
obj.print2() //ans: Window {window: Window, self: Window, document: document, name: '', location: Location, …}

O exemplo acima mostra como a palavra-chave this funciona em um objeto. this refere-se a um contexto de execução de objeto em uma execução de função. No entanto, o escopo this está disponível apenas em uma declaração de função regular e não na arrow function. O exemplo acima mostra a ligação explícita onde, por exemplo, object1.object2.object3.object4.print(), a função de impressão usará o objeto mais recente que é object4 no contexto do this. Se this do objeto não estiver vinculado, ele retornará ao objeto raiz, que é o objeto global do window que está acontecendo quando chamamos obj.print2().

Por outro lado, você também deve entender a ligação implícita em que o contexto do objeto já está vinculado antes, para que a próxima execução da função sempre use esse objeto como contexto do this. Por exemplo, quando usamos func.bind(<object>), ele retornará uma nova função que usará <object> como o novo contexto de execução.

7) Coercion

console.log(1 +  "2" + "2"); //ans: 122
console.log(1 +  +"2" + "2"); //ans: 32
console.log(1 +  -"1" + "2"); //ans: 02
console.log(+"1" +  "1" + "2"); //ans: 112
console.log( "A" - "B" + "2"); //ans: NaN2
console.log( "A" - "B" + 2); //ans: NaN
"10,11" == [[[[10]],11]] //10,11 == 10,11, and: true
"[object Object]" == {name: "test"} //ans true

Coercion é uma das questões mais complicadas do JS. Em geral, existem 2 regras. A primeira regra é se 2 operandos estiverem conectados com o operando +, ambos os operandos serão alterados primeiro para uma string com o método toString e depois concatenados. Enquanto isso, o outro operador como -, *, ou / mudará o operando para um número. Se não puder ser convertido em um número, NaN será retornado.

Será mais complicado se o operando incluir um objeto ou array. Qualquer objeto tem um método toString que retorna [object Object]. Mas em uma array, o método toString retornará o valor subjacente separado por uma vírgula.

Observe que: == significa que Coercion pode acontecer, enquanto === não.

8) Async

//Q13
console.log(1); 
new Promise(resolve => {
  console.log(2); 
  return setTimeout(() => {
    console.log(3)
    resolve()
  }, 0)
})
setTimeout(function(){console.log(4)}, 1000); 
setTimeout(function(){console.log(5)}, 0); 
console.log(6);
//ans: 1 2 6 3 5 4

Aqui, você precisa saber como o loop de eventos, a fila de macrotarefas e a fila de microtarefas funcionam. Em geral, a função assíncrona será executada posteriormente, depois que todas as funções síncronas terminarem de ser executadas.

//Q14
async function foo() {return 10}
console.log(foo()) //ans: Promise { 10 }

Uma vez que a função é declarada com async, ele sempre retorna uma Promise, não importa se a lógica interna é síncrona ou assíncrona.

//Q15
const delay = async (item) => new Promise(
	resolve => setTimeout(() => {
		console.log(item);
		resolve(item)
	}, Math.random() * 100)
)
console.log(1)
let arr = [3,4,5,6]
arr.forEach(async (item) => await delay(item))
console.log(2)

forEach é sempre síncrona, não importa se cada loop é síncrono ou assíncrono. Isso significa que cada loop não esperará pelo outro. Se você deseja executar cada loop em sequência e esperar um pelo outro, use for of em vez disso.

9) Função

//Q16
if(function f(){}){ console.log(f) }
//error: ReferenceError: f is not defined

No exemplo acima, a condição if é satisfeita porque a declaração da função é considerada um valor verdadeiro. Porém, o bloco interno não pode acessar a declaração da função porque eles têm um escopo de bloco diferente.

//Q17
function foo(){
	return
	{ name: 2 }
}
foo() //return undefined

por causa da automatic semicolon insertion (ASI), a declaração de retorno será finalizada com o ponto e vírgula e tudo abaixo dele não será executado ou considerado.

A função acima, quando chamada, retorna undefined. Isso ocorre porque a instrução de retorno está antes da declaração do objeto, então o objeto nunca é retornado. O interpretador javascript ignora a linha seguinte ao "return" e como o return está vazio, ele não retorna nada.

//Q18
function foo(a,b,a){return a+b}
console.log(foo(1,2,3)) //ans: 3+2 = 5

function foo2(a,b,c = a){return a+b+c}
console.log(foo2(1,2)) //ans = 1+2+1 = 4

function foo3(a = b, b){return a+b}
console.log(foo3(1,2)) //ans = 1+2 = 3
console.log(foo3(undefined,2)) //error

A função foo, quando chamada com os argumentos 1, 2 e 3, retorna 5. Isso ocorre porque quando há argumentos com o mesmo nome, o último argumento com esse nome é o que é usado. Então, no caso da função acima, o último argumento 'a' é 3 e quando somado com o argumento 'b', o resultado é 5.

As 3 primeiras execuções são bastante claras, mas a última execução da função gera um erro porque b é usado antes de ser declarado, semelhante a este

let a = b   
let b = 2

Quando a função é chamada e o primeiro parâmetro não é passado, o valor padrão é usado e o segundo parâmetro é usado para somar. No exemplo acima, quando a função é chamada sem passar nenhum valor para o primeiro parâmetro, gera um erro, pois "b" ainda não foi definido.

10) Prototype

//Q19
function Person() { }
Person.prototype.walk = function() {
  return this;
}
Person.run = function() {
  return this;
}
let user = new Person();
let walk = user.walk;
console.log(walk()); //window object
console.log(user.walk()); //user object
let run = Person.run;
console.log(run()); //window object
console.log(user.run()); //TypeError: user.run is not a function

Prototype é um objeto que existe em cada variável que é usada para herdar recursos de seu pai. Por exemplo, quando você declara uma variável de string, essa variável de string tem um protótipo que herda de String.prototype. É por isso que você pode chamar um método de string dentro da variável de string, como string.replace(), string.substring(), etc.

No exemplo acima, atribuímos a função walk ao Prototype da função Person e atribuímos a função run ao objeto de função. São 2 objetos diferentes. Todo objeto criado pela função usando a palavra-chave new herdará o método do Prototype da função e não o objeto da função. Mas lembre-se de que, se atribuirmos essa função a uma variável como esta let walk = user.walk, a função esquecerá o contexto user de this e retornará ao contexto de execução window object.

A função acima mostra como o uso de métodos e propriedades de protótipo e estáticos diferem. O método "walk" é adicionado ao protótipo da classe "Person", o que significa que ele é herdado por todas as instâncias da classe. Quando chamado diretamente a partir de uma instância, como "user.walk()", o "this" se refere à instância. No entanto, quando chamado a partir de uma variável que foi atribuída o método, como "walk", o "this" se refere ao objeto global (no caso, o objeto "window").

Já o método "run" é adicionado como uma propriedade estática à classe "Person", o que significa que ele não é herdado por instâncias da classe e só pode ser acessado diretamente através da classe. Quando chamado diretamente a partir da classe, como "Person.run()", o "this" se refere à classe. No entanto, quando tentado chamar "user.run()" gera-se um erro, pois essa propriedade não existe nessa instância.