Pular para o conteúdo

10. Padrões de JavaScript

Este documento detalha os padrões e práticas recomendados para escrever JavaScript moderno, limpo e eficiente em projetos Astro 5, com foco em código vanilla sem dependências desnecessárias.

Princípios Fundamentais

Código Limpo e Legível

Priorize a legibilidade e a manutenibilidade do código:

// ❌ Evite código denso e difícil de ler
const fn=a=>a.filter(x=>x>10).map(x=>x*2).reduce((a,b)=>a+b,0);
// ✅ Prefira código claro e legível
const processNumbers = (numbers) => {
const filtered = numbers.filter(num => num > 10);
const doubled = filtered.map(num => num * 2);
return doubled.reduce((sum, num) => sum + num, 0);
};

Modularidade

Divida seu código em módulos pequenos e focados:

src/utils/date.js
export function formatDate(date, locale = 'pt-BR') {
return new Date(date).toLocaleDateString(locale);
}
export function isToday(date) {
const today = new Date();
const target = new Date(date);
return (
target.getDate() === today.getDate() &&
target.getMonth() === today.getMonth() &&
target.getFullYear() === today.getFullYear()
);
}
// Uso em um componente
import { formatDate, isToday } from '../utils/date.js';
const publishDate = '2025-03-21';
const formattedDate = formatDate(publishDate);
const isPublishedToday = isToday(publishDate);

Imutabilidade

Prefira operações que não modificam os dados originais:

// ❌ Evite mutar arrays e objetos
function addItem(items, newItem) {
items.push(newItem); // Muta o array original
return items;
}
// ✅ Prefira operações imutáveis
function addItem(items, newItem) {
return [...items, newItem]; // Cria um novo array
}
// ❌ Evite mutar objetos
function updateUser(user, props) {
Object.assign(user, props); // Muta o objeto original
return user;
}
// ✅ Prefira operações imutáveis
function updateUser(user, props) {
return { ...user, ...props }; // Cria um novo objeto
}

Sintaxe Moderna

Destructuring

Use destructuring para extrair valores de objetos e arrays:

// Destructuring de objetos
const user = {
name: 'João',
email: 'joao@exemplo.com',
address: {
city: 'São Paulo',
country: 'Brasil'
}
};
// Extração básica
const { name, email } = user;
// Renomeando variáveis
const { name: userName, email: userEmail } = user;
// Valores padrão
const { age = 30 } = user;
// Destructuring aninhado
const { address: { city, country } } = user;
// Destructuring de arrays
const colors = ['vermelho', 'verde', 'azul'];
// Extração básica
const [primaryColor, secondaryColor] = colors;
// Ignorando elementos
const [, , tertiaryColor] = colors;
// Rest operator
const [first, ...rest] = colors;
// Valores padrão
const [main = 'preto', accent = 'branco'] = [];

Spread Operator

Use o spread operator para combinar arrays e objetos:

// Combinando arrays
const fruits = ['maçã', 'banana'];
const vegetables = ['cenoura', 'brócolis'];
const food = [...fruits, ...vegetables];
// Adicionando elementos
const extendedFruits = [...fruits, 'laranja', 'morango'];
// Clonando arrays
const fruitsCopy = [...fruits];
// Combinando objetos
const userBasic = { name: 'Maria', email: 'maria@exemplo.com' };
const userDetails = { age: 28, city: 'Rio de Janeiro' };
const userComplete = { ...userBasic, ...userDetails };
// Sobrescrevendo propriedades
const updatedUser = { ...userBasic, email: 'nova.maria@exemplo.com' };
// Clonando objetos
const userCopy = { ...userBasic };

Optional Chaining

Use optional chaining para acessar propriedades aninhadas com segurança:

const user = {
name: 'Pedro',
address: {
city: 'Belo Horizonte'
}
};
// ❌ Sem optional chaining (propenso a erros)
const country = user.address && user.address.country;
// ✅ Com optional chaining
const country = user.address?.country;
// Combinando com operador nullish coalescing
const country = user.address?.country ?? 'Brasil';
// Em arrays
const firstCategory = user.categories?.[0];
// Em funções
const result = user.getDetails?.();

