JavaScript Interview Questions

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:
Javascript coding
  • 280+ JavaScript coding questions
  • In-browser coding workspace similar to real interview environment
  • Reference solutions from Big Tech Ex-interviewers
  • Automated test cases
  • Instantly preview your code for UI questions
Get Started
Join 50,000+ engineers

Explique "hoisting"

Topics
JavaScript

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'
function foo() {
console.log('FOOOOO');
}
console.log(foo); // [Function: foo]
// Function Expression
console.log(bar); // undefined
bar(); // Uncaught TypeError: bar is not a function
var bar = 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`?

Topics
JavaScript

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).

function foo() {
// 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.

Qual é a diferença entre `==` e `===`?

Topics
JavaScript

== é 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.

var a = null;
console.log(a == null); // true
console.log(a == undefined); // true

O que é o loop de eventos?

Qual é a diferença entre call stack e task queue?
Topics
JavaScript

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.

Se você ainda não conferiu as palestras sobre o loop de eventos, você deveria. É um dos vídeos mais vistos de JavaScript.

Explique delegação do evento

Topics
Web APIsJavaScript

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

  1. 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.

  2. 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:

// HTML:
// <ul id="item-list">
// <li>Item 1</li>
// <li>Item 2</li>
// <li>Item 3</li>
// </ul>
const itemList = document.getElementById('item-list');
itemList.addEventListener('click', (event) => {
if (event.target.tagName === 'LI') {
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:

  1. Gerenciamento de listas, menus ou tabelas com muitos itens ou linhas.
  2. Manipulação de conteúdo dinâmico em aplicativos de página única.
  3. Simplificação do código, evitando a necessidade de anexar e remover ouvintes de eventos para elementos que mudam.

Recursos

Explique como `this` funciona em JavaScript

Topics
JavaScriptOOP

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:

  1. Se a palavra-chave new é usada ao chamar a função, new dentro da função é um objeto totalmente novo.
  2. Se apply, call, ou bind forem usados para chamar/criar uma função, this dentro da função é o objeto passado como o argumento.
  3. Se uma função é chamada como um método, como obj.method()this é o objeto do qual a função é uma propriedade.
  4. 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.
  5. Se se aplicarem múltiplas das regras acima, a regra que é maior ganha e definirá o valor this.
  6. 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.

Para uma explicação aprofundada, confira o artigo na Medium.

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`.

Topics
Web APIsJavaScript

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

PropriedadeCookielocalStoragesessionStorage
IniciadorCliente ou servidor. O servidor pode usar o cabeçalho Set-CookieClienteCliente
VencimentoDefinir manualmentePara sempreAo fechar a aba
Persistente através de sessões do navegadorDepende se a expiração está definidaSimNão
Enviado para o servidor com cada solicitação HTTPOs cookies são automaticamente enviados via cabeçalho CookieNãoNão
Capacidade (por domínio)4kb5MB5MB
AcessoQuqlquer JanelaQuqlquer JanelaMesma 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>`

Topics
HTMLJavaScript

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".

Referências

Qual é a diferença entre uma variável que é: `null`, `undefined` ou não declarada?

Como você checaria cada um destes estados?"
Topics
JavaScript

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.

function foo() {
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!
function bar() {}
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.

Qual é a diferença entre `.call` e `.apply`?

Topics
JavaScript

.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.

function add(a, b) {
return a + b;
}
console.log(add.call(null, 1, 2)); // 3
console.log(add.apply(null, [1, 2])); // 3

Explique `Function.prototype.bind`

Topics
JavaScriptOOP

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.

Source: Function.prototype.bind() - JavaScript | MDN

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 () {
return this.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.

Prática

Tente implementar seu próprio método Function.prototype.bind() na Great Front End.

Referências

Qual é a vantagem de usar a sintaxe de seta para um método em um constructor?

Topics
JavaScript

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:

const Person = function (firstName) {
this.firstName = firstName;
this.sayName1 = function () {
console.log(this.firstName);
};
this.sayName2 = () => {
console.log(this.firstName);
};
};
const john = new Person('John');
const dave = new Person('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)

Explique como funciona a herança de protótipos

Topics
JavaScriptOOP

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.
function Animal(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.
function Dog(name) {
Animal.call(this, name); // Chama o construtor pai.
}
// Set the child object's prototype to be the parent's prototype.
Object.setPrototypeOf(Dog.prototype, Animal.prototype);
// Adiciona um método ao protótipo do objeto filho.
Dog.prototype.bark = function () {
console.log('Woof!');
};
// Criar uma nova instância do Cachorro.
const bolt = new Dog('Bolt');
// Chama métodos no objeto filho.
console.log(bolt.name); // "Bolt"
bolt.makeSound(); // "O Dog faz um som."
bolt.bark(); // "Woof!"

As coisas a observar são:

  • .makeSound não está definido em Dog, então o navegador aumenta a cadeia de protótipos e encontra .makeSound para fora do Animal herdado.
  • Using Object.create to build the inheritance chain is no longer recommended. Use Object.setPrototypeOf instead.

Resources

Diferença entre: `function Person(){}`, `var person = Person()`, e `var person = new Person()`?

Topics
JavaScriptOOP

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).

function Person(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 = new Person('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() {}`

