190+ JavaScript interview questions and answers in quiz-style format, answered by ex-FAANG interviewers
Questions and solutions by ex-interviewers
Covers critical topics
Tired of scrolling through low-quality JavaScript interview questions? You’ve found the right place!
Our JavaScript interview questions are crafted by experienced ex-FAANG senior / staff engineers, not random unverified sources or AI.
With over 190+ questions covering everything from core JavaScript concepts to advanced JavaScript features (async / await, promises, etc.), you’ll be fully prepared.
Each quiz question comes with:
Concise answers (TL;DR): Clear and to-the-point solutions to help you respond confidently during interviews.
Comprehensive explanations: In-depth insights to ensure you fully understand the concepts and can elaborate when required. Don’t waste time elsewhere—start practicing with the best!
If you're looking for JavaScript coding questions -We've got you covered as well, with:
280+ JavaScript coding questions
In-browser coding workspace similar to real interview environment
Hoisting é um termo usado para explicar o comportamento de declarações variáveis em seu código. Variáveis declaradas ou inicializadas com a palavra-chave var terão sua declaração "movida" até o topo do escopo do módulo/função do escopo, a que chamamos de hoisting. No entanto, apenas a declaração está hoisted, a atribuição (se houver uma), ficará onde está.
Observe que a declaração não é realmente movida - o motor de JavaScript analisa as declarações durante a compilação e torna-se ciente das declarações e dos seus âmbitos. É mais fácil compreender este comportamento, visualizando as declarações como sendo hoisted até ao topo do seu escopo. Vamos explicar com alguns exemplos.
console.log(foo);// indefinido
var foo =1;
console.log(foo);// 1
As declarações de função têm o corpo hoisted enquanto as expressões da função (escritas na forma de declarações variáveis) só tem a declaração da variável hoisted.
// Declaração de função
console.log(foo);// [Function: foo]
foo();// 'FOOOOO'
functionfoo(){
console.log('FOOOOO');
}
console.log(foo);// [Function: foo]
// Function Expression
console.log(bar);// undefined
bar();// Uncaught TypeError: bar is not a function
varbar=function(){
console.log('BARRRR');
};
console.log(bar);// [Function: bar]
Variáveis declaradas via let e const também sofrem o hoisted. No entanto, ao contrário de var e function, eles não são inicializados e acessá-los antes que a declaração resulte em uma exceção ReferenceError. A variável está em uma "zona temporária morta" desde o início do bloco até que a declaração seja processada.
x;// undefined; // Erro de referência: y não está definido
var x ='local';
let y ='local';
Quais são as diferenças entre as variáveis criadas usando `let`, `var` ou `const`?
Variáveis declaradas usando a palavra-chave var têm escopo à função na qual foram criadas, ou se criadas fora de qualquer função, ao objeto global. let and const são block scoped, o que significa que eles só são acessíveis dentro do conjunto mais próximo de chaves (função, bloco if-else ou for-loop).
functionfoo(){
// Todas as variáveis são acessíveis dentro de funções.
var bar ='bar';
let baz ='baz';
const qux ='qux';
console.log(bar);// bar
console.log(baz);// baz
console.log(qux);// qux
}
console.log(bar);// ReferenceError: bar não é o console
console.log(baz);// ReferenceError: baz não é definido
console.log(qux);// ReferenceError: qux não é definido
if(true){
var bar ='bar';
let baz ='baz';
const qux ='qux';
}
// var variáveis declaradas são acessíveis em qualquer lugar do escopo da função.
console.log(bar);// bar
// let e const defined variáveis não são acessíveis fora do bloco no qual eles foram definidos.
console.log(baz);// Referência: baz não está definido
console.log(qux);// Referência: qux não está definido
'var' permite que as variáveis sejam hoisted, o que significa que elas podem ser referenciadas no código antes de serem declaradas. 'let' e 'const' não permitirão isso, em vez disso, será lançado um erro.
console.log(foo);// undefined
var foo ='foo';
console.log(baz);// ReferenceError: não pôde acessar o console da declaração léxico 'baz' antes da inicialização
let baz ='baz';
console.log(bar);// ReferenceError: não é possível acessar a declaração léxico 'bar' antes da inicialização
const bar ='bar';
Redeclarar uma variável com var não causará um erro, mas let e const irá.
var foo ='foo';
var foo ='bar';
console.log(foo);// "bar"
let baz ='baz';
let baz ='qux';// Uncaught SyntaxError: Identifier 'baz' já foi declarado
let e const diferem que let permite reatribuir o valor da variável enquanto const não.
// Tudo bem.
let foo ='foo';
foo ='bar';
// Isso causa uma exceção.
const baz ='baz';
baz ='qux';
Notas
Como a maioria dos navegadores suportam let e const atualmente, usar var não é mais recomendado. Se você precisa dar suporte a navegadores mais antigos, escreva seu código usando let e use um transpiler como o Babel para compilar seu código para uma sintaxe mais antiga.
== é o operador de igualdade abstrato enquanto === é o operador de igualdade rigoroso. O operador == será comparado para a igualdade após fazer quaisquer conversões de tipo necessárias. O operador === não fará conversão de tipo, então se dois valores não forem do mesmo tipo === simplesmente retornará false. Ao usar ==, coisas engraçadas podem acontecer, tais como:
1=='1';// true
1==[1];// true
1==true;// true
0=='';// true
0=='0';// true
0==false;// true
Como regra geral, nunca use o operador ==, exceto por conveniência ao comparar com null ou undefined, onde a == null retornará true se a for null ou undefined.
O loop de eventos é um laço de execução única (single-threaded) que monitora a pilha de chamadas e verifica se há algum trabalho a ser feito na fila de tarefas. Se a pilha de chamadas estiver vazia e houver funções de retorno de chamada (callback) na fila de tarefas, uma função é desenfileirada e colocada na pilha de chamadas para ser executada.
A delegação de eventos é um conceito fundamental no desenvolvimento web que permite gerenciar e tratar eventos eficientemente em vários elementos filhos, anexando um único ouvinte de eventos a um elemento ancestral comum. Em vez de atribuir ouvintes de eventos a cada elemento filho individualmente, você delega a responsabilidade de lidar com eventos ao elemento pai ou ancestral, que intercepta os eventos à medida que eles sobem na árvore DOM e identifica o alvo do evento.
Benefícios da delegação de eventos
Eficiência: A delegação de eventos reduz significativamente o número de ouvintes de eventos em seu código, tornando-o mais eficiente em termos de memória e melhorando o desempenho, especialmente ao lidar com um grande número de elementos semelhantes. Isso resulta em um código mais limpo e de fácil manutenção.
Elementos Dinâmicos: A delegação de eventos funciona perfeitamente com elementos gerados ou removidos dinamicamente no DOM. Você não precisa anexar ou remover ouvintes de eventos toda vez que novos elementos são adicionados ou removidos. O ouvinte de eventos delegado os trata automaticamente.
Exemplo
Vamos ilustrar a delegação de eventos com um exemplo moderno usando a sintaxe do ES6:
console.log(`Clicou em ${event.target.textContent}`);
}
});
Neste exemplo, um único ouvinte de eventos de clique é anexado ao elemento <ul>. Quando ocorre um evento de clique em um elemento <li>, o evento se propaga até o elemento <ul>, onde o ouvinte de eventos verifica o nome da tag do alvo para identificar qual item da lista foi clicado.
Casos de uso
A delegação de eventos é comumente usada em várias situações, incluindo:
Gerenciamento de listas, menus ou tabelas com muitos itens ou linhas.
Manipulação de conteúdo dinâmico em aplicativos de página única.
Simplificação do código, evitando a necessidade de anexar e remover ouvintes de eventos para elementos que mudam.
Não há uma explicação simples para this; ela é um dos conceitos mais confusos do JavaScript. Uma explicação superficial é que o valor do this depende de como a função é chamada. Tendo lido muitas explicações sobre this online, Arnav Aggrawal é a explicação que foi mais clara. As seguintes regras se aplicam:
Se a palavra-chave new é usada ao chamar a função, new dentro da função é um objeto totalmente novo.
Se apply, call, ou bind forem usados para chamar/criar uma função, this dentro da função é o objeto passado como o argumento.
Se uma função é chamada como um método, como obj.method() — this é o objeto do qual a função é uma propriedade.
Se uma função é chamada como uma chamada de função livre, significando que ele foi invocado sem nenhuma das condições presentes acima, this é o objeto global. Em um navegador, é o objeto window. Se em modo estrito ('use strict'), this será undefined em vez do objeto global.
Se se aplicarem múltiplas das regras acima, a regra que é maior ganha e definirá o valor this.
Se a função é uma arrow function ES2015, ela ignora todas as regras acima e recebe o valor this do seu escopo circundante no momento em que ele é criado.
Você pode dar um exemplo de uma das maneiras como o trabalho com "this" mudou no ES2015?
ES2015 permite que você use arrow functions que usa o enclosing lexical scope. Isso geralmente é conveniente, mas impede quem chamou de controlar o contexto através de .call ou .apply—as consequências são que uma biblioteca como jQuery não irá vincular corretamente o this em suas funções de manipulador de eventos. Portanto, é importante ter isso em mente ao refatorar grandes aplicações legadas.
Descreva a diferença entre cookie, `sessionStorage` e `localStorage`.
Local storage is useful for storing data that the user will need to access later, such as offline data, because it stores the data in the browser and the system. This data will persist even if the user closes and reopens the browser and is accessible by other sites.
Session storage is a great way to improve the performance of your web applications. It stores data locally on the browser but is specific to (and only accessible by) the respective site/browser tab and is only available while the user is on the site/tab. This is a more secure storage method due to the restrictive access and promotes better site performance due to reduced data transfer between server and client.
Cookies are a good choice for storing data that should not be persisted for a long time, such as session IDs. Cookies allow you to set an expiry time at which point it would be deleted. Cookies can only be smaller sized data compared to the other two storage methods.
Similaridades
Cookies, localStorage, and sessionStorage, são todos:
Mecanismos de armazenamento no lado do cliente. Isso significa que os clientes podem ler e modificar os valores.
Armazenamento baseado em chave-valor.
Eles só são capazes de armazenar valores como strings. Objetos terão que ser serializados em uma string (JSON.stringify()) a fim de serem armazenados.
Diferenças
Propriedade
Cookie
localStorage
sessionStorage
Iniciador
Cliente ou servidor. O servidor pode usar o cabeçalho Set-Cookie
Cliente
Cliente
Vencimento
Definir manualmente
Para sempre
Ao fechar a aba
Persistente através de sessões do navegador
Depende se a expiração está definida
Sim
Não
Enviado para o servidor com cada solicitação HTTP
Os cookies são automaticamente enviados via cabeçalho Cookie
Não
Não
Capacidade (por domínio)
4kb
5MB
5MB
Acesso
Quqlquer Janela
Quqlquer Janela
Mesma Guia
Também existem outros mecanismos de armazenamento do lado do cliente, como IndexedDB que é mais poderoso do que as tecnologias acima mencionadas, mas mais complicado de usar.
Descreva a diferença entre `<script>`, `<script async>` e `<script defer>`
tags <script> são usadas para incluir JavaScript em uma página da web. Os atributos async e defer são usados para mudar como/quando o carregamento e a execução do script acontecem.
Simples <script>
Para tags normais <script> sem qualquer async ou defer, quando forem encontrados, a análise do HTML é bloqueada, o script é buscado e executado imediatamente. A análise do HTML é retomada após a execução do script.
<script async>
No <script async>, o script será buscado em paralelo à análise do HTML e será executado assim que estiver disponível (potencialmente antes da conclusão da análise do HTML) e não será necessariamente executado na ordem em que aparece no documento HTML. Use async quando o script é independente de quaisquer outros scripts na página, por exemplo, analytics.
<script defer>
No <script defer>, o script será buscado em paralelo à análise HTML e executado quando o documento tiver sido totalmente analisado, mas antes de disparar DOMContentLoaded. Se houver múltiplos, cada script adiado é executado na ordem em que eles apareceram no documento HTML.
Se um script depende de um DOM totalmente analisado, o atributo defer será útil para garantir que o HTML seja totalmente analisado antes de ser executado.
Notas
Geralmente, o atributo async deve ser usado para scripts que não são críticos para a renderização inicial da página e não dependem um do outro. enquanto o atributo defer deve ser usado para scripts que dependem de / é dependente de outro script.
Os atributos async e defer são ignorados para scripts que não têm o atributo src.
<script>s com defer ou async que contêm document.write() serão ignorados com uma mensagem como "Uma chamada para o documento.write() de um script externo carregado de forma assíncrona foi ignorada".
Variáveis não declaradas são criadas quando você atribui um valor a um identificador que não foi criado anteriormente usando var, let ou const. Variáveis não declaradas serão definidas globalmente, fora do escopo atual. No modo estrito, um ReferenceError será lançado quando você tentar atribuir a uma variável não declarada. Variáveis não declaradas são ruins assim como as variáveis globais são ruins. Evite elas a todo custo! Para verificá-las, envolva o uso delas em um bloco try/catch.
functionfoo(){
x =1;// Lança um erro de referência em modo strict
}
foo();
console.log(x);// 1
Uma variável que é undefined é uma variável que foi declarada, mas não atribuída um valor. É do tipo 'undefined'. Se uma função não retornar nenhum valor como resultado de sua execução, e se for atribuída a uma variável, a variável também terá o valor de undefined. Para verificar isso, compare usando o operador de igualdade estrita (===) ou typeof, que retornará a string undefined. Note que você não deve usar o operador de igualdade abstrata para verificar, pois também retornará true se o valor for null.
var foo;
console.log(foo);// undefined
console.log(foo ===undefined);// true
console.log(typeof foo ==='undefined');// true
console.log(foo ==null);// verdadeiro. Errado, não use isso para verificar!
functionbar(){}
var baz =bar();
console.log(baz);// undefined
Uma variável que é null terá sido explicitamente atribuída ao valor null. Ele não representa nenhum valor e é diferente de undefined no sentido de que foi explicitamente atribuído. Para verificar se é null, simplesmente compare usando o operador de igualdade estrita. Observe que, assim como acima, você não deve usar o operador de igualdade abstrata (==) para verificar, pois também retornará true se o valor for undefined.
var foo =null;
console.log(foo ===null);// verdadeiro
console.log(typeof foo ==='object');// verdadeiro
console.log(foo ==undefined);// true. Errado, não use isto para verificar!
Como bom hábito, nunca deixe suas variáveis não declaradas ou não atribuídas. Atribua explicitamente null a elas depois de declará-las, se você não pretende usá-las ainda. Se você usa alguma ferramenta de análise estática em seu fluxo de trabalho (por exemplo, ESLint, TypeScript Compiler), geralmente ela também pode verificar se você está referenciando variáveis não declaradas.
.call e .apply são usados para invocar funções e o primeiro parâmetro será usado como o valor de this dentro da função. No entanto, .call recebe argumentos separados por vírgulas como os próximos argumentos enquanto .apply recebe um array de argumentos como o próximo argumento. Uma maneira fácil de lembrar este é C para chamada e parâmetros separados por vírgulas e A para 'apply' e um array de argumentos.
O método bind() cria uma nova função que, quando chamada, tem sua palavra-chave this definida para o valor fornecido, com uma determinada sequência de argumentos precedendo qualquer um fornecido quando a nova função é chamada.
bind() é mais útil para vincular o valor do this em métodos de classes que você quer passar para outras funções. Isso foi frequentemente feito em métodos de classe de componente React que não foram definidos usando arrow functions.
const john ={
age:42,
getAge:function(){
returnthis.age;
},
};
console.log(john.getAge());// 42
const unboundGetAge = john.getAge;
console.log(unboundGetAge());// undefined
const boundGetAge = john.getAge.bind(john);
console.log(boundGetAge());// 42
const mary ={age:21};
const boundGetAgeMary = john.getAge.bind(mary);
console.log(boundGetAgeMary());// 21
No exemplo acima, quando o método getAge é chamado sem um objeto chamado (como unboundGetAge), o valor é 'indefinido' porque o valor 'this' dentro de getAge() se torna o objeto global. boundGetAge() tem seu this ligado a john, portanto pode obter o idade de john.
Podemos até usar getAge em outro objeto que não é john! boundGetAgeMary retorna o age (idade) de mary.
A principal vantagem de usar uma arrow function como um método dentro de um construtor é que o valor de this é definido no momento da criação da função e não pode mudar depois disso. Então, quando o construtor é usado para criar um novo objeto, this sempre irá referir-se ao objeto. Por exemplo, digamos que temos um construtor Person que toma o primeiro nome como um argumento tem dois métodos para dar um console.log nesse nome, um como uma função normal e um como uma função de seta:
constPerson=function(firstName){
this.firstName= firstName;
this.sayName1=function(){
console.log(this.firstName);
};
this.sayName2=()=>{
console.log(this.firstName);
};
};
const john =newPerson('John');
const dave =newPerson('Dave');
john.sayName1();// John
john.sayName2();// John
// A função regular pode ter seu valor 'this' alterado, mas a arrow function não pode
john.sayName1.call(dave);// Dave (porque "this" é agora o objeto dave)
john.sayName2.call(dave);// John
john.sayName1.apply(dave);// Dave (porque 'this' é agora o objeto dave)
john.sayName2.apply(dave);// João
john.sayName1.bind(dave)();// Dave (porque 'this' é agora o dave object)
john.sayName2.bind(dave)();// João
var sayNameFromWindow1 = john.sayName1;
sayNameFromWindow1();// undefined (porque 'this' agora é o objeto da window)
var sayNameFromWindow2 = john.sayName2;
sayNameFromWindow2();// John
O principal tirada aqui é que this pode ser alterado para uma função normal, mas o contexto sempre permanece o mesmo para uma arrow function. Então, mesmo que você esteja passando pela arrow function para diferentes partes do seu aplicativo, você não precisaria se preocupar com a mudança de contexto.
Isso pode ser particularmente útil em componentes de classe React. Se você definir um método de classe para algo como um manipulador de cliques usando uma função normal, e, em seguida, você passa que clica manipulando em um componente filho como uma propriedade, você também precisará vincular o this no construtor do componente pai. Se você ao invés disso usar uma arrow function, não há necessidade de vincular também "this", como o método irá automaticamente obter seu valor "this" no contexto léxico que está encapsulado. (Veja esse artigo para uma excelente demonstração e código de amostra: https://medium.com/@machnicki/handle-events-in-react-with-arrow-functions-ede88184bbb)
Essa é uma pergunta extremamente comum em entrevistas de JavaScript. Todos os objetos JavaScript têm uma propriedade __proto__ com exceção de objetos criados com object.create(null), ou seja, uma referência a outro objeto, que é chamado de "protótipo" do objeto. Quando uma propriedade é acessada em um objeto e se a propriedade não é encontrada nesse objeto, o motor de JavaScript olha para o objeto __proto__, e o __proto__ do __proto__ e assim por diante, até que encontre a propriedade definida em um dos __proto__s ou até chegar ao final da cadeia de protótipos. Este comportamento simula a herança clássica, mas é realmente mais de delegação do que herança.
Exemplo de Herança de Protótipos
// Construtor de objeto pai.
functionAnimal(name){
this.name= name;
}
// Adiciona um método ao protótipo do objeto pai.
Animal.prototype.makeSound=function(){
console.log('O '+this.constructor.name+' faz um som.');
};
// Construtor filho.
functionDog(name){
Animal.call(this, name);// Chama o construtor pai.
}
// Set the child object's prototype to be the parent's prototype.
Esta pergunta é muito vaga. Nossa melhor suposição sobre a intenção dessa pergunta é que ela está perguntando sobre construtores em JavaScript. Tecnicamente falando, function Person(){} é apenas uma declaração de função normal. A convenção é usar PascalCase para funções que se destinam a ser usadas como construtores.
var person = Person() invoca o Person como uma função, e não como um construtor. Invocar como tal é um erro comum se a função pretende ser usada como um construtor. Normalmente, o construtor não devolve nada. Assim, invocando o construtor como uma função normal retornará undefined e que será atribuído à variável pretendida como a instância.
var person = new Person() cria uma instância do objeto Person usando o operador new, que herda de Person.prototype. Uma alternativa seria usar Object.create, tais como: Object.create(Person.prototype).
functionPerson(name){
this.name= name;
}
var person =Person('John');
console.log(person);// undefined
console.log(person.name);// Uncaught TypeError: Cannot read property 'name' of undefined
var person =newPerson('John');
console.log(person);// Person { name: "John" }
console.log(person.name);// "john"
Explique as diferenças no uso de `foo` entre `function foo() {}` e `var foo = function() {}`
O primeiro é uma declaração de funções, enquanto o segundo é uma expressão de funções. A diferença chave é que as declarações de funções tem seu corpo hoisted, mas os corpos das expressões de função não são (elas têm o mesmo comportamento hoisting que as variáveis). Para obter mais explicações sobre o hoisting, consulte a pergunta sobre hoisting. Se você tentar invocar uma expressão de função antes de ela ser definida, você receberá um erro Uncaught TypeError: XXX não é um erro de função.
Declaração de Função
foo();// 'FOOOO'
functionfoo(){
console.log('FOOOOO');
}
Expressão de Função
foo();// Uncaught TypeError: foo não é uma função
varfoo=function(){
console.log('FOOOOO');
};
Qual é um caso típico de uso para funções anônimas?
Eles podem ser usados em IIFEs (funções autoexecutáveis) para encapsular algum código dentro de um escopo local, para que as variáveis declaradas nele não vazem para o escopo global.
(function(){
// Algum código aqui.
})();
Como uma função de callback que é usada apenas uma vez e não precisa ser usada em nenhum outro lugar. O código parecerá mais autônomo e legível quando os manipuladores forem definidos dentro do próprio código que os chama, em vez de ter que procurar em outro lugar para encontrar o corpo da função.
setTimeout(function(){
console.log('Olá mundo!');
},1000);
Argumentos para construtos de programação funcional ou Lodash (semelhantes a funções de retorno de chamada).
const arr =[1,2,3];
const double = arr.ap(function(el){
return el *2;
});
console.log(double);// [2, 4, 6]
What are the various ways to create objects in JavaScript?
Creating objects in JavaScript offers several methods:
Object literals ({}): Simplest and most popular approach. Define key-value pairs within curly braces.
Object() constructor: Use new Object() with dot notation to add properties.
Object.create(): Create new objects using existing objects as prototypes, inheriting properties and methods.
Constructor functions: Define blueprints for objects using functions, creating instances with new.
ES2015 classes: Structured syntax similar to other languages, using class and constructor keywords.
Objects in JavaScript
Creating objects in JavaScript involves several methods. Here are the various ways to create objects in JavaScript:
Object literals ({})
This is the simplest and most popular way to create objects in JavaScript. It involves defining a collection of key-value pairs within curly braces ({}). It can be used when you need to create a single object with a fixed set of properties.
This method involves using the new keyword with the built-in Object constructor to create an object. You can then add properties to the object using dot notation. It can be used when you need to create an object from a primitive value or to create an empty object.
This method allows you to create a new object using an existing object as a prototype. The new object inherits properties and methods from the prototype object. It can be used when you need to create a new object with a specific prototype.
// Object.create() Method
const personPrototype ={
greet(){
console.log(
`Hello, my name is ${this.name} and I'm ${this.age} years old.`,
);
},
};
const person =Object.create(personPrototype);
person.name='John';
person.age=30;
person.greet();// Output: Hello, my name is John and I'm 30 years old.
An object without a prototype can be created by doing Object.create(null).
ES2015 classes
Classes provide a more structured and familiar syntax (similar to other programming languages) for creating objects. They define a blueprint and use methods to interact with the object's properties. It can be used when you need to create complex objects with inheritance and encapsulation.
classPerson{
constructor(name, age){
this.name= name;
this.age= age;
}
greet=function(){
console.log(
`Hello, my name is ${this.name} and I'm ${this.age} years old.`,
);
};
}
const person1 =newPerson('John',30);
const person2 =newPerson('Alice',25);
person1.greet();// Output: Hello, my name is John and I'm 30 years old.
person2.greet();// Output: Hello, my name is Alice and I'm 25 years old.
Constructor functions
Constructor functions are used to create reusable blueprints for objects. They define the properties and behaviors shared by all objects of that type. You use the new keyword to create instances of the object. It can be used when you need to create multiple objects with similar properties and methods.
However, now that ES2015 classes are readily supported in modern browsers, there's little reason to use constructor functions to create objects.
// Constructor function
functionPerson(name, age){
this.name= name;
this.age= age;
this.greet=function(){
console.log(
`Hello, my name is ${this.name} and I'm ${this.age} years old.`,
);
};
}
const person1 =newPerson('John',30);
const person2 =newPerson('Alice',25);
person1.greet();// Output: Hello, my name is John and I'm 30 years old.
person2.greet();// Output: Hello, my name is Alice and I'm 25 years old.
Em JavaScript, um closure é uma função que captura o escopo léxico no qual foi declarada, permitindo que ela acesse e manipule variáveis de um escopo externo mesmo depois que esse escopo foi fechado.
Aqui está como os closures funcionam:
Escopo léxico: JavaScript utiliza o escopo léxico, o que significa que o acesso de uma função às variáveis é determinado pela sua localização física no código-fonte.
Criação de função: Quando uma função é criada, ela mantém uma referência ao seu escopo léxico. Este escopo contém todas as variáveis locais que estavam em escopo no momento em que o closure foi criado.
Manutenção de estado: Closures são frequentemente usados para manter o estado de uma forma segura, porque as variáveis capturadas pelo closure não são acessíveis fora da função.
Sintaxe do ES6 e closures
Com o ES6, closures podem ser criados usando funções de seta (arrow functions), que proporcionam uma sintaxe mais concisa e vinculam lexicalmente o valor de this. Aqui está um exemplo:
constcriarContador=()=>{
let contador =0;
return()=>{
contador +=1;
return contador;
};
};
const contador =criarContador();
console.log(contador());// Saída: 1
console.log(contador());// Saída: 2
Por que usar closures?
Encapsulamento de dados: Closures fornecem uma maneira de criar variáveis e funções privadas que não podem ser acessadas fora do closure. Isso é útil para ocultar detalhes de implementação e manter o estado de forma encapsulada.
Programação funcional: Closures são fundamentais em paradigmas de programação funcional, onde são usados para criar funções que podem ser passadas e invocadas posteriormente, mantendo o acesso ao escopo em que foram criadas, por exemplo, aplicações parciais ou currying.
Manipuladores de eventos e callbacks: Em JavaScript, closures são frequentemente usados em manipuladores de eventos e callbacks para manter o estado ou acessar variáveis que estavam em escopo quando o manipulador ou callback foi definido.
Padrões de módulo: Closures permitem o uso do padrão de módulo em JavaScript, possibilitando a criação de módulos com partes privadas e públicas.
Qual é a definição de uma função de ordem superior?
Uma função de ordem superior é qualquer função que recebe uma ou mais funções como argumentos, que ela usa para operar em algum dado e/ou retorna uma função como resultado. As funções de ordem superior têm como objetivo abstrair alguma operação que é realizada repetidamente. O exemplo clássico disso é o map, que recebe como argumentos um array e uma função. O map então usa essa função para transformar cada item no array, retornando um novo array com os dados transformados. Outros exemplos populares em JavaScript são forEach, filter e reduce. Uma função de ordem superior não precisa apenas manipular arrays, pois há muitos casos de uso para retornar uma função de outra função. Function.prototype.bind é um exemplo desse tipo em JavaScript.
Map
Vamos supor que temos um array de nomes no qual precisamos transformar cada string em maiúsculas.
Para construtores simples, eles parecem bem semelhantes.
A principal diferença no construtor vem quando se usa herança. Se quisermos criar uma classe Student que seja uma subclasse de Person e adicionar um campo studentId, é isso que devemos fazer além do que foi mostrado acima.
// Construtor de função ES5
functionStudent(name, studentId){
// Chamar o construtor da superclasse para inicializar membros derivados da superclasse.
A propagação de eventos é um mecanismo na DOM (Modelo de Objetos do Documento) em que um evento, como um clique ou um evento de teclado, é primeiro acionado no elemento de destino que iniciou o evento e, em seguida, se propaga para cima (bolhas) pela árvore da DOM até a raiz do documento.
Fase de bolha
Durante a fase de bolha, o evento começa no elemento de destino e se propaga pelos seus ancestrais na hierarquia da DOM. Isso significa que os manipuladores de eventos associados ao elemento de destino e aos seus ancestrais podem potencialmente receber e responder ao evento.
Aqui está um exemplo usando a sintaxe moderna do ES6 para demonstrar a propagação de eventos:
// HTML:
// <div id="pai">
// <button id="filho">Clique em mim!</button>
// </div>
const pai =document.getElementById('pai');
const filho =document.getElementById('filho');
pai.addEventListener('click',()=>{
console.log('Cliquei no elemento pai');
});
filho.addEventListener('click',()=>{
console.log('Cliquei no elemento filho');
});
Quando você clica no botão "Clique em mim!", devido à propagação de eventos, os manipuladores de eventos do filho e do pai serão acionados.
Parando a propagação
A propagação de eventos pode ser interrompida durante a fase de bolha usando o método stopPropagation(). Se um manipulador de eventos chamar stopPropagation(), ele impedirá que o evento continue a se propagar pela árvore da DOM, garantindo que apenas os manipuladores dos elementos até aquele ponto na hierarquia sejam executados.
Delegação de eventos
A propagação de eventos é a base para uma técnica chamada "delegação de eventos", onde você anexa um único manipulador de eventos a um ancestral comum de vários elementos e usa a delegação de eventos para lidar eficientemente com os eventos desses elementos. Isso é particularmente útil quando você tem um grande número de elementos semelhantes, como uma lista de itens, e deseja evitar a adição de manipuladores de eventos individuais a cada item.
Event capturing is a lesser-used counterpart to event bubbling in the DOM event propagation mechanism. It follows the opposite order, where an event triggers first on the ancestor element and then travels down to the target element.
Event capturing is rarely used as compared to event bubbling, but it can be used in specific scenarios where you need to intercept events at a higher level before they reach the target element. It is disabled by default but can be enabled through an option on addEventListener().
What is event capturing?
Event capturing is a propagation mechanism in the DOM (Document Object Model) where an event, such as a click or a keyboard event, is first triggered at the root of the document and then flows down through the DOM tree to the target element.
Capturing has a higher priority than bubbling, meaning that capturing event handlers are executed before bubbling event handlers, as shown by the phases of event propagation:
Capturing phase: The event moves down towards the target element
Target phase: The event reaches the target element
Bubbling phase: The event bubbles up from the target element
Note that event capturing is disabled by default. To enable it you have to pass the capture option into addEventListener().
Capturing phase
During the capturing phase, the event starts at the document root and propagates down to the target element. Any event listeners on ancestor elements in this path will be triggered before the target element's handler. But note that event capturing can't happen until the third argument of addEventListener() is set to true as shown below (default value is false).
Here's an example using modern ES2015 syntax to demonstrate event capturing:
// HTML:
// <div id="parent">
// <button id="child">Click me!</button>
// </div>
const parent =document.getElementById('parent');
const child =document.getElementById('child');
parent.addEventListener(
'click',
()=>{
console.log('Parent element clicked (capturing)');
},
true,// Set third argument to true for capturing
);
child.addEventListener('click',()=>{
console.log('Child element clicked');
});
When you click the "Click me!" button, it will trigger the parent element's capturing handler first, followed by the child element's handler.
Stopping propagation
Event propagation can be stopped during the capturing phase using the stopPropagation() method. This prevents the event from traveling further down the DOM tree.
// HTML:
// <div id="parent">
// <button id="child">Click me!</button>
// </div>
const parent =document.getElementById('parent');
const child =document.getElementById('child');
parent.addEventListener(
'click',
(event)=>{
console.log('Parent element clicked (capturing)');
event.stopPropagation();// Stop event propagation
},
true,
);
child.addEventListener('click',()=>{
console.log('Child element clicked');
});
As a result of stopping event propagation, just the parent event listener will now be called when you click the "Click Me!" button, and the child event listener will never be called because the event propagation has stopped at the parent element.
Uses of event capturing
Event capturing is rarely used as compared to event bubbling, but it can be used in specific scenarios where you need to intercept events at a higher level before they reach the target element.
Stopping event bubbling: Imagine you have a nested element (like a button) inside a container element. Clicking the button might also trigger a click event on the container. By using enabling event capturing on the container's event listener, you can capture the click event there and prevent it from traveling down to the button, potentially causing unintended behavior.
Custom dropdown menus:: When building custom dropdown menus, you might want to capture clicks outside the menu element to close the menu. Using capture: true on the document object allows you to listen for clicks anywhere on the page and close the menu if the click happens outside its boundaries.
Efficiency in certain scenarios:: In some situations, event capturing can be slightly more efficient than relying on bubbling. This is because the event doesn't need to propagate through all child elements before reaching the handler. However, the performance difference is usually negligible for most web applications.
The main difference lies in the bubbling behavior of mouseenter and mouseover events. mouseenter does not bubble while mouseover bubbles.
mouseenter events do not bubble. The mouseenter event is triggered only when the mouse pointer enters the element itself, not its descendants. If a parent element has child elements, and the mouse pointer enters child elements, the mouseenter event will not be triggered on the parent element again, it's only triggered once upon entry of parent element without regard for its contents. If both parent and child have mouseenter listeners attached and the mouse pointer moves from the parent element to the child element, mouseenter will only fire for the child.
mouseover events bubble up the DOM tree. The mouseover event is triggered when the mouse pointer enters the element or one of its descendants. If a parent element has child elements, and the mouse pointer enters child elements, the mouseover event will be triggered on the parent element again as well. If the parent element has multiple child elements, this can result in multiple event callbacks fired. If there are child elements, and the mouse pointer moves from the parent element to the child element, mouseover will fire for both the parent and the child.
Property
mouseenter
mouseover
Bubbling
No
Yes
Trigger
Only when entering itself
When entering itself and when entering descendants
mouseenter event:
Does not bubble: The mouseenter event does not bubble. It is only triggered when the mouse pointer enters the element to which the event listener is attached, not when it enters any child elements.
Triggered once: The mouseenter event is triggered only once when the mouse pointer enters the element, making it more predictable and easier to manage in certain scenarios.
A use case for mouseenter is when you want to detect the mouse entering an element without worrying about child elements triggering the event multiple times.
mouseover Event:
Bubbles up the DOM: The mouseover event bubbles up through the DOM. This means that if you have an event listener on a parent element, it will also trigger when the mouse pointer moves over any child elements.
Triggered multiple times: The mouseover event is triggered every time the mouse pointer moves over an element or any of its child elements. This can lead to multiple triggers if you have nested elements.
A use case for mouseover is when you want to detect when the mouse enters an element or any of its children and are okay with the events triggering multiple times.
Example
Here's an example demonstrating the difference between mouseover and mouseenter events:
'use strict' é uma declaração usada para habilitar o modo estrito em scripts inteiros ou funções individuais. O modo estrito é uma maneira de optar por uma variante restrita do JavaScript.
Vantagens
Faz com que seja impossível criar variáveis globais acidentalmente.
Faz com que as atribuições que falhariam silenciosamente disparem uma exceção.
Faz tentativas de excluir propriedades não deletáveis lançam uma exceção (onde antes da tentativa simplesmente não teria efeito).
Exige que os nomes dos parâmetros de função sejam exclusivos.
this é undefined no contexto global.
Ele captura alguns erros comuns de programação, lançando exceções.
Ele desabilita recursos que são confusos ou mal concebidos.
Desvantagens
Muitos recursos faltantes que alguns desenvolvedores podem estar acostumados.
Não há mais acesso a function.caller e function.arguments.
Concatenação de scripts escritos em diferentes modos estritos pode causar problemas.
No geral, os benefícios superam as desvantagens e não há realmente a necessidade de depender dos recursos que o modo estrito proíbe. Todos nós deveríamos estar usando o modo estrito por padrão.
Explique a diferença entre funções síncronas e assíncronas
Funções síncronas são bloqueadoras enquanto funções assíncronas não são. Em funções síncronas, as instruções são concluídas antes que a próxima instrução seja executada. Nesse caso, o programa é avaliado exatamente na ordem das instruções, e a execução do programa é pausada se uma das instruções demorar muito tempo.
Funções assíncronas geralmente aceitam uma função de retorno de chamada como parâmetro e a execução continua na próxima linha imediatamente após a invocação da função assíncrona. A callback só é invocada quando a operação assíncrona é concluída e a pilha de chamadas está vazia. Operações pesadas, como carregar dados de um servidor web ou consultar um banco de dados, devem ser feitas de forma assíncrona para que a thread principal possa continuar executando outras operações em vez de bloquear até que aquela operação longa seja concluída (no caso dos navegadores, a interface do usuário ficará congelada).
Quais são os prós e contras de usar Promises em vez de callbacks?
Ajax (asynchronous JavaScript and XML) é um conjunto de técnicas de desenvolvimento web que utilizam diversas tecnologias web no lado do cliente para criar aplicações web assíncronas. Com o Ajax, as aplicações web podem enviar dados para e receber do servidor de forma assíncrona (em segundo plano) sem interferir na exibição e comportamento da página existente. Ao separar a camada de intercâmbio de dados da camada de apresentação, o Ajax permite que páginas web, e por extensão, aplicações web, alterem o conteúdo dinamicamente sem precisar recarregar toda a página. Na prática, implementações modernas comumente usam JSON em vez de XML, devido às vantagens de o JSON ser nativo ao JavaScript.
A API XMLHttpRequest é frequentemente usada para a comunicação assíncrona ou hoje em dia, a API fetch.
Quais são as vantagens e desvantagens de usar o Ajax?
Melhor interatividade. O novo conteúdo do servidor pode ser alterado dinamicamente sem a necessidade de recarregar a página inteira.
Reduzir conexões para o servidor, pois scripts e folhas de estilo só precisam ser solicitados uma vez.
O State pode ser mantido em uma página. As variáveis do JavaScript e o estado do DOM persistirão porque a página principal do contêiner não foi recarregada.
Basicamente, a maioria das vantagens de um SPA.
Desvantagens
Páginas dinâmicas da web são mais difíceis de favoritar.
Não funciona se o JavaScript foi desativado no navegador.
Alguns rastreadores web não executam JavaScript e não veem o conteúdo que foi carregado pelo JavaScript.
Páginas web que usam Ajax para buscar dados provavelmente terão que combinar os dados remotos com modelos do lado cliente para atualizar o DOM. Para que isso aconteça, o JavaScript terá que ser analisado e executado no navegador, e dispositivos móveis de baixo custo podem ter problemas com isso.
Basicamente, a maioria das vantagens de um SPA.
What are the differences between `XMLHttpRequest` and `fetch()` in JavaScript and browsers?
XMLHttpRequest (XHR) and fetch() API are both used for asynchronous HTTP requests in JavaScript (AJAX). fetch() offers a cleaner syntax, promise-based approach, and more modern feature set compared to XHR. However, there are some differences:
XMLHttpRequest event callbacks, while fetch() utilizes promise chaining.
fetch() provides more flexibility in headers and request bodies.
fetch() support cleaner error handling with catch().
Handling caching with XMLHttpRequest is difficult but caching is supported by fetch() by default in the options.cache object (cache value of second parameter) to fetch() or Request().
fetch() requires an AbortController for cancelation, while for XMLHttpRequest, it provides abort() property.
XMLHttpRequest has good support for progress tracking, which fetch() lacks.
XMLHttpRequest is only available in the browser and not natively supported in Node.js environments. On the other hand fetch() is part of the JavaScript language and is supported on all modern JavaScript runtimes.
These days fetch() is preferred for its cleaner syntax and modern features.
XMLHttpRequest vs fetch()
Both XMLHttpRequest (XHR) and fetch() are ways to make asynchronous HTTP requests in JavaScript. However, they differ significantly in syntax, promise handling, and feature set.
Syntax and usage
XMLHttpRequest is event-driven and requires attaching event listeners to handle response/error states. The basic syntax for creating an XMLHttpRequest object and sending a request is as follows:
xhr is an instance of the XMLHttpRequest class. The open method is used to specify the request method, URL, and whether the request should be asynchronous. The onload event is used to handle the response, and the send method is used to send the request.
fetch() provides a more straightforward and intuitive way of making HTTP requests. It is Promise-based and returns a promise that resolves with the response or rejects with an error. The basic syntax for making a GET request using fetch() is as follows:
Both XMLHttpRequest and fetch() support setting request headers. However, fetch() provides more flexibility in terms of setting headers, as it supports custom headers and allows for more complex header configurations.
XMLHttpRequest supports setting request headers using the setRequestHeader method:
Both XMLHttpRequest and fetch() support sending request bodies. However, fetch() provides more flexibility in terms of sending request bodies, as it supports sending JSON data, form data, and more.
XMLHttpRequest supports sending request bodies using the send method:
XMLHttpRequest provides a responseType property to set the response format that we are expecting. responseType is 'text' by default but it support types likes 'text', 'arraybuffer', 'blob', 'document' and 'json'.
Both support error handling but fetch() provides more flexibility in terms of error handling, as it supports handling errors using the .catch() method.
XMLHttpRequest supports error handling using the onerror event:
const xhr =newXMLHttpRequest();
xhr.open('GET','https://jsonplaceholder.typicod.com/todos/1',true);// Typo in URL
xhr.responseType='json';
xhr.onload=function(){
if(xhr.status===200){
console.log(xhr.response);
}
};
xhr.onerror=function(){
console.error('Error occurred');
};
xhr.send();
fetch() supports error handling using the catch() method on the returned Promise:
fetch('https://jsonplaceholder.typicod.com/todos/1')// Typo in URL
Handling caching with XMLHttpRequest is difficult, and you might need to add a random value to the query string in order to get around the browser cache. Caching is supported by fetch() by default in the second parameter of the options object:
const res =awaitfetch('https://jsonplaceholder.typicode.com/todos/1',{
method:'GET',
cache:'default',
});
Other values for the cache option include default, no-store, reload, no-cache, force-cache, and only-if-cached.
Cancelation
In-flight XMLHttpRequests can be canceled by running the XMLHttpRequest's abort() method. An abort handler can be attached by assigning to the .onabort property if necessary:
XMLHttpRequest supports tracking the progress of requests by attaching a handler to the XMLHttpRequest object's progress event. This is especially useful when uploading large files such as videos to track the progress of the upload.
The callback assigned to onprogress is passed a ProgressEvent:
The loaded field on the ProgressEvent is a 64-bit integer indicating the amount of work already performed (bytes uploaded/downloaded) by the underlying process.
The total field on the ProgressEvent is a 64-bit integer representing the total amount of work that the underlying process is in the progress of performing. When downloading resources, this is the Content-Length value of the HTTP response.
On the other hand, the fetch() API does not offer any convenient way to track upload progress. It can be implemented by monitoring the body of the Response object as a fraction of the Content-Length header, but it's quite complicated.
Choosing between XMLHttpRequest and fetch()
In modern development scenarios, fetch() is the preferred choice due to its cleaner syntax, promise-based approach, and improved handling of features like error handling, headers, and CORS.
AbortController is used to cancel ongoing asynchronous operations like fetch requests.
const controller =newAbortController();
const signal = controller.signal;
fetch('https://jsonplaceholder.typicode.com/todos/1',{ signal })
.then((response)=>{
// Handle response
})
.catch((error)=>{
if(error.name==='AbortError'){
console.log('Request aborted');
}else{
console.error('Error:', error);
}
});
// Call abort() to abort the request
controller.abort();
Aborting web requests is useful for:
Canceling requests based on user actions.
Prioritizing the latest requests in scenarios with multiple simultaneous requests.
Canceling requests that are no longer needed, e.g. after the user has navigated away from the page.
AbortControllers
AbortController allows graceful cancelation of ongoing asynchronous operations like fetch requests. It offers a mechanism to signal to the underlying network layer that the request is no longer required, preventing unnecessary resource consumption and improving user experience.
Using AbortControllers
Using AbortControllers involve the following steps:
Create an AbortController instance: Initialize an AbortController instance, which creates a signal that can be used to abort requests.
Pass the signal to the request: Pass the signal to the request, typically through the signal property in the request options.
Abort the request: Call the abort() method on the AbortController instance to cancel the ongoing request.
Here is an example of how to use AbortControllers with the fetch() API:
const controller =newAbortController();
const signal = controller.signal;
fetch('https://jsonplaceholder.typicode.com/todos/1',{ signal })
.then((response)=>{
// Handle response
})
.catch((error)=>{
if(error.name==='AbortError'){
console.log('Request aborted');
}else{
console.error('Error:', error);
}
});
// Call abort() to abort the request
controller.abort();
Use cases
Canceling a fetch() request on a user action
Cancel requests that take too long or are no longer relevant due to user interactions (e.g., user cancels uploading of a huge file).
// Only the last request should (posts/3) will be allowed to complete
In this example, when the fetchData() function is called multiple times triggering multiple fetch requests, AbortControllers will cancel all the previous requests except the latest request. This is common in scenarios like type-ahead search or infinite scrolling, where new requests are triggered frequently.
Canceling requests that are no longer needed
In situations where the user has navigated away from the page, aborting the request can prevent unnecessary operations (e.g. success callback handling), and freeing up resources by lowering the likelihood of memory leaks.
Notes
AbortControllers is not fetch()-specific, it can be used to abort other asynchronous tasks as well.
A singular AbortContoller instance can be reused on multiple async tasks and cancel all of them at once.
Calling abort() on AbortControllers does not send any notification or signal to the server. The server is unaware of the cancelation and will continue processing the request until it completes or times out.
Polyfills in JavaScript are pieces of code that provide modern functionality to older browsers that lack native support for those features. They bridge the gap between the JavaScript language features and APIs available in modern browsers and the limited capabilities of older browser versions.
They can be implemented manually or included through libraries and are often used in conjunction with feature detection.
Common use cases include:
New JavaScript Methods: For example, Array.prototype.includes(), Object.assign(), etc.
New APIs: Such as fetch(), Promise, IntersectionObserver, etc. Modern browsers support these now but for a long time they have to be polyfilled.
Libraries and services for polyfills:
core-js: A modular standard library for JavaScript which includes polyfills for a wide range of ECMAScript features.
import'core-js/actual/array/flat-map';// With this, Array.prototype.flatMap is available to be used.
[1,2].flatMap((it)=>[it, it]);// => [1, 1, 2, 2]
Polyfill.io: A service that provides polyfills based on the features and user agents specified in the request.
Polyfills in JavaScript are pieces of code (usually JavaScript) that provide modern functionality on older browsers that do not natively support it. They enable developers to use newer features of the language and APIs while maintaining compatibility with older environments.
How polyfills work
Polyfills detect if a feature or API is missing in a browser and provide a custom implementation of that feature using existing JavaScript capabilities. This allows developers to write code using the latest JavaScript features and APIs without worrying about browser compatibility issues.
For example, let's consider the Array.prototype.includes() method, which determines if an array includes a specific element. This method is not supported in older browsers like Internet Explorer 11. To address this, we can use a polyfill:
// Polyfill for Array.prototype.includes()
if(!Array.prototype.includes){
Array.prototype.includes=function(searchElement){
for(var i =0; i <this.length; i++){
if(this[i]=== searchElement){
returntrue;
}
}
returnfalse;
};
}
console.log([1,2,3].includes(2));// true
console.log([1,2,3].includes(4));// false
By including this polyfill, we can safely use Array.prototype.includes() even in browsers that don't support it natively.
Implementing polyfills
Identify the missing feature: Determine if the feature is compatible with the target browsers or detect its presence using feature detection methods like typeof, in, or window.
Write the fallback implementation: Develop the fallback implementation that provides similar functionality, either using a pre-existing polyfill library or pure JavaScript code.
Test the polyfill: Thoroughly test the polyfill to ensure it functions as intended across different contexts and browsers.
Implement the polyfill: Enclose the code that uses the missing feature in an if statement that checks for feature support. If not supported, run the polyfill code instead.
Considerations
Selective loading: Polyfills should only be loaded for browsers that need them to optimize performance.
Feature detection: Perform feature detection before applying a polyfill to avoid overwriting native implementations or applying unnecessary polyfills.
Size and performance: Polyfills can increase the JavaScript bundle size, so minification and compression techniques should be used to mitigate this impact.
Existing libraries: Consider using existing libraries and tools that offer comprehensive polyfill solutions for multiple features, handling feature detection, conditional loading, and fallbacks efficiently
Libraries and services for polyfills
core-js: A modular standard library for JavaScript which includes polyfills for a wide range of ECMAScript features.
import'core-js/actual/array/flat-map';// With this, Array.prototype.flatMap is available to be used.
[1,2].flatMap((it)=>[it, it]);// => [1, 1, 2, 2]
Polyfill.io: A service that provides polyfills based on the features and user agents specified in the request.
Estender um objeto nativo/embutido do JavaScript significa adicionar propriedades/funções ao seu prototype. Embora isso possa parecer uma boa ideia a princípio, é perigoso na prática. Imagine que seu código use algumas bibliotecas que estendem o Array.prototype adicionando o mesmo método contains. As implementações se sobrescreverão e seu código quebrará se o comportamento desses dois métodos não for o mesmo.
A única vez que você pode querer estender um objeto nativo é quando deseja criar um polyfill, essencialmente fornecendo sua própria implementação para um método que faz parte da especificação do JavaScript, mas pode não existir no navegador do usuário devido a ser um navegador mais antigo.
Por que, em geral, é uma boa ideia deixar o escopo global de um site como está e nunca modificá-lo?
JavaScript que é executado no navegador tem acesso ao escopo global, e se todos usarem o namespace global para definir suas variáveis, as colisões provavelmente ocorrerão. Use o padrão do módulo (IIFES) para encapsular suas variáveis dentro de um namespace local.
Explain the differences between CommonJS modules and ES modules in JavaScript
In JavaScript, modules are reusable pieces of code that encapsulate functionality, making it easier to manage, maintain, and structure your applications. Modules allow you to break down your code into smaller, manageable parts, each with its own scope.
CommonJS is an older module system that was initially designed for server-side JavaScript development with Node.js. It uses the require() function to load modules and the module.exports or exports object to define the exports of a module.
// my-module.js
const value =42;
module.exports={ value };
// main.js
const myModule =require('./my-module.js');
console.log(myModule.value);// 42
ES Modules (ECMAScript Modules) are the standardized module system introduced in ES6 (ECMAScript 2015). They use the import and export statements to handle module dependencies.
// my-module.js
exportconst value =42;
// main.js
import{ value }from'./my-module.js';
console.log(value);// 42
CommonJS vs ES modules
Feature
CommonJS
ES modules
Module Syntax
require() for importing module.exports for exporting
import for importing export for exporting
Environment
Primarily used in Node.js for server-side development
Designed for both browser and server-side JavaScript (Node.js)
Loading
Synchronous loading of modules
Asynchronous loading of modules
Structure
Dynamic imports, can be conditionally called
Static imports/exports at the top level
File extensions
.js (default)
.mjs or .js (with type: "module" in package.json)
Browser support
Not natively supported in browsers
Natively supported in modern browsers
Optimization
Limited optimization due to dynamic nature
Allows for optimizations like tree-shaking due to static structure
Compatibility
Widely used in existing Node.js codebases and libraries
Newer standard, but gaining adoption in modern projects
Modules in Javascript
Modules in JavaScript are a way to organize and encapsulate code into reusable and maintainable units. They allow developers to break down their codebase into smaller, self-contained pieces, promoting code reuse, separation of concerns, and better organization. There are two main module systems in JavaScript: CommonJS and ES modules.
CommonJS
CommonJS is an older module system that was initially designed for server-side JavaScript development with Node.js. It uses the require function to load modules and the module.exports or exports object to define the exports of a module.
Syntax: Modules are included using require() and exported using module.exports.
Environment: Primarily used in Node.js.
Execution: Modules are loaded synchronously.
Modules are loaded dynamically at runtime.
// my-module.js
const value =42;
module.exports={ value };
// main.js
const myModule =require('./my-module.js');
console.log(myModule.value);// 42
ES Modules
ES Modules (ECMAScript Modules) are the standardized module system introduced in ES6 (ECMAScript 2015). They use the import and export statements to handle module dependencies.
Syntax: Modules are imported using import and exported using export.
Environment: Can be used in both browser environments and Node.js (with certain configurations).
Execution: Modules are loaded asynchronously.
Support: Introduced in ES2015, now widely supported in modern browsers and Node.js.
Modules are loaded statically at compile-time.
Enables better performance due to static analysis and tree-shaking.
// my-module.js
exportconst value =42;
// main.js
import{ value }from'./my-module.js';
console.log(value);// 42
Summary
While CommonJS was the default module system in Node.js initially, ES modules are now the recommended approach for new projects, as they provide better tooling, performance, and ecosystem compatibility. However, CommonJS modules are still widely used in existing code bases and libraries especially for legacy dependencies.
In JavaScript, data types can be categorized into primitive and non-primitive types:
Primitive data types
Number: Represents both integers and floating-point numbers.
String: Represents sequences of characters.
Boolean: Represents true or false values.
Undefined: A variable that has been declared but not assigned a value.
Null: Represents the intentional absence of any object value.
Symbol: A unique and immutable value used as object property keys. Read more in our deep dive on Symbols
BigInt: Represents integers with arbitrary precision.
Non-primitive (Reference) data types
Object: Used to store collections of data.
Array: An ordered collection of data.
Function: A callable object.
Date: Represents dates and times.
RegExp: Represents regular expressions.
Map: A collection of keyed data items.
Set: A collection of unique values.
The primitive types store a single value, while non-primitive types can store collections of data or complex entities.
Data types in JavaScript
JavaScript, like many programming languages, has a variety of data types to represent different kinds of data. The main data types in JavaScript can be divided into two categories: primitive and non-primitive (reference) types.
Primitive data types
Number: Represents both integer and floating-point numbers. JavaScript only has one type of number.
let age =25;
let price =99.99;
console.log(price);// 99.99
String: Represents sequences of characters. Strings can be enclosed in single quotes, double quotes, or backticks (for template literals).
let myName ='John Doe';
let greeting ='Hello, world!';
let message =`Welcome, ${myName}!`;
console.log(message);// "Welcome, John Doe!"
Boolean: Represents logical entities and can have two values: true or false.
let isActive =true;
let isOver18 =false;
console.log(isOver18);// false
Undefined: A variable that has been declared but not assigned a value is of type undefined.
let user;
console.log(user);// undefined
Null: Represents the intentional absence of any object value. It is a primitive value and is treated as a falsy value.
let user =null;
console.log(user);// null
if(!user){
console.log('user is a falsy value');
}
Symbol: A unique and immutable primitive value, typically used as the key of an object property.
let sym1 =Symbol();
let sym2 =Symbol('description');
console.log(sym1);// Symbol()
console.log(sym2);// Symbol(description)
BigInt: Used for representing integers with arbitrary precision, useful for working with very large numbers.
let bigNumber =BigInt(9007199254740991);
let anotherBigNumber =1234567890123456789012345678901234567890n;
Object: It is used to store collections of data and more complex entities. Objects are created using curly braces {}.
let person ={
name:'Alice',
age:30,
};
console.log(person);// {name: "Alice", age: 30}
Array: A special type of object used for storing ordered collections of data. Arrays are created using square brackets [].
let numbers =[1,2,3,4,5];
console.log(numbers);
Function: Functions in JavaScript are objects. They can be defined using function declarations or expressions.
functiongreet(){
console.log('Hello!');
}
letadd=function(a, b){
return a + b;
};
greet();// "Hello!"
console.log(add(2,3));// 5
Date: Represents dates and times. The Date object is used to work with dates.
let today =newDate().toLocaleTimeString();
console.log(today);
RegExp: Represents regular expressions, which are patterns used to match character combinations in strings.
let pattern =/abc/;
let str ='123abc456';
console.log(pattern.test(str));// true
Map: A collection of keyed data items, similar to an object but allows keys of any type.
let map =newMap();
map.set('key1','value1');
console.log(map);
Set: A collection of unique values.
let set =newSet();
set.add(1);
set.add(2);
console.log(set);// { 1, 2 }
Determining data types
JavaScript is a dynamically-typed language, which means variables can hold values of different data types over time. The typeof operator can be used to determine the data type of a value or variable.
console.log(typeof42);// "number"
console.log(typeof'hello');// "string"
console.log(typeoftrue);// "boolean"
console.log(typeofundefined);// "undefined"
console.log(typeofnull);// "object" (this is a historical bug in JavaScript)
console.log(typeofSymbol());// "symbol"
console.log(typeofBigInt(123));// "bigint"
console.log(typeof{});// "object"
console.log(typeof[]);// "object"
console.log(typeoffunction(){});// "function"
Pitfalls
Type coercion
JavaScript often performs type coercion, converting values from one type to another, which can lead to unexpected results.
In the first example, since strings can be concatenated with the + operator, the number is converted into a string and the two strings are concatenated together. In the second example, strings cannot work with the minus operator (-), but two numbers can be minused, so the string is first converted into a number and the result is the difference.
A declaração for...in itera sobre todas as propriedades enumeráveis do objeto (incluindo as propriedades enumeráveis herdadas). Portanto, na maioria das vezes, você deve verificar se a propriedade existe diretamente no objeto por meio de Object.hasOwn(object, property) antes de usá-la.
for(const property in obj){
if(Object.hasOwn(obj, property)){
console.log(property);
}
}
Observe que obj.hasOwnProperty() não é recomendado porque não funciona para objetos criados usando Object.create(null). É recomendado usar Object.hasOwn() nos navegadores mais recentes, ou use o bom Object.prototype.hasOwnProperty.call(object, key).
Object.keys()
Object.keys(obj).forEach((property)=>{
console.log(property);
});
Object.keys() é um método estático que retornará um array com todos os nomes de propriedade enumerados do objeto que você passa. Uma vez que Object.keys() retorna um array, você também pode usar as abordagens de iteração de matriz listadas abaixo para iterar através dele.
Object.getOwnPropertyNames() é um método estático que listará todas as propriedades enumeradas e não-enumeráveis do objeto que você passou. Uma vez que Object.getOwnPropertyNames() retorna uma matriz, você também pode usar as abordagens de iteração de matriz listadas abaixo para percorrer a matriz.
Uma armadilha comum aqui é que var está no escopo da função e não no escopo de bloco, e na maioria das vezes você desejará uma variável de iterador com escopo de bloco. ES2015 introduz o let que tem escopo de bloco e é recomendado usar let em vez de var.
for(let i =0; i < arr.length; i++){
console.log(arr[i]);
}
Array.prototype.forEach()
arr.forEach((element, index)=>{
console.log(element, index);
});
O método Array.prototype.forEach() pode ser mais conveniente em algumas situações se você não precisar usar o índice e só precisar dos elementos individuais do array. No entanto, o lado negativo é que não se pode parar a iteração a meio do caminho e a função fornecida será executada uma vez nos elementos. Um laço for ou uma declaração for...of são mais relevantes quando é necessário um controle mais preciso sobre a iteração.
ES2015 introduz uma nova forma de iterar, o laço para, que permite repetir o loop sobre objetos que estejam em conformidade com o protocolo iterável como String, Array, Set, etc. Ele combina as vantagens do loop for e do método forEach(). A vantagem do laço para é que você pode parar dele, e a vantagem de forEach() é que ele é mais conciso que o laço for porque você não precisa de uma variável contadora. Com a declaração for...of, você obtém a capacidade de quebrar de um laço e uma sintaxe mais concisa.
A maioria das vezes, prefira o método .forEach, mas realmente depende do que você está tentando fazer. Antes do ES2015, usamos laços for quando precisávamos encerrar prematuramente o laço usando break. Mas agora com ES2015, podemos fazer isso com a declaração for...of. Use laços for quando você precisar de mais flexibilidade, como incrementar o iterador mais de uma vez por laço.
Além disso, ao usar o for...of declaração, se você precisar acessar o índice e o valor de cada elemento de matriz, você pode fazer isso com ES2015 Array.prototype.entries():
A sintaxe spread do ES2015 é muito útil quando programando em um paradigma funcional, pois podemos facilmente criar cópias de arrays ou objetos sem recorrer a Object.create, slice, ou uma função de biblioteca. Esse recurso de linguagem é frequentemente utilizado em projetos Redux e RxJS.
functionputDookieInAnyArray(arr){
return[...arr,'dookie'];
}
const result =putDookieInAnyArray(['I','really',"don't",'like']);// ["I", "really", "don't", "like", "dookie"]
const person ={
name:'Todd',
age:29,
};
const copyOfTodd ={...person };
A sintaxe rest do ES2015 oferece uma maneira mais fácil de incluir um número arbitrário de argumentos para serem passados para uma função. É como uma inversão da sintaxe spread, pegando dados e colocando-os em um array em vez de desempacotar um array de dados, e funciona em argumentos de função, bem como em atribuições de desconstrução de arrays e objetos.
functionaddFiveToABunchOfNumbers(...numbers){
return numbers.map((x)=> x +5);
}
const result =addFiveToABunchOfNumbers(4,5,6,7,8,9,10);// [9, 10, 11, 12, 13, 14, 15]
In JavaScript, iterators and generators are powerful tools for managing sequences of data and controlling the flow of execution in a more flexible way.
Iterators are objects that define a sequence and potentially a return value upon its termination. It adheres to a specific interface:
An iterator object must implement a next() method.
The next() method returns an object with two properties:
value: The next value in the sequence.
done: A boolean that is true if the iterator has finished its sequence, otherwise false.
Here's an example of an object implementing the iterator interface.
Generators are a special functions that can pause execution and resume at a later point. It uses the function* syntax and the yield keyword to control the flow of execution. When you call a generator function, it doesn't execute completely like a regular function. Instead, it returns an iterator object. Calling the next() method on the returned iterator advances the generator to the next yield statement, and the value after yield becomes the return value of next().
Generators are powerful for creating iterators on-demand, especially for infinite sequences or complex iteration logic. They can be used for:
Lazy evaluation – processing elements only when needed, improving memory efficiency for large datasets.
Implementing iterators for custom data structures.
Creating asynchronous iterators for handling data streams.
Iterators
Iterators are objects that define a sequence and provide a next() method to access the next value in the sequence. They are used to iterate over data structures like arrays, strings, and custom objects. The key use case of iterators include:
Implementing the iterator protocol to make custom objects iterable, allowing them to be used with for...of loops and other language constructs that expect iterables.
Providing a standard way to iterate over different data structures, making code more reusable and maintainable.
Creating a custom iterator for a range of numbers
In JavaScript, we can provide a default implementation for iterator by implementing [Symbol.iterator]() in any custom object.
// Define a class named Range
classRange{
// The constructor takes two parameters: start and end
constructor(start, end){
// Assign the start and end values to the instance
this.start= start;
this.end= end;
}
// Define the default iterator for the object
[Symbol.iterator](){
// Initialize the current value to the start value
let current =this.start;
const end =this.end;
// Return an object with a next method
return{
// The next method returns the next value in the iteration
next(){
// If the current value is less than or equal to the end value...
if(current <= end){
// ...return an object with the current value and done set to false
return{value: current++,done:false};
}
// ...otherwise, return an object with value set to undefined and done set to true
return{value:undefined,done:true};
},
};
}
}
// Create a new Range object with start = 1 and end = 3
const range =newRange(1,3);
// Iterate over the range object
for(const number of range){
// Log each number to the console
console.log(number);// 1, 2, 3
}
Built-in objects using the iterator protocol
In JavaScript, several built-in objects implement the iterator protocol, meaning they have a default @@iterator method. This allows them to be used in constructs like for...of loops and with the spread operator. Here are some of the key built-in objects that implement iterators:
Arrays: Arrays have a built-in iterator that allows you to iterate over their elements.
console.log(node);// Logs each <div> element, in this case only div1
}
Maps and Sets also have built-in iterators.
Generators
Generators are a special kind of function that can pause and resume their execution, allowing them to generate a sequence of values on-the-fly. They are commonly used to create iterators but have other applications as well. The key use cases of generators include:
Creating iterators in a more concise and readable way compared to manually implementing the iterator protocol.
Implementing lazy evaluation, where values are generated only when needed, saving memory and computation time.
Simplifying asynchronous programming by allowing code to be written in a synchronous-looking style using yield and await.
Generators provide several benefits:
Lazy evaluation: They generate values on the fly and only when required, which is memory efficient.
Pause and resume: Generators can pause execution (via yield) and can also receive new data upon resuming.
Asynchronous iteration: With the advent of async/await, generators can be used to manage asynchronous data flows.
Creating an iterator using a generator function
We can rewrite our Range example to use a generator function:
// Define a class named Range
classRange{
// The constructor takes two parameters: start and end
constructor(start, end){
// Assign the start and end values to the instance
this.start= start;
this.end= end;
}
// Define the default iterator for the object using a generator
*[Symbol.iterator](){
// Initialize the current value to the start value
let current =this.start;
// While the current value is less than or equal to the end value...
while(current <=this.end){
// ...yield the current value
yield current++;
}
}
}
// Create a new Range object with start = 1 and end = 3
const range =newRange(1,3);
// Iterate over the range object
for(const number of range){
// Log each number to the console
console.log(number);// 1, 2, 3
}
Iterating over data streams
Generators are well-suited for iterating over data streams, such as fetching data from an API or reading files. This example demonstrates using a generator to fetch data from an API in batches:
This generator function fetchDataInBatches fetches data from an API in batches of a specified size. It yields each batch of data, allowing you to process it before fetching the next batch. This approach can be more memory-efficient than fetching all data at once.
Implementing asynchronous iterators
Generators can be used to implement asynchronous iterators, which are useful for working with asynchronous data sources. This example demonstrates an asynchronous iterator for fetching data from an API:
The generator function fetchDataAsyncIterator is an asynchronous iterator that fetches data from an API in pages. It yields each page of data, allowing you to process it before fetching the next page. This approach can be useful for handling large datasets or long-running operations.
Generators are also used extensively in JavaScript libraries and frameworks, such as Redux-Saga and RxJS, for handling asynchronous operations and reactive programming.
Summary
Iterators and generators provide a powerful and flexible way to work with collections of data in JavaScript. Iterators define a standardized way to traverse data sequences, while generators offer a more expressive and efficient way to create iterators, handle asynchronous operations, and compose complex data pipelines.
A imutabilidade é um princípio central na programação funcional e também tem muito a oferecer para programas orientados para objetos. Um objeto mutável é um objeto cujo estado pode ser modificado depois de criado. Um objeto mutável é um objeto cujo estado pode ser modificado depois de criado.
O que é um exemplo de um objeto imutável em JavaScript?
Em JavaScript, alguns tipos internos (números, strings) são imutáveis, mas objetos personalizados geralmente são mutáveis.
Alguns objetos JavaScript imutáveis integrados são Math, Date.
Aqui estão algumas maneiras de adicionar/simular imutabilidade em objetos JavaScript simples.
Propriedades Constante de Objeto
Ao combinar gravável: falso e configurável: falso, você pode criar uma constante (não pode ser alterada, redefinida ou apagada) como uma propriedade do objeto, como:
let myObject ={};
Object.defineProperty(myObject,'number',{
value:42,
writable:false,
configurable:false,
});
console.log(myObject.number);// 42
myObject.number=43;
console.log(myObject.number);// 42
Evitar Extensões
Se você quiser evitar que um objeto tenha novas propriedades adicionadas, mas deixando o resto das propriedades do objeto sozinhas, chame Object.preventExtensions(...):
let myObject ={
a:2,
};
Object.preventExtensions(myObject);
myObject.b=3;
myObject.b;// undefined
No modo não restrito, a criação de b falha silenciosamente. No modo estrito, ele lança um TypeError.
Proteção
Object.seal() cria um objeto "selado", o que significa que recebe um objeto existente e basicamente chama Object.preventExtensions() nele, mas também marca todas as suas propriedades existentes como configurable: false.
Então, não só você não pode adicionar mais propriedades, mas você também não pode reconfigurar ou apagar quaisquer propriedades existentes (embora você ainda possa modificar seus valores).
Travamento
Object.freeze() cria um objeto congelado, o que significa que é preciso um objeto existente e basicamente chama objeto.seal() nele, mas também marca todas as propriedades do tipo "acesso a dados" como writable:false, para que seus valores não possam ser alterados.
Essa abordagem é o maior nível de imutabilidade que você pode alcançar para um objeto em si, pois impede quaisquer mudanças no objeto ou em qualquer uma de suas propriedades diretas (embora, como mencionado acima, o conteúdo de quaisquer outros objetos referenciados não seja afetado).
let immutableObject =Object.freeze({});
Congelar um objeto não permite que novas propriedades sejam adicionadas a um objeto e impede que os usuários removam ou modifiquem as propriedades existentes. Object.freeze() preserva a enumerabilidade, configurabilidade, gravabilidade e protótipo do objeto. Ele retorna o objeto passado e não cria uma cópia congelada.
Quais são os prós e os contras da imutabilidade?
Prós
Detecção de mudanças mais fácil: A igualdade de objetos pode ser determinada de maneira eficiente e fácil por meio da igualdade referencial. Isso é útil para comparar diferenças de objetos no React e Redux.
Menos complicado: Programas com objetos imutáveis são menos complicados de se pensar, já que você não precisa se preocupar sobre como um objeto pode evoluir ao longo do tempo.
Compartilhamento fácil por referências: Uma cópia de um objeto é tão boa quanto outra, então você pode armazenar objetos em cache ou reutilizar o mesmo objeto várias vezes.
Seguro para threads: Objetos imutáveis podem ser usados com segurança entre threads em um ambiente multi-threaded, já que não há risco de serem modificados em outras threads que estão sendo executadas simultaneamente.
Menos necessidade de memória: Usando bibliotecas como Immer e Immutable.js, objetos são modificados usando compartilhamento estrutural e menos memória é necessária para ter vários objetos com estruturas semelhantes.
Não há necessidade de cópias defensivas: cópias defensivas não são mais necessárias quando objetos imutáveis são retornados ou passados para funções, uma vez que não há possibilidade de um objeto imutável ser modificado por ela.
Contras
Complexo para criar por si mesmo: Implementações ingênuas de estruturas de dados imutáveis e suas operações podem resultar em desempenho extremamente pobre porque novos objetos são criados cada vez. É recomendado o uso de bibliotecas para estruturas de dados imutáveis e operações eficientes que utilizam compartilhamento estrutural.
Potencial impacto negativo na performance: Alocação (e desalocação) de muitos objetos pequenos ao invés de modificar objetos existentes pode causar um impacto na performance. A complexidade do alocador ou do coletor de lixo geralmente depende do número de objetos no heap.
Complexidade para estruturas de dados cíclicas: estruturas de dados cíclicas como grafos são difíceis de construir. Se você tiver dois objetos que não podem ser modificados após a inicialização, como você pode fazer com que eles apontem um para o outro?
What is the difference between a `Map` object and a plain object in JavaScript?
Both Map objects and plain objects in JavaScript can store key-value pairs, but they have several key differences:
Feature
Map
Plain object
Key type
Any data type
String (or Symbol)
Key order
Maintained
Not guaranteed
Size property
Yes (size)
None
Iteration
forEach, keys(), values(), entries()
for...in, Object.keys(), etc.
Inheritance
No
Yes
Performance
Generally better for larger datasets and frequent additions/deletions
Faster for small datasets and simple operations
Serializable
No
Yes
Map vs plain JavaScript objects
In JavaScript, Map objects and a plain object (also known as a "POJO" or "plain old JavaScript object") are both used to store key-value pairs, but they have different characteristics, use cases, and behaviors.
Plain JavaScript objects (POJO)
A plain object is a basic JavaScript object created using the {} syntax. It is a collection of key-value pairs, where each key is a string (or a symbol, in modern JavaScript) and each value can be any type of value, including strings, numbers, booleans, arrays, objects, and more.
const person ={name:'John',age:30,occupation:'Developer'};
console.log(person);
Map objects
A Map object, introduced in ECMAScript 2015 (ES6), is a more advanced data structure that allows you to store key-value pairs with additional features. A Map is an iterable, which means you can use it with for...of loops, and it provides methods for common operations like get, set, has, and delete.
const person =newMap([
['name','John'],
['age',30],
['occupation','Developer'],
]);
console.log(person);
Key differences
Here are the main differences between a Map object and a plain object:
Key types: In a plain object, keys are always strings (or symbols). In a Map, keys can be any type of value, including objects, arrays, and even other Maps.
Key ordering: In a plain object, the order of keys is not guaranteed. In a Map, the order of keys is preserved, and you can iterate over them in the order they were inserted.
Iteration: A Map is iterable, which means you can use for...of loops to iterate over its key-value pairs. A plain object is not iterable by default, but you can use Object.keys() or Object.entries() to iterate over its properties.
Performance: Map objects are generally faster and more efficient than plain objects, especially when dealing with large datasets.
Methods: A Map object provides additional methods, such as get, set, has, and delete, which make it easier to work with key-value pairs.
Serialization: When serializing a Map object to JSON, it will be converted to an object but the existing Map properties might be lost in the conversion. A plain object, on the other hand, is serialized to a JSON object with the same structure.
When to use which
Use a plain object (POJO) when:
You need a simple, lightweight object with string keys.
You're working with a small dataset.
You need to serialize the object to JSON (e.g. to send over the network).
Use a Map object when:
You need to store key-value pairs with non-string keys (e.g., objects, arrays).
You need to preserve the order of key-value pairs.
You need to iterate over the key-value pairs in a specific order.
You're working with a large dataset and need better performance.
In summary, while both plain objects and Map objects can be used to store key-value pairs, Map objects offer more advanced features, better performance, and additional methods, making them a better choice for more complex use cases.
Notes
Map objects cannot be serialized to be sent in HTTP requests, but libraries like superjson allow them to be serialized and deserialized.
The primary difference between Map/Set and WeakMap/WeakSet in JavaScript lies in how they handle keys. Here's a breakdown:
Map vs. WeakMap
Maps allows any data type (strings, numbers, objects) as keys. The key-value pairs remain in memory as long as the Map object itself is referenced. Thus they are suitable for general-purpose key-value storage where you want to maintain references to both keys and values. Common use cases include storing user data, configuration settings, or relationships between objects.
WeakMaps only allows objects as keys. However, these object keys are held weakly. This means the garbage collector can remove them from memory even if the WeakMap itself still exists, as long as there are no other references to those objects. WeakMaps are ideal for scenarios where you want to associate data with objects without preventing those objects from being garbage collected. This can be useful for things like:
Caching data based on objects without preventing garbage collection of the objects themselves.
Storing private data associated with DOM nodes without affecting their lifecycle.
Set vs. WeakSet
Similar to Map, Sets allow any data type as keys. The elements within a Set must be unique. Sets are useful for storing unique values and checking for membership efficiently. Common use cases include removing duplicates from arrays or keeping track of completed tasks.
On the other hand, WeakSet only allows objects as elements, and these object elements are held weakly, similar to WeakMap keys. WeakSets are less commonly used, but applicable when you want a collection of unique objects without affecting their garbage collection. This might be necessary for:
Tracking DOM nodes that have been interacted with without affecting their memory management.
Implementing custom object weak references for specific use cases.
Here's a table summarizing the key differences:
Feature
Map
WeakMap
Set
WeakSet
Key Types
Any data type
Objects (weak references)
Any data type (unique)
Objects (weak references, unique)
Garbage Collection
Keys and values are not garbage collected
Keys can be garbage collected if not referenced elsewhere
Elements are not garbage collected
Elements can be garbage collected if not referenced elsewhere
Use Cases
General-purpose key-value storage
Caching, private DOM node data
Removing duplicates, membership checks
Object weak references, custom use cases
Choosing between them
Use Map and Set for most scenarios where you need to store key-value pairs or unique elements and want to maintain references to both the keys/elements and the values.
Use WeakMap and WeakSet cautiously in specific situations where you want to associate data with objects without affecting their garbage collection. Be aware of the implications of weak references and potential memory leaks if not used correctly.
Map/Set vs WeakMap/WeakSet
The key differences between Map/Set and WeakMap/WeakSet in JavaScript are:
Key types: Map and Set can have keys of any type (objects, primitive values, etc.), while WeakMap and WeakSet can only have objects as keys. Primitive values like strings or numbers are not allowed as keys in WeakMap and WeakSet.
Memory management: The main difference lies in how they handle memory. Map and Set have strong references to their keys and values, which means they will prevent garbage collection of those values. On the other hand, WeakMap and WeakSet have weak references to their keys (objects), allowing those objects to be garbage collected if there are no other strong references to them.
Key enumeration: Keys in Map and Set are enumerable (can be iterated over), while keys in WeakMap and WeakSet are not enumerable. This means you cannot get a list of keys or values from a WeakMap or WeakSet.
size property: Map and Set have a size property that returns the number of elements, while WeakMap and WeakSet do not have a size property because their size can change due to garbage collection.
Use cases: Map and Set are useful for general-purpose data structures and caching, while WeakMap and WeakSet are primarily used for storing metadata or additional data related to objects, without preventing those objects from being garbage collected.
Map and Set are regular data structures that maintain strong references to their keys and values, while WeakMap and WeakSet are designed for scenarios where you want to associate data with objects without preventing those objects from being garbage collected when they are no longer needed.
Use cases of WeakMap and WeakSet
Tracking active users
In a chat application, you might want to track which user objects are currently active without preventing garbage collection when the user logs out or the session expires. We use a WeakSet to track active user objects. When a user logs out or their session expires, the user object can be garbage-collected if there are no other references to it.
const activeUsers =newWeakSet();
// Function to mark a user as active
functionmarkUserActive(user){
activeUsers.add(user);
}
// Function to check if a user is active
functionisUserActive(user){
return activeUsers.has(user);
}
// Example usage
let user1 ={id:1,name:'Alice'};
let user2 ={id:2,name:'Bob'};
markUserActive(user1);
markUserActive(user2);
console.log(isUserActive(user1));// true
console.log(isUserActive(user2));// true
// Simulate user logging out
user1 =null;
// user1 is now eligible for garbage collection
console.log(isUserActive(user1));// false
Detecting circular references
WeakSet is provides a way of guarding against circular data structures by tracking which objects have already been processed.
Os membros estáticos de classe (propriedades/métodos) não estão ligados a uma instância específica de uma classe e têm o mesmo valor independentemente da instância que está se referindo a ele. Propriedades estáticas geralmente são variáveis de configuração e métodos estáticos são normalmente funções utilitárias puras que não dependem do estado da instância.
Symbols in JavaScript are a new primitive data type introduced in ES6 (ECMAScript 2015). They are unique and immutable identifiers that is primarily for object property keys to avoid name collisions. These values can be created using Symbol(...) function, and each Symbol value is guaranteed to be unique, even if they have the same key/description. Symbol properties are not enumerable in for...in loops or Object.keys(), making them suitable for creating private/internal object state.
let sym1 =Symbol();
let sym2 =Symbol('myKey');
console.log(typeof sym1);// "symbol"
console.log(sym1 === sym2);// false, because each symbol is unique
let obj ={};
let sym =Symbol('uniqueKey');
obj[sym]='value';
console.log(obj[sym]);// "value"
Note: The Symbol() function must be called without the new keyword. It is not exactly a constructor because it can only be called as a function instead of with new Symbol().
Symbols in JavaScript
Symbols in JavaScript are a unique and immutable data type used primarily for object property keys to avoid name collisions.
Key characteristics
Uniqueness: Each Symbol value is unique, even if they have the same description.
Immutability: Symbol values are immutable, meaning their value cannot be changed.
Non-enumerable: Symbol properties are not included in for...in loops or Object.keys().
Creating Symbols
Symbols can be created using the Symbol() function:
const sym1 =Symbol();
const sym2 =Symbol('uniqueKey');
console.log(typeof sym1);// "symbol"
console.log(sym1 === sym2);// false, because each symbol is unique
The Symbol(..) function must be called without the new keyword.
Using Symbols as object property keys
Symbols can be used to add properties to an object without risk of name collision:
const obj ={};
const sym =Symbol('uniqueKey');
obj[sym]='value';
console.log(obj[sym]);// "value"
Symbols are not enumerable
Symbol properties are not included in for...in loops or Object.keys().
This makes them suitable for creating private/internal object state.
Use Object.getOwnPropertySymbols(obj) to get all symbol properties on an object.
const mySymbol =Symbol('privateProperty');
const obj ={
name:'John',
[mySymbol]:42,
};
console.log(Object.keys(obj));// Output: ['name']
console.log(obj[mySymbol]);// Output: 42
Global Symbol registry
You can create global Symbols using Symbol.for('key'), which creates a new Symbol in the global registry if it doesn't exist, or returns the existing one. This allows you to reuse Symbols across different parts of your code base or even across different code bases.
const globalSym1 =Symbol.for('globalKey');
const globalSym2 =Symbol.for('globalKey');
console.log(globalSym1 === globalSym2);// true
const key =Symbol.keyFor(globalSym1);
console.log(key);// "globalKey"
Well-known Symbol
JavaScript includes several built-in Symbols, referred as well-known Symbols.
Symbol.iterator: Defines the default iterator for an object.
Symbol.toStringTag: Used to create a string description for an object.
Symbol.hasInstance: Used to determine if an object is an instance of a constructor.
Symbols are a powerful feature in JavaScript, especially useful for creating unique object properties and customizing object behavior. They provide a means to create hidden properties, preventing accidental access or modification, which is particularly beneficial in large-scale applications and libraries.
Server-sent events (SSE) is a standard that allows a web page to receive automatic updates from a server via an HTTP connection. Server-sent events are used with EventSource instances that opens a connection with a server and allows client to receive events from the server. Connections created by server-sent events are persistent (similar to the WebSockets), however there are a few differences:
Property
WebSocket
EventSource
Direction
Bi-directional – both client and server can exchange messages
Unidirectional – only server sends data
Data type
Binary and text data
Only text
Protocol
WebSocket protocol (ws://)
Regular HTTP (http://)
Creating an event source
const eventSource =newEventSource('/sse-stream');
Listening for events
// Fired when the connection is established.
eventSource.addEventListener('open',()=>{
console.log('Connection opened');
});
// Fired when a message is received from the server.
eventSource.addEventListener('message',(event)=>{
console.log('Received message:', event.data);
});
// Fired when an error occurs.
eventSource.addEventListener('error',(error)=>{
console.error('Error occurred:', error);
});
Sending events from server
const express =require('express');
const app =express();
app.get('/sse-stream',(req, res)=>{
// `Content-Type` need to be set to `text/event-stream`.
app.listen(3000,()=>console.log('Server started on port 3000'));
In this example, the server sends a "Hello from server" message initially, and then sends the current date every second. The connection is kept alive until the client closes it
Server-sent events (SSE)
Server-sent events (SSE) is a standard that allows a server to push updates to a web client over a single, long-lived HTTP connection. It enables real-time updates without the client having to constantly poll the server for new data.
How SSE works
The client creates a new EventSource object, passing the URL of the server-side script that will generate the event stream:
The server-side script sets the appropriate headers to indicate that it will be sending an event stream (Content-Type: text/event-stream), and then starts sending events to the client.
Each event sent by the server follows a specific format, with fields like event, data, and id. For example:
event: message
data:Hello, world!
event: update
id:123
data:{"temperature":25,"humidity":60}
On the client-side, the EventSource object receives these events and dispatches them as browser events, which can be handled using event listeners or the onmessage event handler:
The EventSource object automatically handles reconnection if the connection is lost, and it can resume the event stream from the last received event ID using the Last-Event-ID HTTP header.
SSE features
Unidirectional: Only the server can send data to the client. For bidirectional communication, web sockets would be more appropriate.
Retry mechanism: The client will retry the connection if it fails, with the retry interval specified by the retry: field from the server.
Text-only data: SSE can only transmit text data, which means binary data needs to be encoded (e.g., Base64) before transmission. This can lead to increased overhead and inefficiency for applications that need to transmit large binary payloads.
Built-in browser support: Supported by most modern browsers without additional libraries.
Event types: SSE supports custom event types using the event: field, allowing categorization of messages.
Last-Event-Id: The client sends the Last-Event-Id header when reconnecting, allowing the server to resume the stream from the last received event. However, there is no built-in mechanism to replay missed events during the disconnection period. You may need to implement a mechanism to handle missed events, such as using the Last-Event-Id header.
Connection limitations: Browsers have a limit on the maximum number of concurrent SSE connections, typically around 6 per domain. This can be a bottleneck if you need to establish multiple SSE connections from the same client. Using HTTP/2 will mitigate this issue.
Implementing SSE in JavaScript
The following code demonstrates a minimal implementation of SSE on the client and the server:
The server sets the appropriate headers to establish an SSE connection.
Messages are sent to the client every 5 seconds.
The server cleans up the interval and ends the response when the client disconnects.
On the client:
// Create a new EventSource object
const eventSource =newEventSource('/sse');
// Event listener for receiving messages
eventSource.onmessage=function(event){
console.log('New message:', event.data);
};
// Event listener for errors
eventSource.onerror=function(error){
console.error('Error occurred:', error);
};
// Optional: Event listener for open connection
eventSource.onopen=function(){
console.log('Connection opened');
};
On the server:
const http =require('http');
http
.createServer((req, res)=>{
if(req.url==='/sse'){
// Set headers for SSE
res.writeHead(200,{
'Content-Type':'text/event-stream',
'Cache-Control':'no-cache',
Connection:'keep-alive',
});
// Function to send a message
constsendMessage=(message)=>{
res.write(`data: ${message}\n\n`);// Messages are delimited with double line breaks.
Server-sent events provide an efficient and straightforward way to push updates from a server to a client in real-time. They are particularly well-suited for applications that require continuous data streams but do not need full bidirectional communication. With built-in support in modern browsers, SSE is a reliable choice for many real-time web applications.