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
, symbol
e 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.length
como 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 let
e const
sã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.