Topics
JavaScript

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'
function foo() {
console.log('FOOOOO');
}

Expressão de Função

foo(); // Uncaught TypeError: foo não é uma função
var foo = function () {
console.log('FOOOOO');
};

Qual é um caso típico de uso para funções anônimas?

Topics
JavaScript

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?

Topics
JavaScript

TL;DR

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.

const person = {
firstName: 'John',
lastName: 'Doe',
age: 50,
eyeColor: 'blue',
};
console.log(person); // {firstName: "John", lastName: "Doe", age: 50, eyeColor: "blue"}

Object() constructor

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.

const person = new Object();
person.firstName = 'John';
person.lastName = 'Doe';
console.log(person); // {firstName: "John", lastName: "Doe"}

Object.create() Method

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.

class Person {
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 = new Person('John', 30);
const person2 = new Person('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
function Person(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 = new Person('John', 30);
const person2 = new Person('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.

Further reading

O que é um closure e como/por que você usaria um?

Topics
ClosureJavaScript

Compreendendo Closures em JavaScript

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:

  1. 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.
  2. 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.
  3. 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:

const criarContador = () => {
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?

  1. 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.
  2. 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.
  3. 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.
  4. 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?

Topics
JavaScript

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.

const names = ['irish', 'daisy', 'anna'];

A maneira imperativa seria assim:

const transformNamesToUppercase = function (names) {
const results = [];
for (let i = 0; i < names.length; i++) {
results.push(names[i].toUpperCase());
}
return results;
};
transformNamesToUppercase(names); // ['IRISH', 'DAISY', 'ANNA']

Use .map(transformerFn) torna o código mais curto e mais declarativo.

const transformNamesToUppercase = function (names) {
return names.map((name) => name.toUpperCase());
};
transformNamesToUppercase(names); // ['IRISH', 'DAISY', 'ANNA']

Quais são as diferenças entre a classe ES2015 e os construtores de função ES5?

Topics
JavaScriptOOP

Vamos primeiro olhar o exemplo de cada um deles:

// Constructor de função ES5
function Person(name) {
this.name = name;
}
// ES2015 Class
class Person {
constructor(name) {
this.name = name;
}
}

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
function Student(name, studentId) {
// Chamar o construtor da superclasse para inicializar membros derivados da superclasse.
Person.call(this, name);
// Inicializar os próprios membros da subclasse.
this.studentId = studentId;
}
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;
// Classe ES2015
class Student extends Person {
constructor(name, studentId) {
super(name);
this.studentId = studentId;
}
}

É muito mais verboso usar a herança na ES5 e a versão ES2015 é mais fácil de entender e lembrar.

Descreva a propagação de eventos

Topics
Web APIsJavaScript

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.

Referências

Describe event capturing in JavaScript and browsers

Topics
Web APIsJavaScript

TL;DR

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.

Further reading

What is the difference between `mouseenter` and `mouseover` event in JavaScript and browsers?

Topics
Web APIsHTMLJavaScript

TL;DR

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.

Propertymouseentermouseover
BubblingNoYes
TriggerOnly when entering itselfWhen 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:

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Mouse Events Example</title>
<style>
.parent {
width: 200px;
height: 200px;
background-color: lightblue;
padding: 20px;
}
.child {
width: 100px;
height: 100px;
background-color: lightcoral;
}
</style>
</head>
<body>
<div class="parent">
Parent Element
<div class="child">Child Element</div>
</div>
<script>
const parent = document.querySelector('.parent');
const child = document.querySelector('.child');
// Mouseover event on parent.
parent.addEventListener('mouseover', () => {
console.log('Mouseover on parent');
});
// Mouseenter event on parent.
parent.addEventListener('mouseenter', () => {
console.log('Mouseenter on parent');
});
// Mouseover event on child.
child.addEventListener('mouseover', () => {
console.log('Mouseover on child');
});
// Mouseenter event on child.
child.addEventListener('mouseenter', () => {
console.log('Mouseenter on child');
});
</script>
</body>
</html>

Expected behavior

  • When the mouse enters the parent element:
    • The mouseover event on the parent will trigger.
    • The mouseenter event on the parent will trigger.
  • When the mouse enters the child element:
    • The mouseover event on the parent will trigger again because mouseover bubbles up from the child.
    • The mouseover event on the child will trigger.
    • The mouseenter event on the child will trigger.
    • The mouseenter event on the parent will not trigger again because mouseenter does not bubble.

Further reading

O que é `"use strict";`?

Quais são as vantagens e desvantagens de usa-lo?
Topics
JavaScript

'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

Topics
AsyncJavaScript

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?

Topics
AsyncJavaScript

Prós

  • Evite o "callback hell", que pode ser ilegível.
  • Facilita a escrita de código assíncrono sequencial que é legívell com .then().
  • Facilita a escrita paralela de código assíncrono com Promise.all().
  • Com promessas, esses cenários que estão presentes em código somente com retorno de chamada não ocorrerão:
    • Chamar a função de retorno de chamada muito cedo
    • Chamar a função de retorno de chamada tarde demais (ou nunca)
    • Chamar a função de retorno de chamada poucas ou muitas vezes
    • Falhar em passar qualquer ambiente/parâmetros necessários para a função de retorno de chamada
    • Ignorar quaisquer erros/exceções que possam ocorrer

Contras

  • O código pode se tornar ligeiramente mais complexo (isso pode ser discutível).
  • Em navegadores mais antigos onde ES2015 não é suportado, é necessário carregar um polyfill para poder usá-lo.

Explique o Ajax com o máximo de detalhes possível.

Topics
JavaScriptNetworking

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?

Topics
JavaScriptNetworking

Vantagens

  • 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?

Topics
JavaScriptNetworking

TL;DR

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:

const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://jsonplaceholder.typicode.com/todos/1', true);
xhr.responseType = 'json';
xhr.onload = function () {
if (xhr.status === 200) {
console.log(xhr.response);
}
};
xhr.send();

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:

fetch('https://jsonplaceholder.typicode.com/todos/1')
.then((response) => response.text())
.then((data) => console.log(data));

Request headers

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:

xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('Authorization', 'Bearer YOUR_TOKEN');

For fetch(), headers are passed as an object in the second argument to fetch():

fetch('https://jsonplaceholder.typicode.com/todos/1', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer YOUR_TOKEN',
},
body: JSON.stringify({
name: 'John Doe',
age: 30,
}),
});

Request body

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:

const xhr = new XMLHttpRequest();
xhr.open('POST', 'https://jsonplaceholder.typicode.com/todos/1', true);
xhr.send(
JSON.stringify({
name: 'John Doe',
age: 30,
}),
);

fetch() supports sending request bodies using the body property in the second argument to fetch():

fetch('https://jsonplaceholder.typicode.com/todos/1', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'John Doe',
age: 30,
}),
});