Nullish Coalescing

Use o operador nullish coalescing para valores padrão:

// ❌ Operador OR pode causar problemas com valores falsy
const count = userCount || 10; // Se userCount for 0, count será 10
// ✅ Nullish coalescing só substitui null ou undefined
const count = userCount ?? 10; // Se userCount for 0, count será 0
// Combinando com destructuring
const { items = [], count = 0 } = data ?? {};

Funções

Arrow Functions

Use arrow functions para código mais conciso:

// Função tradicional
function add(a, b) {
return a + b;
}
// Arrow function
const add = (a, b) => a + b;
// Com corpo de bloco
const calculateTotal = (items) => {
const subtotal = items.reduce((sum, item) => sum + item.price, 0);
const tax = subtotal * 0.1;
return subtotal + tax;
};
// Em métodos de array
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(num => num * 2);
const even = numbers.filter(num => num % 2 === 0);

Parâmetros Padrão

Use parâmetros padrão para valores opcionais:

// ❌ Verificação manual de parâmetros
function createUser(name, role) {
role = role || 'user';
// ...
}
// ✅ Parâmetros padrão
function createUser(name, role = 'user') {
// ...
}
// Com destructuring
function fetchData({ url, method = 'GET', headers = {} } = {}) {
// ...
}

Rest Parameters

Use rest parameters para funções com número variável de argumentos:

// ❌ Usando arguments (não recomendado)
function sum() {
let total = 0;
for (let i = 0; i < arguments.length; i++) {
total += arguments[i];
}
return total;
}
// ✅ Usando rest parameters
function sum(...numbers) {
return numbers.reduce((total, num) => total + num, 0);
}
// Combinando com parâmetros regulares
function logItems(category, ...items) {
console.log(`Categoria: ${category}`);
items.forEach(item => console.log(`- ${item}`));
}

Padrões Assíncronos

Promises

Use Promises para operações assíncronas:

// Criando uma Promise
function fetchUserData(userId) {
return new Promise((resolve, reject) => {
// Operação assíncrona
fetch(`/api/users/${userId}`)
.then(response => {
if (!response.ok) {
throw new Error('Falha ao buscar usuário');
}
return response.json();
})
.then(data => resolve(data))
.catch(error => reject(error));
});
}
// Encadeamento de Promises
fetchUserData(123)
.then(user => fetchUserPosts(user.id))
.then(posts => {
// Processar posts
})
.catch(error => {
// Tratar erro
});
// Promise.all para operações paralelas
Promise.all([
fetchUserData(123),
fetchUserPosts(123),
fetchUserComments(123)
])
.then(([user, posts, comments]) => {
// Todos os dados disponíveis
})
.catch(error => {
// Se qualquer Promise falhar
});
// Promise.allSettled para operações independentes
Promise.allSettled([
fetchUserData(123),
fetchUserPosts(123),
fetchUserComments(123)
])
.then(results => {
// results contém status e valor/razão para cada Promise
});

Async/Await

Use async/await para código assíncrono mais legível:

// Função assíncrona básica
async function getUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error('Falha ao buscar usuário');
}
return await response.json();
} catch (error) {
console.error('Erro:', error);
throw error;
}
}
// Chamando função assíncrona
async function displayUserProfile(userId) {
try {
const user = await getUserData(userId);
const posts = await getUserPosts(userId);
// Renderizar perfil com user e posts
} catch (error) {
// Tratar erro
}
}
// Operações paralelas com async/await
async function getUserProfile(userId) {
try {
// Executa em paralelo
const [user, posts, comments] = await Promise.all([
getUserData(userId),
getUserPosts(userId),
getUserComments(userId)
]);
return { user, posts, comments };
} catch (error) {
console.error('Erro ao buscar perfil:', error);
throw error;
}
}

Padrões de Design

Module Pattern

Organize código relacionado em módulos:

src/utils/storage.js
const storage = (() => {
// Variáveis privadas
const PREFIX = 'app_';
// Métodos privados
function getKey(key) {
return `${PREFIX}${key}`;
}
// API pública
return {
get(key) {
const value = localStorage.getItem(getKey(key));
try {
return JSON.parse(value);
} catch {
return value;
}
},
set(key, value) {
const serialized = typeof value === 'object'
? JSON.stringify(value)
: value;
localStorage.setItem(getKey(key), serialized);
},
remove(key) {
localStorage.removeItem(getKey(key));
},
clear() {
Object.keys(localStorage)
.filter(key => key.startsWith(PREFIX))
.forEach(key => localStorage.removeItem(key));
}
};
})();
export default storage;

Factory Pattern

Crie objetos com uma interface consistente:

src/utils/api.js
function createApiClient(baseUrl, options = {}) {
const defaultHeaders = {
'Content-Type': 'application/json',
...options.headers
};
async function request(endpoint, method, data = null, customHeaders = {}) {
const url = `${baseUrl}${endpoint}`;
const headers = { ...defaultHeaders, ...customHeaders };
const config = {
method,
headers,
...options.fetchOptions
};
if (data) {
config.body = JSON.stringify(data);
}
try {
const response = await fetch(url, config);
if (!response.ok) {
throw new Error(`API error: ${response.status}`);
}
// Verifica se a resposta é JSON
const contentType = response.headers.get('content-type');
if (contentType && contentType.includes('application/json')) {
return await response.json();
}
return await response.text();
} catch (error) {
console.error('Request failed:', error);
throw error;
}
}
return {
get: (endpoint, customHeaders) =>
request(endpoint, 'GET', null, customHeaders),
post: (endpoint, data, customHeaders) =>
request(endpoint, 'POST', data, customHeaders),
put: (endpoint, data, customHeaders) =>
request(endpoint, 'PUT', data, customHeaders),
patch: (endpoint, data, customHeaders) =>
request(endpoint, 'PATCH', data, customHeaders),
delete: (endpoint, customHeaders) =>
request(endpoint, 'DELETE', null, customHeaders)
};
}
// Uso
const api = createApiClient('https://api.exemplo.com');
const users = await api.get('/users');
const newUser = await api.post('/users', { name: 'Ana', email: 'ana@exemplo.com' });

Observer Pattern

Implemente um sistema de eventos para comunicação entre componentes:

src/utils/eventBus.js
function createEventBus() {
const events = {};
return {
subscribe(event, callback) {
if (!events[event]) {
events[event] = [];
}
events[event].push(callback);
// Retorna função para cancelar inscrição
return () => {
events[event] = events[event].filter(cb => cb !== callback);
};
},
publish(event, data) {
if (!events[event]) {
return;
}
events[event].forEach(callback => {
callback(data);
});
},
clear(event) {
if (event) {
delete events[event];
} else {
Object.keys(events).forEach(key => {
delete events[key];
});
}
}
};
}
// Uso
const eventBus = createEventBus();
// Componente A
const unsubscribe = eventBus.subscribe('user:updated', (user) => {
console.log('Usuário atualizado:', user);
updateUI(user);
});
// Componente B
function updateUser(userId, data) {
api.put(`/users/${userId}`, data)
.then(updatedUser => {
eventBus.publish('user:updated', updatedUser);
});
}
// Cancelar inscrição quando não for mais necessário
unsubscribe();

Manipulação de DOM

Seletores Modernos

Use métodos modernos para selecionar elementos do DOM:

// Selecionar um único elemento
const header = document.querySelector('.header');
// Selecionar múltiplos elementos
const buttons = document.querySelectorAll('.btn');
// Verificar se um elemento corresponde a um seletor
const isActive = element.matches('.active');
// Encontrar o elemento pai mais próximo que corresponda a um seletor
const card = element.closest('.card');
// Selecionar elementos filhos
const items = container.querySelectorAll(':scope > .item');

Manipulação de Elementos

Métodos eficientes para manipular o DOM:

// Criar elementos
const button = document.createElement('button');
button.textContent = 'Clique aqui';
button.classList.add('btn', 'btn-primary');
// Adicionar atributos
button.setAttribute('data-id', '123');
button.id = 'submit-btn';
// Adicionar estilos
button.style.backgroundColor = '#0077ff';
button.style.padding = '8px 16px';
// Adicionar ao DOM
container.appendChild(button);
// Inserir em posição específica
container.insertBefore(button, container.firstChild);
// Métodos modernos de inserção
container.append(button); // Adiciona ao final
container.prepend(button); // Adiciona ao início
existingElement.before(button); // Insere antes
existingElement.after(button); // Insere depois
existingElement.replaceWith(button); // Substitui
// Remover elementos
button.remove();
// Clonar elementos
const clone = button.cloneNode(true); // true para clonar também os filhos

Manipulação de Classes

Trabalhe com classes de forma eficiente:

// Adicionar/remover classes
element.classList.add('active');
element.classList.remove('disabled');
element.classList.toggle('expanded');
element.classList.replace('old-class', 'new-class');
// Verificar se possui uma classe
if (element.classList.contains('selected')) {
// ...
}
// Adicionar várias classes
element.classList.add('card', 'shadow', 'rounded');

Delegação de Eventos

Use delegação de eventos para melhor performance:

// ❌ Adicionar eventos a múltiplos elementos (menos eficiente)
document.querySelectorAll('.btn').forEach(button => {
button.addEventListener('click', handleClick);
});
// ✅ Usar delegação de eventos (mais eficiente)
document.addEventListener('click', (event) => {
if (event.target.matches('.btn')) {
handleClick(event);
}
});
// Função de manipulação de eventos
function handleClick(event) {
const button = event.target;
const id = button.dataset.id;
// Lógica específica
console.log(`Botão ${id} clicado`);
}

Otimizações de Performance

Debounce e Throttle

Limite a frequência de execução de funções:

// Debounce: executa a função apenas após um período de inatividade
function debounce(func, delay = 300) {
let timeout;
return function(...args) {
clearTimeout(timeout);
timeout = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
// Throttle: limita a execução a uma vez a cada período
function throttle(func, limit = 300) {
let inThrottle;
return function(...args) {
if (!inThrottle) {
func.apply(this, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
};
}
// Uso com evento de scroll
const handleScroll = () => {
// Lógica de scroll
console.log('Scroll position:', window.scrollY);
};
// Versão com debounce (executa apenas quando o usuário para de rolar)
const debouncedScroll = debounce(handleScroll, 200);
// Versão com throttle (executa no máximo a cada 200ms durante rolagem)
const throttledScroll = throttle(handleScroll, 200);
window.addEventListener('scroll', throttledScroll);

Memoização

Cache de resultados de funções para melhor performance:

// Função de memoização simples
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
// Função custosa
function calculateFactorial(n) {
if (n <= 1) return 1;
return n * calculateFactorial(n - 1);
}
// Versão memoizada
const memoizedFactorial = memoize(calculateFactorial);
// Primeira chamada (calcula)
console.time('first call');
memoizedFactorial(20);
console.timeEnd('first call');
// Segunda chamada (usa cache)
console.time('second call');
memoizedFactorial(20);
console.timeEnd('second call');

Lazy Loading

Carregue recursos apenas quando necessário:

// Lazy loading de módulos
async function loadFeature() {
try {
// Carrega o módulo apenas quando necessário
const { initFeature } = await import('./features/special-feature.js');
// Inicializa o recurso
initFeature();
} catch (error) {
console.error('Falha ao carregar o recurso:', error);
}
}
// Botão que carrega o recurso
document.querySelector('#load-feature').addEventListener('click', loadFeature);
// Lazy loading de imagens com Intersection Observer
function setupLazyImages() {
const images = document.querySelectorAll('img[data-src]');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
img.removeAttribute('data-src');
observer.unobserve(img);
}
});
}, {
rootMargin: '100px 0px' // Carrega imagens 100px antes de entrarem na viewport
});
images.forEach(img => observer.observe(img));
}
// Inicializar lazy loading
document.addEventListener('DOMContentLoaded', setupLazyImages);

Integração com Astro

Scripts em Componentes Astro

Adicione JavaScript a componentes Astro:

src/components/Tabs.astro
---
const { tabs } = Astro.props;
---
<div class="tabs">
<div class="tabs-header">
{tabs.map((tab, index) => (
<button
class="tab-button"
data-tab={index}
aria-selected={index === 0 ? 'true' : 'false'}
>
{tab.label}
</button>
))}
</div>
<div class="tabs-content">
{tabs.map((tab, index) => (
<div
class="tab-panel"
data-tab-content={index}
hidden={index !== 0}
>
{tab.content}
</div>
))}
</div>
</div>
<script>
// Este script será hidratado automaticamente
function setupTabs() {
const tabsContainer = document.querySelector('.tabs');
if (!tabsContainer) return;
const buttons = tabsContainer.querySelectorAll('.tab-button');
const panels = tabsContainer.querySelectorAll('.tab-panel');
buttons.forEach(button => {
button.addEventListener('click', () => {
const tabIndex = button.dataset.tab;
// Atualiza os botões
buttons.forEach(btn => {
btn.setAttribute('aria-selected', btn === button ? 'true' : 'false');
});
// Atualiza os painéis
panels.forEach(panel => {
panel.hidden = panel.dataset.tabContent !== tabIndex;
});
});
});
}
// Executa quando o DOM estiver pronto
document.addEventListener('DOMContentLoaded', setupTabs);
// Também executa quando o documento é carregado via View Transitions
document.addEventListener('astro:page-load', setupTabs);
</script>

Comunicação entre Componentes

Use eventos personalizados para comunicação:

// Componente A dispara evento
function notifyUpdate(data) {
const event = new CustomEvent('data-updated', {
detail: data,
bubbles: true // Permite que o evento borbulhe para cima na árvore DOM
});
document.dispatchEvent(event);
}
// Componente B escuta o evento
function listenForUpdates() {
document.addEventListener('data-updated', (event) => {
const data = event.detail;
console.log('Dados atualizados:', data);
updateUI(data);
});
}
// Inicializar listener
listenForUpdates();

Persistência de Estado

Mantenha o estado entre navegações:

src/utils/state.js
export function createPersistentState(key, initialValue) {
// Tenta recuperar o estado do localStorage
let storedValue;
try {
const item = localStorage.getItem(key);
storedValue = item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error('Erro ao recuperar estado:', error);
storedValue = initialValue;
}
// Função para atualizar o estado
function setState(value) {
try {
// Suporta função para cálculo do novo estado
const newValue = typeof value === 'function'
? value(storedValue)
: value;
// Atualiza o valor em memória
storedValue = newValue;
// Persiste no localStorage
localStorage.setItem(key, JSON.stringify(newValue));
// Dispara evento de atualização
window.dispatchEvent(
new CustomEvent(`${key}:updated`, { detail: newValue })
);
return newValue;
} catch (error) {
console.error('Erro ao salvar estado:', error);
return storedValue;
}
}
// Função para observar mudanças
function subscribe(callback) {
const handler = (event) => callback(event.detail);
window.addEventListener(`${key}:updated`, handler);
// Retorna função para cancelar inscrição
return () => {
window.removeEventListener(`${key}:updated`, handler);
};
}
return {
get value() {
return storedValue;
},
set: setState,
subscribe
};
}
// Uso
const userPreferences = createPersistentState('userPreferences', {
theme: 'light',
fontSize: 'medium'
});
// Obter valor
console.log('Tema atual:', userPreferences.value.theme);
// Atualizar valor
userPreferences.set({
...userPreferences.value,
theme: 'dark'
});
// Observar mudanças
const unsubscribe = userPreferences.subscribe((newPrefs) => {
console.log('Preferências atualizadas:', newPrefs);
applyTheme(newPrefs.theme);
});

Conclusão

Os padrões e práticas de JavaScript apresentados neste documento fornecem uma base sólida para o desenvolvimento de soluções web modernas e eficientes com Astro 5. Ao seguir estas recomendações, você pode escrever código mais limpo, manutenível e performático, aproveitando ao máximo os recursos do JavaScript moderno sem depender de frameworks pesados.

Lembre-se de adaptar estes padrões às necessidades específicas do seu projeto e de manter-se atualizado com as melhores práticas da linguagem.

Os próximos documentos detalharão outros aspectos do desenvolvimento:


Última atualização: 21 de março de 2025