Response handling

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'.

const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://jsonplaceholder.typicode.com/todos/1', true);
xhr.responseType = 'json'; // or 'text', 'blob', 'arraybuffer'
xhr.onload = function () {
if (xhr.status === 200) {
console.log(xhr.response);
}
};
xhr.send();

On the other hand, fetch() provides a unified Response object with then method for accessing data.

// JSON data
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then((response) => response.json())
.then((data) => console.log(data));
// Text data
fetch('https://jsonplaceholder.typicode.com/todos/1')
.then((response) => response.text())
.then((data) => console.log(data));

Error handling

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 = new XMLHttpRequest();
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
.then((response) => response.json())
.then((data) => console.log(data))
.catch((error) => console.error('Error occurred: ' + error));

Caching control

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 = await fetch('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:

const xhr = new XMLHttpRequest();
xhr.open('GET', 'https://jsonplaceholder.typicode.com/todos/1');
xhr.send();
// ...
xhr.onabort = () => console.log('aborted');
xhr.abort();

Aborting a fetch() requires creating an AbortController object and passing it to as the signal property of the options object when calling fetch().

const controller = new AbortController();
const signal = controller.signal;
fetch('https://jsonplaceholder.typicode.com/todos/1', { signal })
.then((response) => response.json())
.then((data) => console.log(data))
.catch((error) => console.error('Error occurred: ' + error));
// Abort request.
controller.abort();

Progress support

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.

const xhr = new XMLHttpRequest();
// The callback is passed a `ProgressEvent`.
xhr.upload.onprogress = (event) => {
console.log(Math.round((event.loaded / event.total) * 100) + '%');
};

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.

Further reading

How do you abort a web request using `AbortController` in JavaScript?

Topics
JavaScriptNetworking

TL;DR

AbortController is used to cancel ongoing asynchronous operations like fetch requests.

const controller = new AbortController();
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:

  1. Create an AbortController instance: Initialize an AbortController instance, which creates a signal that can be used to abort requests.
  2. Pass the signal to the request: Pass the signal to the request, typically through the signal property in the request options.
  3. 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 = new AbortController();
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).

// HTML: <button id='cancel-button'>Cancel upload</button>
const btn = document.createElement('button');
btn.id = 'cancel-button';
btn.innerHTML = 'Cancel upload';
document.body.appendChild(btn);
const controller = new AbortController();
const signal = controller.signal;
fetch('https://jsonplaceholder.typicode.com/todos/1', { signal })
.then((response) => {
// Handle successful response
})
.catch((error) => {
if (error.name === 'AbortError') {
console.log('Request canceled');
} else {
console.error('Network or other error:', error);
}
});
document.getElementById('cancel-button').addEventListener('click', () => {
controller.abort();
});
document.getElementById('cancel-button').click(); // Simulate clicking the cancel button

When you click the "Cancel upload" button, in-flight request will be aborted.

Prioritizing latest requests in a race condition

In scenarios where multiple requests are initiated for the same data, use AbortController to prioritize the latest request and abort earlier ones.

let latestController = null; // Keeps track of the latest controller
function fetchData(url) {
if (latestController) {
latestController.abort(); // Abort any previous request
}
const controller = new AbortController();
latestController = controller;
const signal = controller.signal;
fetch(url, { signal })
.then((response) => response.json())
.then((data) => console.log('Fetched data:', data))
.catch((error) => {
if (error.name === 'AbortError') {
console.log('Request canceled');
} else {
console.error('Network or other error:', error);
}
});
}
fetchData('https://jsonplaceholder.typicode.com/posts/1');
// Simulate race conditions with new requests that quickly cancel the previous one
setTimeout(() => {
fetchData('https://jsonplaceholder.typicode.com/posts/2');
}, 5);
setTimeout(() => {
fetchData('https://jsonplaceholder.typicode.com/posts/3');
}, 5);
// 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.

Further reading

What are JavaScript polyfills for?

Topics
JavaScript

TL;DR

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.

    <script src="https://polyfill.io/v3/polyfill.min.js"></script>

Polyfills in JavaScript

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) {
return true;
}
}
return false;
};
}
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

  1. 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.
  2. Write the fallback implementation: Develop the fallback implementation that provides similar functionality, either using a pre-existing polyfill library or pure JavaScript code.
  3. Test the polyfill: Thoroughly test the polyfill to ensure it functions as intended across different contexts and browsers.
  4. 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.

    <script src="https://polyfill.io/v3/polyfill.min.js"></script>

Further reading

Por que estender objetos JavaScript nativos não é uma boa ideia?

Topics
JavaScriptOOP

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?

Topics
JavaScript

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

Topics
JavaScript

TL;DR

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
export const value = 42;
// main.js
import { value } from './my-module.js';
console.log(value); // 42

CommonJS vs ES modules

FeatureCommonJSES modules
Module Syntaxrequire() for importing module.exports for exportingimport for importing export for exporting
EnvironmentPrimarily used in Node.js for server-side developmentDesigned for both browser and server-side JavaScript (Node.js)
LoadingSynchronous loading of modulesAsynchronous loading of modules
StructureDynamic imports, can be conditionally calledStatic imports/exports at the top level
File extensions.js (default).mjs or .js (with type: "module" in package.json)
Browser supportNot natively supported in browsersNatively supported in modern browsers
OptimizationLimited optimization due to dynamic natureAllows for optimizations like tree-shaking due to static structure
CompatibilityWidely used in existing Node.js codebases and librariesNewer 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
export const 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.

Further reading

What are the various data types in JavaScript?

Topics
JavaScript

TL;DR

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

  1. 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
  1. 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!"
  1. Boolean: Represents logical entities and can have two values: true or false.
let isActive = true;
let isOver18 = false;
console.log(isOver18); // false
  1. Undefined: A variable that has been declared but not assigned a value is of type undefined.
let user;
console.log(user); // undefined
  1. 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');
}
  1. 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)
  1. BigInt: Used for representing integers with arbitrary precision, useful for working with very large numbers.
let bigNumber = BigInt(9007199254740991);
let anotherBigNumber = 1234567890123456789012345678901234567890n;
console.log(bigNumber); // 9007199254740991n
console.log(anotherBigNumber); // 1234567890123456789012345678901234567890n

Non-primitive (reference) data types

  1. 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}
  1. 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);
  1. Function: Functions in JavaScript are objects. They can be defined using function declarations or expressions.
function greet() {
console.log('Hello!');
}
let add = function (a, b) {
return a + b;
};
greet(); // "Hello!"
console.log(add(2, 3)); // 5
  1. Date: Represents dates and times. The Date object is used to work with dates.
let today = new Date().toLocaleTimeString();
console.log(today);
  1. 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
  1. Map: A collection of keyed data items, similar to an object but allows keys of any type.
let map = new Map();
map.set('key1', 'value1');
console.log(map);
  1. Set: A collection of unique values.
let set = new Set();
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(typeof 42); // "number"
console.log(typeof 'hello'); // "string"
console.log(typeof true); // "boolean"
console.log(typeof undefined); // "undefined"
console.log(typeof null); // "object" (this is a historical bug in JavaScript)
console.log(typeof Symbol()); // "symbol"
console.log(typeof BigInt(123)); // "bigint"
console.log(typeof {}); // "object"
console.log(typeof []); // "object"
console.log(typeof function () {}); // "function"

Pitfalls

Type coercion

JavaScript often performs type coercion, converting values from one type to another, which can lead to unexpected results.

let result = '5' + 2;
console.log(result, typeof result); // "52 string" (string concatenation)
let difference = '5' - 2;
console.log(difference, typeof difference); // 3 "number" (numeric subtraction)

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.

Further reading

Que construções de idioma você usa para iterar sobre propriedades de objeto e itens de matriz?

Topics
JavaScript

Objetos

declaração for...in

for (const property in obj) {
console.log(property);
}

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.

Referência: Object.keys() - JavaScript | MDN

Object.getOwnPropertyNames()

Object.getOwnPropertyNames(obj).forEach((property) => {
console.log(property);
});

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.

Referência: Object.keys() - JavaScript | MDN

Arrays

for loop

for (let i = 0; i < arr.length; i++) {
console.log(arr[i]);
}

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.

Referência: Object.keys() - JavaScript | MDN

declaração for...of

for (let element of arr) {
console.log(element);
}

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():

const arr = ['a', 'b', 'c'];
for (let [index, elem] of arr.entries()) {
console.log(index, ': ', elem);
}

Referência: for...of - JavaScript | MDN

Quais são os benefícios de usar a sintaxe de spread e como ela é diferente da sintaxe de rest?

Topics
JavaScript

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.

function putDookieInAnyArray(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.

function addFiveToABunchOfNumbers(...numbers) {
return numbers.map((x) => x + 5);
}
const result = addFiveToABunchOfNumbers(4, 5, 6, 7, 8, 9, 10); // [9, 10, 11, 12, 13, 14, 15]
const [a, b, ...rest] = [1, 2, 3, 4]; // a: 1, b: 2, rest: [3, 4]
const { e, f, ...others } = {
e: 1,
f: 2,
g: 3,
h: 4,
}; // e: 1, f: 2, others: { g: 3, h: 4 }

What are iterators and generators in JavaScript and what are they used for?

Topics
JavaScript

TL;DR

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.

const iterator = {
current: 0,
last: 5,
next() {
if (this.current <= this.last) {
return { value: this.current++, done: false };
} else {
return { value: undefined, done: true };
}
},
};
let result = iterator.next();
while (!result.done) {
console.log(result.value); // Logs 0, 1, 2, 3, 4, 5
result = iterator.next();
}

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().

function* numberGenerator() {
let num = 0;
while (num <= 5) {
yield num++;
}
}
const gen = numberGenerator();
console.log(gen.next()); // { value: 0, done: false }
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: 4, done: false }
console.log(gen.next()); // { value: 5, done: false }
console.log(gen.next()); // { value: undefined, done: true }

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
class Range {
// 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 = new Range(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:

  1. Arrays: Arrays have a built-in iterator that allows you to iterate over their elements.

    const array = [1, 2, 3];
    const iterator = array[Symbol.iterator]();
    console.log(iterator.next()); // { value: 1, done: false }
    console.log(iterator.next()); // { value: 2, done: false }
    console.log(iterator.next()); // { value: 3, done: false }
    console.log(iterator.next()); // { value: undefined, done: true }
    for (const value of array) {
    console.log(value); // Logs 1, 2, 3
    }
  2. Strings: Strings have a built-in iterator that allows you to iterate over their characters.

    const string = 'hello';
    const iterator = string[Symbol.iterator]();
    console.log(iterator.next()); // { value: "h", done: false }
    console.log(iterator.next()); // { value: "e", done: false }
    console.log(iterator.next()); // { value: "l", done: false }
    console.log(iterator.next()); // { value: "l", done: false }
    console.log(iterator.next()); // { value: "o", done: false }
    console.log(iterator.next()); // { value: undefined, done: true }
    for (const char of string) {
    console.log(char); // Logs h, e, l, l, o
    }
  3. DOM NodeLists

    // Create a new div and append it to the DOM
    const newDiv = document.createElement('div');
    newDiv.id = 'div1';
    document.body.appendChild(newDiv);
    const nodeList = document.querySelectorAll('div');
    const iterator = nodeList[Symbol.iterator]();
    console.log(iterator.next()); // { value: HTMLDivElement, done: false }
    console.log(iterator.next()); // { value: undefined, done: true }
    for (const node of nodeList) {
    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
class Range {
// 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 = new Range(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:

async function* fetchDataInBatches(url, numBatches = 5, batchSize = 10) {
let startIndex = 0;
let currBatch = 0;
while (currBatch < numBatches) {
const response = await fetch(
`${url}?_start=${startIndex}&_limit=${batchSize}`,
);
const data = await response.json();
if (data.length === 0) break;
yield data;
startIndex += batchSize;
currBatch += 1;
}
}
async function fetchAndLogData() {
const dataGenerator = fetchDataInBatches(
'https://jsonplaceholder.typicode.com/todos',
);
for await (const batch of dataGenerator) {
console.log(batch);
}
}
fetchAndLogData();

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:

async function* fetchDataAsyncIterator(url, pagesToFetch = 3) {
let currPage = 1;
while (currPage <= pagesToFetch) {
const response = await fetch(`${url}?_page=${currPage}`);
const data = await response.json();
if (data.length === 0) break;
yield data;
currPage++;
}
}
async function fetchAndLogData() {
const asyncIterator = fetchDataAsyncIterator(
'https://jsonplaceholder.typicode.com/todos',
);
for await (const chunk of asyncIterator) {
console.log(chunk);
}
}
fetchAndLogData();

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.

Further reading

Explique a diferença entre objetos mutáveis e imutáveis

Topics
JavaScript

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?

Topics
JavaScript

TL;DR

Both Map objects and plain objects in JavaScript can store key-value pairs, but they have several key differences:

FeatureMapPlain object
Key typeAny data typeString (or Symbol)
Key orderMaintainedNot guaranteed
Size propertyYes (size)None
IterationforEach, keys(), values(), entries()for...in, Object.keys(), etc.
InheritanceNoYes
PerformanceGenerally better for larger datasets and frequent additions/deletionsFaster for small datasets and simple operations
SerializableNoYes

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 = new Map([
['name', 'John'],
['age', 30],
['occupation', 'Developer'],
]);
console.log(person);

Key differences

Here are the main differences between a Map object and a plain object:

  1. 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.
  2. 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.
  3. 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.
  4. Performance: Map objects are generally faster and more efficient than plain objects, especially when dealing with large datasets.
  5. Methods: A Map object provides additional methods, such as get, set, has, and delete, which make it easier to work with key-value pairs.
  6. 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.

Further reading

What are the differences between `Map`/`Set` and `WeakMap`/`WeakSet` in JavaScript?

Topics
JavaScript

TL;DR

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:

FeatureMapWeakMapSetWeakSet
Key TypesAny data typeObjects (weak references)Any data type (unique)Objects (weak references, unique)
Garbage CollectionKeys and values are not garbage collectedKeys can be garbage collected if not referenced elsewhereElements are not garbage collectedElements can be garbage collected if not referenced elsewhere
Use CasesGeneral-purpose key-value storageCaching, private DOM node dataRemoving duplicates, membership checksObject 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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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 = new WeakSet();
// Function to mark a user as active
function markUserActive(user) {
activeUsers.add(user);
}
// Function to check if a user is active
function isUserActive(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.

// Create a WeakSet to track visited objects
const visited = new WeakSet();
// Function to traverse an object recursively
function traverse(obj) {
// Check if the object has already been visited
if (visited.has(obj)) {
return;
}
// Add the object to the visited set
visited.add(obj);
// Traverse the object's properties
for (let prop in obj) {
if (obj.hasOwnProperty(prop)) {
let value = obj[prop];
if (typeof value === 'object' && value !== null) {
traverse(value);
}
}
}
// Process the object
console.log(obj);
}
// Create an object with a circular reference
const obj = {
name: 'John',
age: 30,
friends: [
{ name: 'Alice', age: 25 },
{ name: 'Bob', age: 28 },
],
};
// Create a circular reference
obj.self = obj;
// Traverse the object
traverse(obj);

Further reading

Por que você pode querer criar membros de classe estáticos?

Topics
JavaScriptOOP

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.

What are `Symbol`s used for in JavaScript?

Topics
JavaScript

TL;DR

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.

Symbol.iterator

let iterable = {
[Symbol.iterator]() {
let step = 0;
return {
next() {
step++;
if (step <= 5) {
return { value: step, done: false };
}
return { done: true };
},
};
},
};
for (let value of iterable) {
console.log(value); // 1, 2, 3, 4, 5
}

Symbol.toStringTag

let myObj = {
[Symbol.toStringTag]: 'MyCustomObject',
};
console.log(Object.prototype.toString.call(myObj)); // "[object MyCustomObject]"

Summary

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.

Further reading

What are server-sent events?

Topics
JavaScriptNetworking

TL;DR

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:

PropertyWebSocketEventSource
DirectionBi-directional – both client and server can exchange messagesUnidirectional – only server sends data
Data typeBinary and text dataOnly text
ProtocolWebSocket protocol (ws://)Regular HTTP (http://)

Creating an event source

const eventSource = new EventSource('/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`.
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// Each message should be prefixed with data.
const sendEvent = (data) => res.write(`data: ${data}\n\n`);
sendEvent('Hello from server');
const intervalId = setInterval(() => sendEvent(new Date().toString()), 1000);
res.on('close', () => {
console.log('Client closed connection');
clearInterval(intervalId);
});
});
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

  1. The client creates a new EventSource object, passing the URL of the server-side script that will generate the event stream:

    const eventSource = new EventSource('/event-stream');
  2. 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.

  3. 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}
  4. 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:

    eventSource.onmessage = function (event) {
    console.log('Received message:', event.data);
    };
    eventSource.addEventListener('update', function (event) {
    console.log('Received update:', JSON.parse(event.data));
    });
  5. 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 = new EventSource('/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
const sendMessage = (message) => {
res.write(`data: ${message}\n\n`); // Messages are delimited with double line breaks.
};
// Send a message every 5 seconds
const intervalId = setInterval(() => {
sendMessage(`Current time: ${new Date().toLocaleTimeString()}`);
}, 5000);
// Handle client disconnect
req.on('close', () => {
clearInterval(intervalId);
res.end();
});
} else {
res.writeHead(404);
res.end();
}
})
.listen(8080, () => {
console.log('SSE server running on port 8080');
});

Summary

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.

Further reading

Explique "hoisting"