GUIA COMPLETO: JAVASCRIPT DO BÁSICO AO
AVANÇADO
Parte 1: Fundamentos da Linguagem | Parte 2: Algoritmos para
Entrevistas
ESTRUTURA GERAL DO GUIA
Baseado na Pesquisa Profunda de:
JavaScript Multiparadigma - OOP, Funcional, Imperativo, Event-Driven
SOLID Principles em JavaScript e TypeScript
Programação Funcional vs OOP vs Imperativo - Vantagens e quando usar
TypeScript vs JavaScript - Tipagem estática, vantagens, desvantagens
Coding Interview University + FAANG - Algoritmos para entrevistas
Filosofia de Aprendizado:
Entendimento Profundo: Como JavaScript funciona internamente
Paradigmas Comparados: Exemplos práticos de cada abordagem
Qualidade de Código: SOLID, clean code, melhores práticas
Preparação Técnica: Da base sólida aos algoritmos FAANG
Divisão de Conteúdo:
PARTE 1 (60%): FUNDAMENTOS JAVASCRIPT PROFUNDO
Como JavaScript funciona como linguagem
Paradigmas: OOP vs Funcional vs Imperativo
SOLID principles aplicados
TypeScript vs JavaScript - quando usar
PARTE 2 (40%): ALGORITMOS PARA ENTREVISTAS
Estruturas de dados em JavaScript
Padrões LeetCode essenciais
Problemas FAANG reais
PARTE 1: FUNDAMENTOS JAVASCRIPT
PROFUNDO
Objetivo: Dominar JavaScript como linguagem multiparadigma
1.1 O QUE É JAVASCRIPT COMO LINGUAGEM
Entendimento profundo sobre como JavaScript funciona internamente
1.1.1 JavaScript: História e Evolução - A Linguagem Multiparadigma
JavaScript não nasceu com a complexidade que possui hoje. Sua evolução de uma linguagem de script simples
para uma das mais versáteis do mundo é uma história fascinante que todo desenvolvedor deve conhecer.
Origem e Criação (1995) - Os 10 Dias que Mudaram a Web
Em maio de 1995, Brendan Eich estava trabalhando na Netscape Communications quando recebeu uma tarefa
aparentemente simples: criar uma linguagem de script para o navegador Netscape Navigator. O que ele não
sabia era que estava prestes a criar uma das linguagens de programação mais influentes da história.
O Contexto Histórico:
// Em 1995, a web era assim:
// - Páginas estáticas HTML
// - Sem interatividade
// - Java era complexo demais para scripts simples
// - A Netscape precisava de algo "mais simples que Java"
// A primeira função JavaScript ever written (conceitual):
function makePageDynamic() {
// Brendan Eich queria algo que pudesse:
// 1. Ser fácil para designers web
// 2. Complementar Java (daí o nome "JavaScript")
// 3. Rodar diretamente no navegador
document.write("Hello World!");
}
As 10 Dias Históricos: Brendan Eich tinha apenas 10 dias para criar uma linguagem completa. Suas
influências principais foram:
1. Scheme (Funcional): Funções como first-class citizens, closures
2. Self (Protótipo): Sistema de herança baseado em protótipos
3. Java (Sintaxe): Sintaxe familiar com C/C++/Java
4. Perl (Strings): Manipulação poderosa de strings
// Herança do Scheme - Funções são valores
const multiply = function(x) {
return function(y) {
return x * y;
};
};
const double = multiply(2);
console.log(double(5)); // 10
// Herança do Self - Protótipos
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
return `${this.name} makes a sound`;
};
function Dog(name) {
Animal.call(this, name);
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.speak = function() {
return `${this.name} barks`;
};
// Herança do Java - Sintaxe familiar
class ModernAnimal {
constructor(name) {
this.name = name;
}
speak() {
return `${this.name} makes a sound`;
}
}
A Evolução através das Versões ECMAScript
JavaScript é padronizado pelo ECMAScript (ES). Cada versão trouxe features importantes:
ES1 (1997) - A Base:
// Funcionalidades básicas que ainda usamos
var name = "JavaScript";
function greet() {
return "Hello, " + name + "!";
}
if (name) {
console.log(greet());
}
// Arrays básicos
var numbers = [1, 2, 3];
for (var i = 0; i < numbers.length; i++) {
console.log(numbers[i]);
}
ES3 (1999) - Expressões Regulares e Try/Catch:
// RegExp - revolucionou validação
function validateEmail(email) {
var regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
}
// Try/Catch - tratamento de erros
function divideNumbers(a, b) {
try {
if (b === 0) {
throw new Error("Division by zero not allowed");
}
return a / b;
} catch (error) {
console.error("Error:", error.message);
return null;
}
}
console.log(validateEmail("[email protected]")); // true
console.log(divideNumbers(10, 2)); // 5
console.log(divideNumbers(10, 0)); // null (with error message)
ES5 (2009) - JavaScript Moderno Nasce:
// Strict mode - código mais seguro
"use strict";
// Array methods que revolucionaram a linguagem
var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Programação funcional chegou ao JavaScript
var evenNumbers = numbers.filter(function(num) {
return num % 2 === 0;
});
var doubled = evenNumbers.map(function(num) {
return num * 2;
});
var sum = doubled.reduce(function(acc, num) {
return acc + num;
}, 0);
console.log("Even numbers:", evenNumbers); // [2, 4, 6, 8, 10]
console.log("Doubled:", doubled); // [4, 8, 12, 16, 20]
console.log("Sum:", sum); // 60
// JSON nativo
var userData = {
name: "John",
age: 30,
active: true
};
var jsonString = JSON.stringify(userData);
var parsedData = JSON.parse(jsonString);
// Object methods
var person = {
firstName: "John",
lastName: "Doe"
};
Object.defineProperty(person, 'fullName', {
get: function() {
return this.firstName + ' ' + this.lastName;
},
set: function(value) {
var parts = value.split(' ');
this.firstName = parts[0];
this.lastName = parts[1];
}
});
console.log(person.fullName); // "John Doe"
person.fullName = "Jane Smith";
console.log(person.firstName); // "Jane"
ES6/ES2015 - A Revolução:
// Classes - sintaxe mais familiar
class Vehicle {
constructor(brand, model) {
this.brand = brand;
this.model = model;
}
getInfo() {
return `${this.brand} ${this.model}`;
}
}
class Car extends Vehicle {
constructor(brand, model, doors) {
super(brand, model);
this.doors = doors;
}
getInfo() {
return `${super.getInfo()} - ${this.doors} doors`;
}
}
// Arrow functions - sintaxe concisa
const numbers = [1, 2, 3, 4, 5];
const squares = numbers.map(n => n * n);
const evenSquares = squares.filter(n => n % 2 === 0);
console.log(evenSquares); // [4, 16]
// Destructuring - extração elegante
const user = {
name: "John",
age: 30,
address: {
street: "123 Main St",
city: "New York"
}
};
const { name, age, address: { city } } = user;
console.log(`${name}, ${age}, lives in ${city}`);
// Template literals - strings poderosas
const message = `
Hello ${name}!
You are ${age} years old.
Welcome to our platform.
`;
// Spread operator - manipulação de arrays/objects
const moreNumbers = [...numbers, 6, 7, 8];
const updatedUser = { ...user, email: "
[email protected]" };
// Promises - programação assíncrona
function fetchUserData(userId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (userId > 0) {
resolve({ id: userId, name: `User ${userId}` });
} else {
reject(new Error("Invalid user ID"));
}
}, 1000);
});
}
fetchUserData(123)
.then(user => console.log("User found:", user))
.catch(error => console.error("Error:", error.message));
// Modules - organização do código
// math.js
export const PI = 3.14159;
export function area(radius) {
return PI * radius * radius;
}
// app.js
// import { PI, area } from './math.js';
// console.log(`Circle area: ${area(5)}`);
ES2017 - Async/Await:
// Programação assíncrona mais legível
async function getUserData(userId) {
try {
const user = await fetchUserData(userId);
const profile = await fetchUserProfile(user.id);
const permissions = await fetchUserPermissions(user.id);
return {
user,
profile,
permissions
};
} catch (error) {
console.error("Failed to fetch user data:", error);
return null;
}
}
// Uso mais natural
async function displayUser(userId) {
const userData = await getUserData(userId);
if (userData) {
console.log("User loaded:", userData.user.name);
}
}
ES2020+ - Features Modernas:
// Optional chaining - acesso seguro
const user = {
profile: {
social: {
twitter: "@johndoe"
}
}
};
console.log(user?.profile?.social?.twitter); // "@johndoe"
console.log(user?.profile?.social?.facebook); // undefined (sem erro)
// Nullish coalescing - valores padrão inteligentes
const config = {
timeout: 0,
retries: null,
debug: false
};
const timeout = config.timeout ?? 5000; // 0 (não 5000, porque 0 é válido)
const retries = config.retries ?? 3; // 3 (porque null é nullish)
const debug = config.debug ?? true; // false (não true, porque false é válido)
// BigInt - números grandes
const largeNumber = 1234567890123456789012345678901234567890n;
console.log(typeof largeNumber); // "bigint"
// Private fields em classes
class BankAccount {
#balance = 0; // Private field
constructor(initialBalance) {
this.#balance = initialBalance;
}
deposit(amount) {
this.#balance += amount;
return this.#balance;
}
get balance() {
return this.#balance;
}
// console.log(account.#balance); // SyntaxError: Private field '#balance'
}
1.1.2 JavaScript Engine e Runtime - Como Tudo Funciona
Para escrever JavaScript eficiente, você precisa entender como ele é executado. Vamos mergulhar no
funcionamento interno dos engines JavaScript.
JavaScript Engines - Os Intérpretes Modernos
V8 Engine (Chrome, Node.js):
// O V8 é conhecido por suas otimizações agressivas
function hotFunction(numbers) {
let sum = 0;
for (let i = 0; i < numbers.length; i++) {
sum += numbers[i]; // Loops como este são altamente otimizados
}
return sum;
}
// Se você chamar esta função muitas vezes, o V8 irá:
// 1. Interpretar inicialmente (Ignition)
// 2. Compilar para código otimizado (TurboFan)
// 3. Fazer inline de operações simples
// 4. Otimizar acesso a arrays
const numbers = [1, 2, 3, 4, 5];
for (let i = 0; i < 10000; i++) {
hotFunction(numbers); // Após algumas execuções, fica super rápido
}
SpiderMonkey (Firefox):
// SpiderMonkey tem foco em conformidade com specs
// Frequentemente implementa features ES mais cedo
// Exemplo: Firefox foi um dos primeiros a ter WeakMap
const cache = new WeakMap();
const obj1 = {};
const obj2 = {};
cache.set(obj1, "cached data 1");
cache.set(obj2, "cached data 2");
console.log(cache.get(obj1)); // "cached data 1"
// Quando obj1 e obj2 saem de escopo, são automaticamente removidos do cache
JavaScriptCore (Safari):
// JavaScriptCore (Nitro) é focado em eficiência energética
// Importante para dispositivos móveis
// Otimizações específicas para iOS/macOS
function energyEfficientLoop(data) {
// JavaScriptCore otimiza bem operações vectorizáveis
return data.map(x => x * 2).filter(x => x > 10);
}
Call Stack - A Pilha de Execução
// Vamos visualizar como o Call Stack funciona
function third() {
console.log("3. Em third()");
console.trace(); // Mostra a pilha atual
}
function second() {
console.log("2. Em second()");
third();
console.log("4. Voltou para second()");
}
function first() {
console.log("1. Em first()");
second();
console.log("5. Voltou para first()");
}
first();
/*
Call Stack durante a execução:
1. [] (vazio)
2. [first] (first é chamado)
3. [first, second] (second é chamado dentro de first)
4. [first, second, third] (third é chamado dentro de second)
5. [first, second, third, console.trace] (trace é chamado)
6. [first, second] (third termina)
7. [first] (second termina)
8. [] (first termina)
*/
// Exemplo de Stack Overflow
function infiniteRecursion() {
console.log("Chamando recursivamente...");
infiniteRecursion(); // Vai estourar a pilha
}
// infiniteRecursion(); // RangeError: Maximum call stack size exceeded
Event Loop - O Coração Assíncrono
// O Event Loop é crucial para entender JavaScript assíncrono
console.log("1. Código síncrono primeiro");
setTimeout(() => {
console.log("4. Callback do setTimeout (Task Queue)");
}, 0);
Promise.resolve().then(() => {
console.log("3. Promise then (Microtask Queue)");
});
console.log("2. Mais código síncrono");
/*
Ordem de execução:
1. "1. Código síncrono primeiro"
2. "2. Mais código síncrono"
3. "3. Promise then (Microtask Queue)" <- Microtasks têm prioridade
4. "4. Callback do setTimeout (Task Queue)"
Por que esta ordem?
1. Call Stack executa primeiro (código síncrono)
2. Microtask Queue tem prioridade sobre Task Queue
3. Task Queue só executa quando Call Stack e Microtask Queue estão vazios
*/
// Demonstração mais complexa do Event Loop
function eventLoopDemo() {
console.log("=== Início da demonstração ===");
// Macrotask (Task Queue)
setTimeout(() => console.log("Timeout 1"), 0);
// Microtask
Promise.resolve().then(() => console.log("Promise 1"));
// Microtask aninhada
Promise.resolve().then(() => {
console.log("Promise 2");
Promise.resolve().then(() => console.log("Promise 3 (aninhada)"));
});
// Outro Macrotask
setTimeout(() => console.log("Timeout 2"), 0);
// Código síncrono
console.log("=== Fim da demonstração ===");
}
eventLoopDemo();
/*
Resultado:
=== Início da demonstração ===
=== Fim da demonstração ===
Promise 1
Promise 2
Promise 3 (aninhada)
Timeout 1
Timeout 2
*/
Memory Heap e Garbage Collection
// Como o JavaScript gerencia memória
function memoryDemo() {
// Variáveis primitivas (Call Stack)
let number = 42;
let string = "Hello";
let boolean = true;
// Objetos (Memory Heap)
let object = {
name: "John",
age: 30,
hobbies: ["reading", "coding"]
};
let array = [1, 2, 3, { nested: true }];
// Função (também no Heap)
let func = function() {
return "I'm in the heap!";
};
return { object, array, func }; // Retorna referências
}
// Exemplo de vazamento de memória (evitar!)
function memoryLeakExample() {
let data = new Array(1000000).fill("data"); // Array grande
// Closure que captura 'data'
return function() {
// Mesmo que não use 'data', ele fica na memória
console.log("Function called");
};
}
// Evitando vazamentos
function noMemoryLeak() {
let data = new Array(1000000).fill("data");
// Processe os dados
let result = data.reduce((acc, item) => acc + item.length, 0);
// Limpe a referência
data = null;
return function() {
console.log("Result:", result); // Só 'result' fica na memória
};
}
// WeakMap e WeakSet ajudam com garbage collection
const metadata = new WeakMap();
function attachMetadata(obj, data) {
metadata.set(obj, data); // Não impede GC do obj
}
let myObject = { id: 1 };
attachMetadata(myObject, { created: new Date() });
myObject = null; // metadata será automaticamente removido
JIT Compilation - Otimização em Tempo Real
// O V8 otimiza código "quente" (executado frequentemente)
function demonstrateJIT() {
// Função que será otimizada pelo JIT
function addNumbers(a, b) {
return a + b; // Operação simples, será inline
}
// Tipos consistentes ajudam na otimização
function processArray(numbers) {
let sum = 0;
for (let i = 0; i < numbers.length; i++) {
sum += numbers[i]; // Tipos sempre number = otimização agressiva
}
return sum;
}
// Execução repetida para trigger JIT optimization
const numbers = [1, 2, 3, 4, 5];
// Primeiras execuções: interpretadas
for (let i = 0; i < 1000; i++) {
processArray(numbers);
}
// Depois de muitas execuções: compiladas e otimizadas
console.time("Optimized execution");
for (let i = 0; i < 100000; i++) {
processArray(numbers); // Agora está super otimizado
}
console.timeEnd("Optimized execution");
// Mudança de tipo causa "deoptimization"
const mixedArray = [1, 2, "3", 4, 5]; // Mistura number e string
processArray(mixedArray); // JIT vai "deoptimizar" a função
}
// Dicas para código JIT-friendly
function jitFriendlyCode() {
// BOM: Tipos consistentes
function calculate(numbers) {
return numbers.reduce((sum, n) => sum + n, 0);
}
// BOM: Estruturas de dados consistentes
const users = [
{ id: 1, name: "John", age: 30 },
{ id: 2, name: "Jane", age: 25 },
{ id: 3, name: "Bob", age: 35 }
];
// ❌ RUIM: Tipos inconsistentes
function badCalculate(input) {
if (typeof input === 'number') {
return input * 2;
} else if (Array.isArray(input)) {
return input.reduce((sum, n) => sum + n, 0);
} else {
return 0;
}
}
// ❌ RUIM: Objetos com estruturas diferentes
const badUsers = [
{ id: 1, name: "John" },
{ id: 2, name: "Jane", age: 25, email: "
[email protected]" },
{ name: "Bob", extra: true } // Estrutura totalmente diferente
];
}
demonstrateJIT();
1.1.3 Tipos de Dados e Type System - A Base de Tudo
JavaScript tem um sistema de tipos único que combina flexibilidade com alguns pegadinhas. Vamos entender
cada tipo profundamente.
Tipos Primitivos - Os Blocos Fundamentais
1. undefined - “Não foi definido”
// undefined é um tipo e um valor
let variable; // Declarada mas não inicializada
console.log(variable); // undefined
console.log(typeof variable); // "undefined"
// Casos comuns de undefined
function testUndefined() {
let a; // undefined
let b = undefined; // explicitamente undefined
const obj = { name: "John" };
console.log(obj.age); // undefined (propriedade não existe)
function noReturn() {
// não retorna nada
}
console.log(noReturn()); // undefined
function withParams(x, y) {
console.log(x, y); // segundo parâmetro pode ser undefined
}
withParams(1); // 1, undefined
const arr = [1, 2, 3];
console.log(arr[5]); // undefined (índice não existe)
}
// undefined é falsy
if (undefined) {
console.log("Nunca executa");
}
// Verificações seguras
function safeCheck(value) {
if (value !== undefined) {
console.log("Value is defined:", value);
}
// Ou usando void 0 (mais seguro que undefined)
if (value !== void 0) {
console.log("Definitely defined:", value);
}
}
2. null - “Intencionalmente vazio”
// null representa ausência intencional de valor
let data = null; // Explicitamente "sem valor"
console.log(typeof null); // "object" (bug histórico do JavaScript!)
// Diferença conceitual: undefined vs null
let user = {
name: "John",
email: null, // Não tem email (intencionalmente vazio)
phone: undefined // Propriedade não foi definida ainda
};
// Comparações com null
console.log(null == undefined); // true (coerção de tipo)
console.log(null === undefined); // false (tipos diferentes)
// null é falsy
if (null) {
console.log("Nunca executa");
}
// Verificação segura de null
function processUser(userData) {
if (userData !== null && userData !== undefined) {
// Ou simplesmente: if (userData != null)
console.log("Processing user:", userData.name);
}
}
// Nullish coalescing (ES2020)
const userName = user.name ?? "Anonymous"; // Só usa default se null/undefined
const userEmail = user.email ?? "
[email protected]";
3. boolean - “Verdadeiro ou Falso”
// Boolean é o tipo mais simples
let isActive = true;
let isCompleted = false;
console.log(typeof isActive); // "boolean"
// Boolean() constructor
let bool1 = new Boolean(true); // Objeto Boolean (evitar!)
let bool2 = Boolean(true); // Primitivo boolean (preferir!)
console.log(typeof bool1); // "object"
console.log(typeof bool2); // "boolean"
// Valores falsy em JavaScript (apenas 8!)
const falsyValues = [
false, // boolean false
0, // number zero
-0, // negative zero
0n, // BigInt zero
"", // string vazia
null, // null
undefined, // undefined
NaN // Not a Number
];
falsyValues.forEach(value => {
if (!value) {
console.log(`${value} é falsy`);
}
});
// TODOS os outros valores são truthy!
const truthyValues = [
true,
1,
-1,
"0", // String "0" é truthy!
"false", // String "false" é truthy!
[], // Array vazio é truthy!
{}, // Object vazio é truthy!
function(){} // Função é truthy!
];
truthyValues.forEach(value => {
if (value) {
console.log(`${value} é truthy`);
}
});
// Conversão para boolean
function demonstrateBooleanConversion() {
console.log(!!0); // false
console.log(!!""); // false
console.log(!!null); // false
console.log(!!undefined); // false
console.log(!!NaN); // false
console.log(!!1); // true
console.log(!!"hello"); // true
console.log(!![]); // true
console.log(!!{}); // true
}
4. number - “Números de Ponto Flutuante”
// JavaScript tem apenas um tipo numérico: IEEE 754 double precision
let integer = 42;
let float = 3.14159;
let scientific = 1.23e-4; // 0.000123
let binary = 0b1010; // 10 em decimal
let octal = 0o755; // 493 em decimal
let hex = 0xFF; // 255 em decimal
console.log(typeof integer); // "number"
console.log(typeof float); // "number"
// Valores especiais
let infinity = Infinity;
let negInfinity = -Infinity;
let notANumber = NaN;
console.log(typeof infinity); // "number"
console.log(typeof notANumber); // "number" (NaN é do tipo number!)
// Operações que resultam em NaN
console.log(Math.sqrt(-1)); // NaN
console.log(parseInt("hello")); // NaN
console.log(0 / 0); // NaN
console.log(Infinity - Infinity); // NaN
// NaN é o único valor que não é igual a si mesmo!
console.log(NaN === NaN); // false!
console.log(Number.isNaN(NaN)); // true (forma correta de verificar)
// Precisão de ponto flutuante - CUIDADO!
console.log(0.1 + 0.2); // 0.30000000000000004 (não é 0.3!)
console.log(0.1 + 0.2 === 0.3); // false!
// Solução para precisão
function floatEquals(a, b, epsilon = Number.EPSILON) {
return Math.abs(a - b) < epsilon;
}
console.log(floatEquals(0.1 + 0.2, 0.3)); // true
// Limites numéricos
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991
console.log(Number.MIN_SAFE_INTEGER); // -9007199254740991
console.log(Number.MAX_VALUE); // 1.7976931348623157e+308
console.log(Number.MIN_VALUE); // 5e-324
// Verificações úteis
function numberUtils(value) {
console.log(`Value: ${value}`);
console.log(`Is finite: ${Number.isFinite(value)}`);
console.log(`Is integer: ${Number.isInteger(value)}`);
console.log(`Is safe integer: ${Number.isSafeInteger(value)}`);
console.log(`Is NaN: ${Number.isNaN(value)}`);
console.log("---");
}
numberUtils(42);
numberUtils(3.14);
numberUtils(Infinity);
numberUtils(NaN);
numberUtils("123"); // Não é number!
5. string - “Texto e Caracteres”
// Strings são imutáveis em JavaScript
let single = 'Single quotes';
let double = "Double quotes";
let template = `Template literal with ${single}`;
console.log(typeof template); // "string"
// Imutabilidade das strings
let original = "Hello";
let modified = original.toUpperCase(); // Não modifica 'original'
console.log(original); // "Hello" (não mudou!)
console.log(modified); // "HELLO" (nova string)
// Template literals - poder real
const name = "John";
const age = 30;
const email = "
[email protected]";
const userInfo = `
User Information:
-----------------
Name: ${name}
Age: ${age}
Email: ${email}
Adult: ${age >= 18 ? 'Yes' : 'No'}
Generated at: ${new Date().toLocaleString()}
`;
console.log(userInfo);
// Tagged templates - funcionalidade avançada
function highlight(strings, ...values) {
return strings.reduce((result, string, i) => {
const value = values[i] ? `<mark>${values[i]}</mark>` : '';
return result + string + value;
}, '');
}
const highlightedText = highlight`Hello ${name}, you are ${age} years old!`;
console.log(highlightedText);
// "Hello <mark>John</mark>, you are <mark>30</mark> years old!"
// String methods essenciais
function stringMethods() {
const text = " JavaScript is Amazing! ";
console.log(text.length); // 25
console.log(text.trim()); // "JavaScript is Amazing!"
console.log(text.toLowerCase()); // " javascript is amazing! "
console.log(text.toUpperCase()); // " JAVASCRIPT IS AMAZING! "
console.log(text.includes("Script")); // true
console.log(text.indexOf("Script")); // 4
console.log(text.lastIndexOf("a")); // 18
console.log(text.slice(2, 12)); // "JavaScript"
console.log(text.substring(2, 12)); // "JavaScript"
console.log(text.substr(2, 10)); // "JavaScript" (deprecated)
const words = text.trim().split(" ");
console.log(words); // ["JavaScript", "is", "Amazing!"]
const joined = words.join("-");
console.log(joined); // "JavaScript-is-Amazing!"
console.log(text.replace("Amazing", "Awesome"));
console.log(text.replaceAll("a", "@")); // ES2021
}
// Unicode e caracteres especiais
function unicodeExamples() {
console.log("Hello\nWorld"); // Quebra de linha
console.log("Hello\tWorld"); // Tab
console.log("\"Quoted\""); // Aspas escapadas
console.log('It\'s working'); // Apóstrofe escapado
console.log("\\\\server\\share"); // Barra invertida
// Unicode
console.log("\u0048\u0065\u006C\u006C\u006F"); // "Hello"
console.log("\u{1F600}"); // (emoji)
// Codepoints
const emoji = " ";
console.log(emoji.length); // 2 (surrogate pair!)
console.log([...emoji].length); // 1 (contagem correta)
}
stringMethods();
unicodeExamples();
6. symbol - “Identificadores Únicos”
// Symbol: tipo primitivo para identificadores únicos
let sym1 = Symbol();
let sym2 = Symbol("description");
let sym3 = Symbol("description");
console.log(typeof sym1); // "symbol"
console.log(sym2 === sym3); // false! (sempre únicos)
// Uso principal: propriedades privadas/meta
const PRIVATE_PROPERTY = Symbol("private");
const ITERATOR = Symbol.iterator;
const obj = {
name: "John",
[PRIVATE_PROPERTY]: "secret data",
[Symbol.toStringTag]: "CustomObject"
};
console.log(obj.name); // "John"
console.log(obj[PRIVATE_PROPERTY]); // "secret data"
// Symbols não aparecem em Object.keys()
console.log(Object.keys(obj)); // ["name"] (não inclui symbols!)
console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(private), Symbol(Symbol.toStringTag)]
// Well-known symbols
const myIterable = {
data: [1, 2, 3],
[Symbol.iterator]() {
let index = 0;
const data = this.data;
return {
next() {
if (index < data.length) {
return { value: data[index++], done: false };
} else {
return { done: true };
}
}
};
}
};
// Agora pode usar for...of
for (let value of myIterable) {
console.log(value); // 1, 2, 3
}
// Global symbol registry
const globalSym1 = Symbol.for("app.config");
const globalSym2 = Symbol.for("app.config");
console.log(globalSym1 === globalSym2); // true! (mesmo global symbol)
console.log(Symbol.keyFor(globalSym1)); // "app.config"
7. bigint - “Números Grandes”
// BigInt para números maiores que Number.MAX_SAFE_INTEGER
const bigNumber1 = BigInt(9007199254740992);
const bigNumber2 = 9007199254740992n; // Literal bigint
console.log(typeof bigNumber1); // "bigint"
// Operações com BigInt
const big1 = 123456789012345678901234567890n;
const big2 = 987654321098765432109876543210n;
console.log(big1 + big2); // 1111111110111111111011111111100n
console.log(big1 * big2); // Número gigantesco
// NÃO pode misturar BigInt com Number!
// console.log(10n + 5); // TypeError!
// Conversões necessárias
console.log(10n + BigInt(5)); // 15n
console.log(Number(10n) + 5); // 15
// Comparações
console.log(10n === 10); // false (tipos diferentes)
console.log(10n == 10); // true (coerção de tipo)
console.log(10n > 5); // true (comparação funciona)
// JSON não suporta BigInt nativamente
const data = { id: 123n };
// JSON.stringify(data); // TypeError!
// Solução
JSON.stringify(data, (key, value) =>
typeof value === 'bigint' ? value.toString() : value
); // '{"id":"123"}'
Tipos Compostos e Referência
Objects - A Base de Tudo
// Em JavaScript, quase tudo é object (exceto primitivos)
const obj = {}; // Object literal
const arr = []; // Array é object
const func = function(){}; // Function é object
const date = new Date(); // Date é object
const regex = /pattern/; // RegExp é object
console.log(typeof obj); // "object"
console.log(typeof arr); // "object"
console.log(typeof func); // "function" (caso especial)
console.log(typeof date); // "object"
console.log(typeof regex); // "object"
// Reference vs Primitive
let primitive1 = 5;
let primitive2 = primitive1; // Copia o valor
primitive1 = 10;
console.log(primitive2); // 5 (não mudou)
let object1 = { value: 5 };
let object2 = object1; // Copia a referência!
object1.value = 10;
console.log(object2.value); // 10 (mudou também!)
// Cloning objects
const original = {
name: "John",
age: 30,
address: {
street: "123 Main St",
city: "NY"
}
};
// Shallow copy
const shallowCopy = { ...original };
const alsoShallow = Object.assign({}, original);
shallowCopy.name = "Jane"; // Não afeta original
console.log(original.name); // "John"
shallowCopy.address.city = "LA"; // Afeta original! (referência compartilhada)
console.log(original.address.city); // "LA"
// Deep copy (cuidado com performance)
const deepCopy = JSON.parse(JSON.stringify(original));
// Limitações: não copia functions, undefined, symbols, dates...
// Deep copy mais robusto (biblioteca ou implementação custom)
function deepClone(obj) {
if (obj === null || typeof obj !== "object") return obj;
if (obj instanceof Date) return new Date(obj);
if (obj instanceof Array) return obj.map(item => deepClone(item));
if (typeof obj === "object") {
const clonedObj = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clonedObj[key] = deepClone(obj[key]);
}
}
return clonedObj;
}
}
Type Coercion - Conversões Automáticas
// JavaScript faz coerção de tipos automaticamente
console.log("=== COERÇÃO AUTOMÁTICA ===");
// String + qualquer coisa = concatenação
console.log("5" + 3); // "53"
console.log("5" + true); // "5true"
console.log("5" + null); // "5null"
console.log("5" + undefined); // "5undefined"
console.log("5" + {}); // "5[object Object]"
console.log("5" + []); // "5" (array vazio vira string vazia)
console.log("5" + [1,2,3]); // "51,2,3"
// Outros operadores fazem conversão para number
console.log("5" - 3); // 2 (subtração)
console.log("5" * 3); // 15 (multiplicação)
console.log("5" / 2); // 2.5 (divisão)
console.log("5" % 3); // 2 (módulo)
// Boolean em contextos numéricos
console.log(true + 1); // 2 (true = 1)
console.log(false + 1); // 1 (false = 0)
// Comparações com coerção (== vs ===)
console.log("=== COMPARAÇÕES ===");
console.log(5 == "5"); // true (coerção)
console.log(5 === "5"); // false (sem coerção)
console.log(true == 1); // true (coerção)
console.log(true === 1); // false (sem coerção)
console.log(null == undefined); // true (caso especial)
console.log(null === undefined); // false (tipos diferentes)
// Casos bizarros de coerção
console.log("=== CASOS BIZARROS ===");
console.log([] + []); // "" (string vazia)
console.log({} + {}); // "[object Object][object Object]"
console.log([] + {}); // "[object Object]"
console.log({} + []); // 0 (depende do contexto!)
// Conversão explícita (preferir sempre!)
function explicitConversion() {
// Para string
console.log(String(123)); // "123"
console.log((123).toString()); // "123"
console.log(`${123}`); // "123"
// Para number
console.log(Number("123")); // 123
console.log(Number("123.45")); // 123.45
console.log(Number("123abc")); // NaN
console.log(parseInt("123abc")); // 123
console.log(parseFloat("123.45abc")); // 123.45
console.log(+"123"); // 123 (unary plus)
// Para boolean
console.log(Boolean(1)); // true
console.log(Boolean(0)); // false
console.log(Boolean("")); // false
console.log(Boolean("hello")); // true
console.log(!!"hello"); // true (double negation)
}
explicitConversion();
Type Checking - Verificação de Tipos
// typeof é útil mas tem limitações
console.log("=== TYPEOF LIMITATIONS ===");
console.log(typeof null); // "object" (bug histórico!)
console.log(typeof []); // "object" (não distingue array)
console.log(typeof new Date()); // "object" (não distingue date)
console.log(typeof /regex/); // "object" (não distingue regex)
// Método mais preciso
function getType(value) {
return Object.prototype.toString.call(value).slice(8, -1).toLowerCase();
}
console.log("=== GETTYPE PRECISO ===");
console.log(getType(null)); // "null"
console.log(getType([])); // "array"
console.log(getType(new Date())); // "date"
console.log(getType(/regex/)); // "regexp"
console.log(getType({})); // "object"
console.log(getType(function(){})); // "function"
// Verificações específicas
function typeCheckers() {
const value = [1, 2, 3];
// Array check
console.log(Array.isArray(value)); // true
console.log(value instanceof Array); // true
// Object check (exclui null e arrays)
function isObject(val) {
return val !== null && typeof val === 'object' && !Array.isArray(val);
}
console.log(isObject({})); // true
console.log(isObject([])); // false
console.log(isObject(null)); // false
// Function check
function isFunction(val) {
return typeof val === 'function';
}
// Number check (excluindo NaN)
function isValidNumber(val) {
return typeof val === 'number' && !isNaN(val) && isFinite(val);
}
console.log(isValidNumber(42)); // true
console.log(isValidNumber(NaN)); // false
console.log(isValidNumber(Infinity)); // false
// String check (não vazia)
function isNonEmptyString(val) {
return typeof val === 'string' && val.length > 0;
}
}
// Duck typing em JavaScript
function duckTyping() {
// "Se anda como um pato e faz quack como um pato, é um pato"
function canFly(object) {
return typeof object.fly === 'function';
}
function canSwim(object) {
return typeof object.swim === 'function';
}
const bird = {
fly() { return "Flying high!"; },
chirp() { return "Tweet!"; }
};
const fish = {
swim() { return "Swimming deep!"; },
breatheUnderwater() { return "Bubble bubble"; }
};
const duck = {
fly() { return "Flying low!"; },
swim() { return "Paddling around!"; },
quack() { return "Quack!"; }
};
console.log("Bird can fly:", canFly(bird)); // true
console.log("Fish can swim:", canSwim(fish)); // true
console.log("Duck can fly:", canFly(duck)); // true
console.log("Duck can swim:", canSwim(duck)); // true
// Duck typing é mais flexível que herança rígida
function makeItFly(flyingThing) {
if (canFly(flyingThing)) {
console.log(flyingThing.fly());
} else {
console.log("This thing cannot fly!");
}
}
makeItFly(bird); // "Flying high!"
makeItFly(duck); // "Flying low!"
makeItFly(fish); // "This thing cannot fly!"
}
typeCheckers();
duckTyping();
Esta é a base fundamental do JavaScript. Entender esses tipos e como eles se comportam é essencial para:
1. Evitar bugs comuns (coerção inesperada, comparações falsas)
2. Escrever código mais robusto (verificações de tipo adequadas)
3. Otimizar performance (engines optimizam melhor com tipos consistentes)
4. Debugging eficiente (entender o que cada variável realmente contém)
Na próxima seção, vamos ver como esses tipos se comportam nos diferentes paradigmas de programação que
JavaScript suporta.
1.2 JAVASCRIPT MULTIPARADIGMA: TEORIA E PRÁTICA
Comparação profunda dos paradigmas suportados
1.2.1 Programação Imperativa em JavaScript
Como fazer - especifica passos exatos
A programação imperativa é um paradigma de programação que se concentra em como fazer alguma coisa, ao
invés de o que fazer. É chamada de “imperativa” porque você dá comandos explícitos (imperativos) ao
computador sobre cada passo que ele deve executar para resolver um problema.
Definição Conceitual
Na programação imperativa, você escreve código que: - Modifica o estado do programa através de variáveis -
Executa instruções sequencialmente, uma após a outra - Usa estruturas de controle como loops e
condicionais - Foca no processo de resolução do problema, não apenas no resultado
Analogia com o Mundo Real
Imagine que você está dando instruções para alguém fazer um sanduíche:
Estilo Imperativo (Passo a passo):
1. Pegue 2 fatias de pão
2. Abra o pote de manteiga
3. Passe manteiga na primeira fatia
4. Coloque presunto sobre a manteiga
5. Adicione queijo sobre o presunto
6. Feche com a segunda fatia
7. Corte ao meio
Estilo Declarativo (Resultado desejado):
"Quero um sanduíche de presunto e queijo"
A programação imperativa é como dar as instruções passo a passo.
Características Fundamentais
1. Estado Mutável
Em programação imperativa, as variáveis podem ter seus valores alterados durante a execução:
// Estado inicial
let contador = 0;
let usuario = { nome: 'João', idade: 25 };
let lista = [1, 2, 3];
// Modificando o estado
contador = contador + 1; // contador agora é 1
contador += 5; // contador agora é 6
contador++; // contador agora é 7
// Modificando objeto
usuario.nome = 'Pedro';
usuario.idade = 30;
// Modificando array
lista.push(4); // lista agora é [1, 2, 3, 4]
lista[0] = 10; // lista agora é [10, 2, 3, 4]
2. Sequência de Comandos
As instruções são executadas em uma ordem específica:
// Cada linha é executada em sequência
console.log("Passo 1: Inicializando variáveis");
let x = 10;
let y = 20;
console.log("Passo 2: Calculando soma");
let soma = x + y;
console.log("Passo 3: Modificando valores");
x = x * 2;
y = y + 5;
console.log("Passo 4: Nova soma");
let novaSoma = x + y;
console.log(`Resultados: soma inicial = ${soma}, nova soma = ${novaSoma}`);
// Output: Resultados: soma inicial = 30, nova soma = 45
3. Efeitos Colaterais
Funções imperativas frequentemente causam efeitos colaterais (modificam estado externo):
let saldo = 1000;
let historico = [];
// Função com efeito colateral - modifica variáveis globais
function sacar(valor) {
if (saldo >= valor) {
saldo -= valor; // Modifica variável externa
historico.push(`Saque: R$ ${valor}`); // Modifica array externo
console.log(`Saque realizado. Saldo atual: R$ ${saldo}`);
return true;
} else {
console.log("Saldo insuficiente");
return false;
}
}
// Uso da função
console.log(`Saldo inicial: R$ ${saldo}`);
sacar(300); // Modifica saldo e historico
sacar(800); // Tentativa que falhará
console.log("Histórico:", historico);
Estruturas de Controle
1. Condicionais (if/else)
Controlam o fluxo baseado em condições:
function verificarIdade(idade) {
let categoria;
let podeVotar;
let podeDirigir;
// Estrutura condicional imperativa
if (idade < 16) {
categoria = "criança";
podeVotar = false;
podeDirigir = false;
} else if (idade < 18) {
categoria = "adolescente";
podeVotar = true; // No Brasil, voto é opcional aos 16
podeDirigir = false;
} else if (idade < 65) {
categoria = "adulto";
podeVotar = true;
podeDirigir = true;
} else {
categoria = "idoso";
podeVotar = true; // Voto opcional após 70 anos
podeDirigir = true;
}
// Modificando um objeto com base nas condições
let resultado = {
categoria: categoria,
podeVotar: podeVotar,
podeDirigir: podeDirigir,
descricao: `Pessoa de ${idade} anos é ${categoria}`
};
return resultado;
}
// Teste da função
console.log(verificarIdade(15)); // criança
console.log(verificarIdade(17)); // adolescente
console.log(verificarIdade(25)); // adulto
console.log(verificarIdade(70)); // idoso
2. Loops (for, while, do-while)
Repetem operações modificando estado:
// Exemplo: Processando lista de vendas
let vendas = [
{ produto: 'Notebook', preco: 2500, categoria: 'eletrônicos' },
{ produto: 'Mouse', preco: 50, categoria: 'eletrônicos' },
{ produto: 'Livro', preco: 30, categoria: 'educação' },
{ produto: 'Teclado', preco: 150, categoria: 'eletrônicos' },
{ produto: 'Curso', preco: 200, categoria: 'educação' }
];
// Variáveis que serão modificadas durante os loops
let totalVendas = 0;
let totalEletronicos = 0;
let totalEducacao = 0;
let produtosCaros = [];
let contador = 0;
// Loop FOR tradicional - modificando estado a cada iteração
console.log("=== Processando vendas com FOR ===");
for (let i = 0; i < vendas.length; i++) {
let venda = vendas[i];
// Acumulando total
totalVendas += venda.preco;
// Separando por categoria
if (venda.categoria === 'eletrônicos') {
totalEletronicos += venda.preco;
} else if (venda.categoria === 'educação') {
totalEducacao += venda.preco;
}
// Identificando produtos caros
if (venda.preco > 100) {
produtosCaros.push(venda.produto);
}
contador++;
console.log(`${contador}. ${venda.produto}: R$ ${venda.preco}`);
}
// Resultados finais
console.log("\n=== RESULTADOS FINAIS ===");
console.log(`Total de vendas: R$ ${totalVendas}`);
console.log(`Total eletrônicos: R$ ${totalEletronicos}`);
console.log(`Total educação: R$ ${totalEducacao}`);
console.log(`Produtos caros (>R$100): ${produtosCaros.join(', ')}`);
Exemplo Prático: Sistema de Controle de Estoque
// Sistema de estoque imperativo
let estoque = {
produtos: new Map(),
proximoId: 1,
movimentacoes: []
};
// Função imperativa para adicionar produto
function adicionarProduto(nome, quantidade, preco) {
let produto = {
id: estoque.proximoId++,
nome: nome,
quantidade: quantidade,
preco: preco,
dataCriacao: new Date()
};
estoque.produtos.set(produto.id, produto);
// Registra movimentação
estoque.movimentacoes.push({
tipo: 'ENTRADA',
produtoId: produto.id,
quantidade: quantidade,
data: new Date()
});
console.log(`Produto ${nome} adicionado com ID ${produto.id}`);
return produto.id;
}
function atualizarQuantidade(id, novaQuantidade) {
if (!estoque.produtos.has(id)) {
console.log("Produto não encontrado");
return false;
}
let produto = estoque.produtos.get(id);
let quantidadeAnterior = produto.quantidade;
produto.quantidade = novaQuantidade;
let tipoMovimentacao = novaQuantidade > quantidadeAnterior ? 'ENTRADA' : 'SAÍDA';
let diferenca = Math.abs(novaQuantidade - quantidadeAnterior);
estoque.movimentacoes.push({
tipo: tipoMovimentacao,
produtoId: id,
quantidade: diferenca,
quantidadeAnterior: quantidadeAnterior,
quantidadeAtual: novaQuantidade,
data: new Date()
});
console.log(`Quantidade do produto ${produto.nome} atualizada: ${quantidadeAnterior} → ${novaQuantidade}`);
return true;
}
// Simulando uso
console.log("=== SISTEMA DE ESTOQUE ===");
let idCaneta = adicionarProduto("Caneta", 100, 2.50);
let idCaderno = adicionarProduto("Caderno", 50, 15.00);
atualizarQuantidade(idCaneta, 90); // Vendeu 10 canetas
console.log("Estado final do estoque:", estoque.produtos.size, "produtos");
console.log("Total de movimentações:", estoque.movimentacoes.length);
Vantagens da Programação Imperativa
1. Facilidade de Compreensão - Fluxo Natural: Segue a lógica humana natural de “faça isso, depois aquilo”
- Transparência: É fácil ver exatamente o que está acontecendo a cada passo
2. Controle Fino sobre Performance - Otimização Específica: Você pode otimizar exatamente onde
necessário - Gerenciamento de Memória: Controle direto sobre alocação e liberação
3. Facilidade para Debug - Step-by-Step: Fácil de debugar linha por linha - Estado Visível: Você pode
inspecionar o estado a qualquer momento
Desvantagens da Programação Imperativa
1. Efeitos Colaterais Indesejados - Estado Compartilhado: Múltiplas funções modificando o mesmo estado
- Bugs Difíceis: Modificações inesperadas podem causar bugs complexos
2. Dificuldade de Teste - Estado Mutável: Testes podem interferir uns nos outros - Dependências: Funções
dependem de estado externo
3. Código Mais Verboso - Muitas Linhas: Requer mais código para operações simples - Repetição: Padrões
repetitivos de loops e condicionais
Quando Usar Programação Imperativa
Cenários Ideais: 1. Algoritmos de Manipulação de Dados Complexos 2. Processamento Sequencial
com Estado 3. Interação com Hardware/APIs Externas 4. Games e Simulações
Quando NÃO Usar: 1. Transformações Simples de Dados 2. Operações Matemáticas Complexas
// EXEMPLO: Quando usar imperativo
function encontrarSequenciaConsecutiva(numeros, tamanho) {
// Imperativo é melhor para lógica complexa com breaks
for (let i = 0; i <= numeros.length - tamanho; i++) {
let sequenciaValida = true;
for (let j = 0; j < tamanho - 1; j++) {
if (numeros[i + j] + 1 !== numeros[i + j + 1]) {
sequenciaValida = false;
break; // Break é mais claro e eficiente aqui
}
}
if (sequenciaValida) {
return i; // Return direto é mais eficiente
}
}
return -1;
}
console.log(encontrarSequenciaConsecutiva([1, 2, 3, 5, 6, 7, 8, 10], 3)); // 4 (posição de [5,6,7])
A programação imperativa é fundamental em JavaScript e serve como base para entender os outros
paradigmas. É especialmente útil quando você precisa de controle fino sobre o fluxo de execução e
performance do programa.
1.2.2 Programação Funcional em JavaScript
O que fazer - declara o resultado desejado
A programação funcional é um paradigma de programação que trata a computação como a avaliação de
funções matemáticas e evita mudanças de estado e dados mutáveis. Em vez de focar em como fazer algo
(imperativo), foca em o que fazer (declarativo).
Definição Conceitual
Na programação funcional, você escreve código que: - Usa funções como blocos fundamentais de
construção - Evita mutação de estado - dados não são modificados após criados - Favorece imutabilidade -
criando novos dados em vez de modificar existentes - Trata funções como valores - podem ser passadas como
parâmetros e retornadas - Foca na composição - combinando funções simples para criar comportamentos
complexos
Analogia com Matemática
Na matemática, uma função sempre produz a mesma saída para a mesma entrada:
f(x) = x² + 2x + 1
f(3) = 9 + 6 + 1 = 16 (sempre será 16)
Na programação funcional, buscamos o mesmo comportamento:
// Função matemática - sempre retorna o mesmo resultado
const quadratica = (x) => x * x + 2 * x + 1;
console.log(quadratica(3)); // 16
console.log(quadratica(3)); // 16 (sempre o mesmo resultado)
// Comparação com abordagem imperativa (não funcional)
let resultado = 0;
function quadraticaImperativa(x) {
resultado = x * x + 2 * x + 1; // Modifica estado externo
return resultado;
}
Funções Puras - O Coração da Programação Funcional
Uma função pura é uma função que: 1. Sempre retorna o mesmo resultado para os mesmos argumentos 2.
Não produz efeitos colaterais (não modifica estado externo)
Exemplos de Funções Puras:
// PURA - sempre mesmo resultado, sem efeitos colaterais
const somar = (a, b) => a + b;
const multiplicar = (x, y) => x * y;
const obterComprimento = (str) => str.length;
// PURA - função mais complexa mas ainda pura
const calcularDesconto = (preco, percentual) => {
const desconto = preco * (percentual / 100);
return {
precoOriginal: preco,
desconto: desconto,
precoFinal: preco - desconto
};
};
// PURA - trabalhando com arrays sem modificá-los
const adicionarItem = (lista, item) => [...lista, item];
const removerItem = (lista, indice) => lista.filter((_, i) => i !== indice);
const atualizarItem = (lista, indice, novoItem) =>
lista.map((item, i) => i === indice ? novoItem : item);
// Testando pureza
const numeros = [1, 2, 3];
console.log('Original:', numeros);
console.log('Adicionado:', adicionarItem(numeros, 4)); // [1,2,3,4]
console.log('Original ainda:', numeros); // [1,2,3] - não modificado!
Exemplos de Funções Impuras:
// IMPURA - modifica variável externa
let total = 0;
function somarAoTotal(valor) {
total += valor; // Efeito colateral
return total;
}
// IMPURA - resultado depende de estado externo
let configuracao = { taxa: 0.1 };
function calcularTaxa(valor) {
return valor * configuracao.taxa; // Depende de estado externo
}
// IMPURA - efeito colateral (console.log)
function logarESomar(a, b) {
console.log(`Somando ${a} + ${b}`); // Efeito colateral
return a + b;
}
// IMPURA - modifica o array de entrada
function adicionarItemImpuro(lista, item) {
lista.push(item); // Modifica o array original
return lista;
}
Imutabilidade - Dados que Não Mudam
Imutabilidade significa que uma vez que um dado é criado, ele nunca é alterado. Em vez de modificar,
criamos novos dados.
Imutabilidade com Arrays:
const numeros = [1, 2, 3, 4, 5];
// MÉTODOS IMUTÁVEIS (preferir em programação funcional)
const adicionarFim = arr => [...arr, 6];
const adicionarInicio = arr => [0, ...arr];
const removerUltimo = arr => arr.slice(0, -1);
const removerPrimeiro = arr => arr.slice(1);
const removerIndice = (arr, indice) => arr.filter((_, i) => i !== indice);
const substituirIndice = (arr, indice, novoValor) =>
arr.map((item, i) => i === indice ? novoValor : item);
// Testando imutabilidade
console.log('Original:', numeros);
console.log('Com 6 no fim:', adicionarFim(numeros));
console.log('Com 0 no início:', adicionarInicio(numeros));
console.log('Sem último:', removerUltimo(numeros));
console.log('Sem primeiro:', removerPrimeiro(numeros));
console.log('Sem índice 2:', removerIndice(numeros, 2));
console.log('Índice 2 = 100:', substituirIndice(numeros, 2, 100));
console.log('Original ainda:', numeros); // [1,2,3,4,5] - nunca mudou!
// Operações mais complexas imutáveis
const produtos = [
{ id: 1, nome: 'Notebook', preco: 2000, categoria: 'eletrônicos' },
{ id: 2, nome: 'Mouse', preco: 50, categoria: 'eletrônicos' },
{ id: 3, nome: 'Livro', preco: 30, categoria: 'educação' }
];
// Aumentar preços em 10% - imutável
const aumentarPrecos = (produtos, percentual) =>
produtos.map(produto => ({
...produto,
preco: produto.preco * (1 + percentual / 100)
}));
// Adicionar produto - imutável
const adicionarProduto = (produtos, novoProduto) => [...produtos, novoProduto];
// Atualizar produto - imutável
const atualizarProduto = (produtos, id, atualizacao) =>
produtos.map(produto =>
produto.id === id ? { ...produto, ...atualizacao } : produto
);
// Remover produto - imutável
const removerProduto = (produtos, id) => produtos.filter(p => p.id !== id);
console.log('Produtos originais:', produtos);
console.log('Com aumento de 10%:', aumentarPrecos(produtos, 10));
console.log('Originais nunca mudaram:', produtos);
Higher-Order Functions - Funções de Alto Nível
Higher-Order Functions são funções que: 1. Recebem outras funções como parâmetros, ou 2. Retornam
funções como resultado
Funções que Recebem Funções:
// Função que recebe uma função como parâmetro
function aplicarOperacao(a, b, operacao) {
console.log(`Aplicando operação em ${a} e ${b}`);
return operacao(a, b);
}
// Diferentes operações
const somar = (x, y) => x + y;
const multiplicar = (x, y) => x * y;
const potencia = (x, y) => Math.pow(x, y);
console.log(aplicarOperacao(5, 3, somar)); // 8
console.log(aplicarOperacao(5, 3, multiplicar)); // 15
console.log(aplicarOperacao(5, 3, potencia)); // 125
// Sistema de validação usando higher-order functions
function criarValidador(...validacoes) {
return function(valor) {
for (let validacao of validacoes) {
const resultado = validacao(valor);
if (!resultado.valido) {
return resultado;
}
}
return { valido: true, mensagem: 'Válido' };
};
}
// Funções de validação
const validarTamanhoMinimo = (min) => (valor) => ({
valido: valor.length >= min,
mensagem: valor.length >= min ? 'OK' : `Mínimo ${min} caracteres`
});
const validarTamanhoMaximo = (max) => (valor) => ({
valido: valor.length <= max,
mensagem: valor.length <= max ? 'OK' : `Máximo ${max} caracteres`
});
const validarTemNumeros = () => (valor) => ({
valido: /\d/.test(valor),
mensagem: /\d/.test(valor) ? 'OK' : 'Deve conter ao menos um número'
});
// Criando validador de senha
const validarSenha = criarValidador(
validarTamanhoMinimo(8),
validarTamanhoMaximo(20),
validarTemNumeros()
);
console.log(validarSenha('123')); // Inválido: muito curta
console.log(validarSenha('senhafraca')); // Inválido: sem números
console.log(validarSenha('SenhaForte123')); // Válido
Funções que Retornam Funções:
// Factory de funções simples
function criarMultiplicador(fator) {
return function(numero) {
return numero * fator;
};
}
const dobrar = criarMultiplicador(2);
const triplicar = criarMultiplicador(3);
const multiplicarPorCinco = criarMultiplicador(5);
console.log(dobrar(4)); // 8
console.log(triplicar(4)); // 12
console.log(multiplicarPorCinco(4)); // 20
// Factory de filtros
function criarFiltro(campo, operador, valor) {
return function(item) {
switch (operador) {
case '>':
return item[campo] > valor;
case '<':
return item[campo] < valor;
case '===':
return item[campo] === valor;
case 'includes':
return item[campo].includes(valor);
default:
return false;
}
};
}
const produtos = [
{ nome: 'Notebook', preco: 2000, categoria: 'eletrônicos' },
{ nome: 'Mouse', preco: 50, categoria: 'eletrônicos' },
{ nome: 'Livro JS', preco: 45, categoria: 'livros' },
{ nome: 'Monitor', preco: 800, categoria: 'eletrônicos' }
];
const filtrarCaros = criarFiltro('preco', '>', 100);
const filtrarEletronicos = criarFiltro('categoria', '===', 'eletrônicos');
console.log('Produtos caros:', produtos.filter(filtrarCaros));
console.log('Eletrônicos:', produtos.filter(filtrarEletronicos));
Composição de Funções - Combinando Simplicidade
Composição é a técnica de combinar funções simples para criar funções mais complexas:
// Funções simples para composição
const adicionar1 = x => x + 1;
const multiplicarPor2 = x => x * 2;
const elevarAoQuadrado = x => x * x;
const toString = x => x.toString();
const adicionarExclamacao = str => str + '!';
// Pipeline (esquerda para direita)
const pipeline = (...funcs) => (valor) =>
funcs.reduce((acc, func) => func(acc), valor);
// Testando pipeline (mais intuitivo para leitura)
const transformar = pipeline(
adicionar1,
multiplicarPor2,
elevarAoQuadrado,
toString,
adicionarExclamacao
);
console.log(transformar(3)); // 3 -> 4 -> 8 -> 64 -> "64" -> "64!"
// Exemplo prático: processamento de dados de usuários
const usuarios = [
{ nome: 'ana silva', idade: 25, salario: 5000, departamento: 'ti' },
{ nome: 'joão santos', idade: 30, salario: 6000, departamento: 'vendas' },
{ nome: 'maria oliveira', idade: 28, salario: 5500, departamento: 'ti' }
];
// Funções de transformação
const formatarNome = (usuario) => ({
...usuario,
nome: usuario.nome
.split(' ')
.map(parte => parte.charAt(0).toUpperCase() + parte.slice(1))
.join(' ')
});
const calcularSalarioLiquido = (taxaDesconto) => (usuario) => ({
...usuario,
salarioLiquido: usuario.salario * (1 - taxaDesconto)
});
const adicionarCategoriaSalario = (usuario) => ({
...usuario,
categoriaSalario: usuario.salario >= 6000 ? 'alto' :
usuario.salario >= 5000 ? 'médio' : 'baixo'
});
// Compondo transformações
const processarUsuario = pipeline(
formatarNome,
calcularSalarioLiquido(0.25), // 25% de desconto
adicionarCategoriaSalario
);
const usuariosProcessados = usuarios.map(processarUsuario);
console.log('Usuários processados:', usuariosProcessados);
Métodos Funcionais de Array - Poder Built-in
JavaScript oferece métodos built-in que seguem princípios funcionais:
map() - Transformação:
const numeros = [1, 2, 3, 4, 5];
const produtos = [
{ id: 1, nome: 'Notebook', preco: 2000 },
{ id: 2, nome: 'Mouse', preco: 50 },
{ id: 3, nome: 'Teclado', preco: 150 }
];
// Transformações simples
const dobrados = numeros.map(x => x * 2);
const quadrados = numeros.map(x => x * x);
const strings = numeros.map(x => `Número ${x}`);
console.log('Originais:', numeros);
console.log('Dobrados:', dobrados);
console.log('Quadrados:', quadrados);
// Transformações de objetos
const produtosComDesconto = produtos.map(produto => ({
...produto,
precoComDesconto: produto.preco * 0.9,
desconto: produto.preco * 0.1
}));
console.log('Com desconto:', produtosComDesconto);
filter() - Filtragem:
const numeros = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const usuarios = [
{ id: 1, nome: 'Ana', idade: 25, ativo: true, departamento: 'TI' },
{ id: 2, nome: 'João', idade: 17, ativo: false, departamento: 'Vendas' },
{ id: 3, nome: 'Maria', idade: 30, ativo: true, departamento: 'TI' },
{ id: 4, nome: 'Pedro', idade: 28, ativo: true, departamento: 'RH' }
];
// Filtros simples
const pares = numeros.filter(x => x % 2 === 0);
const maioresQue5 = numeros.filter(x => x > 5);
console.log('Pares:', pares);
console.log('Maiores que 5:', maioresQue5);
// Filtros de objetos
const usuariosAtivos = usuarios.filter(u => u.ativo);
const usuariosMaiores = usuarios.filter(u => u.idade >= 18);
const usuariosTI = usuarios.filter(u => u.departamento === 'TI');
console.log('Usuários ativos:', usuariosAtivos);
console.log('Usuários de TI:', usuariosTI);
// Filtros complexos combinados
const usuariosAtivosTI = usuarios.filter(u => u.ativo && u.departamento === 'TI');
console.log('Ativos de TI:', usuariosAtivosTI);
reduce() - Agregação:
const numeros = [1, 2, 3, 4, 5];
const produtos = [
{ id: 1, nome: 'Notebook', preco: 2000, categoria: 'eletrônicos', vendidos: 5 },
{ id: 2, nome: 'Mouse', preco: 50, categoria: 'eletrônicos', vendidos: 20 },
{ id: 3, nome: 'Livro', preco: 30, categoria: 'livros', vendidos: 10 }
];
// Reduções simples
const soma = numeros.reduce((acc, num) => acc + num, 0);
const produto = numeros.reduce((acc, num) => acc * num, 1);
const maximo = numeros.reduce((acc, num) => Math.max(acc, num), -Infinity);
console.log('Soma:', soma);
console.log('Produto:', produto);
console.log('Máximo:', maximo);
// Reduções de objetos
const valorTotalProdutos = produtos.reduce((acc, prod) => acc + prod.preco, 0);
const faturamentoTotal = produtos.reduce((acc, prod) => acc + (prod.preco * prod.vendidos), 0);
console.log('Valor total produtos:', valorTotalProdutos);
console.log('Faturamento total:', faturamentoTotal);
// Agrupar por categoria
const produtosPorCategoria = produtos.reduce((acc, produto) => {
if (!acc[produto.categoria]) {
acc[produto.categoria] = [];
}
acc[produto.categoria].push(produto);
return acc;
}, {});
console.log('Por categoria:', produtosPorCategoria);
Combinando Métodos (Chaining):
const vendas = [
{ id: 1, produto: 'Notebook', valor: 2000, categoria: 'eletrônicos', vendedor: 'Ana', mes: 1 },
{ id: 2, produto: 'Mouse', valor: 50, categoria: 'eletrônicos', vendedor: 'João', mes: 1 },
{ id: 3, produto: 'Teclado', valor: 150, categoria: 'eletrônicos', vendedor: 'Pedro', mes: 2 },
{ id: 4, produto: 'Monitor', valor: 800, categoria: 'eletrônicos', vendedor: 'Ana', mes: 3 }
];
// Pipeline complexo: vendas de eletrônicos > R$100, agrupadas por vendedor
const vendasEletronicosCaras = vendas
.filter(venda => venda.categoria === 'eletrônicos') // Só eletrônicos
.filter(venda => venda.valor > 100) // Só acima de R$100
.map(venda => ({ // Adiciona informações
...venda,
trimestre: Math.ceil(venda.mes / 3),
categoria_premium: venda.valor > 500
}))
.reduce((acc, venda) => { // Agrupa por vendedor
if (!acc[venda.vendedor]) {
acc[venda.vendedor] = {
totalVendas: 0,
totalValor: 0,
vendas: []
};
}
acc[venda.vendedor].totalVendas++;
acc[venda.vendedor].totalValor += venda.valor;
acc[venda.vendedor].vendas.push(venda);
return acc;
}, {});
console.log('Eletrônicos caros por vendedor:', vendasEletronicosCaras);
// Top 3 produtos mais caros
const top3Produtos = vendas
.sort((a, b) => b.valor - a.valor) // Ordena por valor desc
.slice(0, 3) // Pega os 3 primeiros
.map((venda, posicao) => ({ // Enriquece dados
posicao: posicao + 1,
produto: venda.produto,
valor: venda.valor,
vendedor: venda.vendedor
}));
console.log('Top 3 produtos:', top3Produtos);
Vantagens da Programação Funcional
1. Código Mais Previsível e Testável
// FUNCIONAL - Fácil de testar e prever
const calcularDesconto = (preco, percentual) => ({
precoOriginal: preco,
desconto: preco * (percentual / 100),
precoFinal: preco * (1 - percentual / 100)
});
// Teste simples e confiável
console.log(calcularDesconto(100, 10)); // Sempre retorna o mesmo resultado
2. Menor Chance de Bugs
// FUNCIONAL - Imutável, sem efeitos colaterais
const adicionarItem = (lista, item) => [...lista, item];
const lista1 = [1, 2, 3];
const lista2 = adicionarItem(lista1, 4);
console.log(lista1); // [1, 2, 3] - original preservado
console.log(lista2); // [1, 2, 3, 4] - nova lista
3. Código Mais Expressivo e Legível
const usuarios = [
{ nome: 'Ana', idade: 25, salario: 5000, ativo: true },
{ nome: 'João', idade: 30, salario: 6000, ativo: false },
{ nome: 'Maria', idade: 28, salario: 5500, ativo: true }
];
// FUNCIONAL - Expressivo e legível
const salarioMedioUsuariosAtivos = usuarios
.filter(usuario => usuario.ativo)
.map(usuario => usuario.salario)
.reduce((acc, salario) => acc + salario, 0) /
usuarios.filter(usuario => usuario.ativo).length;
console.log('Salário médio usuários ativos:', salarioMedioUsuariosAtivos);
Desvantagens da Programação Funcional
1. Performance - Criação de Novos Objetos
// FUNCIONAL - Cria novos arrays/objects a cada operação
const numeros = Array.from({ length: 100000 }, (_, i) => i);
console.time('Funcional');
const resultadoFuncional = numeros
.filter(n => n % 2 === 0) // Cria novo array
.map(n => n * 2) // Cria novo array
.slice(0, 1000); // Cria novo array
console.timeEnd('Funcional');
// IMPERATIVO - Modifica estruturas existentes
console.time('Imperativo');
const resultadoImperativo = [];
for (let i = 0; i < numeros.length && resultadoImperativo.length < 1000; i++) {
if (numeros[i] % 2 === 0) {
resultadoImperativo.push(numeros[i] * 2);
}
}
console.timeEnd('Imperativo');
// Imperativo geralmente é mais rápido para grandes volumes
Quando Usar Programação Funcional
Cenários Ideais:
1. Transformação de Dados
// Ideal: pipeline de transformações
const usuarios = [
{ nome: 'ana silva', email: '
[email protected]', idade: 25, salario: 5000 },
{ nome: 'joão santos', email: '
[email protected]', idade: 30, salario: 6000 }
];
const usuariosProcessados = usuarios
.map(usuario => ({
...usuario,
nome: usuario.nome
.split(' ')
.map(parte => parte.charAt(0).toUpperCase() + parte.slice(1))
.join(' '),
email: usuario.email.toLowerCase(),
categoria: usuario.salario >= 6000 ? 'senior' : 'junior'
}))
.filter(usuario => usuario.idade >= 25);
console.log(usuariosProcessados);
2. Operações Matemáticas e Estatísticas
const vendas = [1200, 800, 1500, 950, 1800, 700, 1100];
const estatisticas = {
total: vendas.reduce((acc, v) => acc + v, 0),
media: vendas.reduce((acc, v) => acc + v, 0) / vendas.length,
maximo: vendas.reduce((acc, v) => Math.max(acc, v), 0),
minimo: vendas.reduce((acc, v) => Math.min(acc, v), Infinity),
acimaDaMedia: vendas.filter(v => v > vendas.reduce((acc, v) => acc + v, 0) / vendas.length)
};
console.log(estatisticas);
Quando NÃO Usar:
1. Loops Complexos com Break/Continue
// MELHOR usar imperativo para lógica complexa de loop
function encontrarSequencia(numeros, tamanhoSequencia) {
for (let i = 0; i <= numeros.length - tamanhoSequencia; i++) {
let sequenciaValida = true;
for (let j = 0; j < tamanhoSequencia - 1; j++) {
if (numeros[i + j] + 1 !== numeros[i + j + 1]) {
sequenciaValida = false;
break; // Break é mais claro aqui
}
}
if (sequenciaValida) {
return i; // Return direto é mais eficiente
}
}
return -1;
}
A programação funcional em JavaScript oferece uma abordagem poderosa para escrever código mais limpo,
previsível e testável. É especialmente útil para transformação e processamento de dados, mas deve ser
combinada com outros paradigmas quando apropriado.
1.2.3 Programação Orientada a Objetos em JavaScript
Organização através de objetos que encapsulam dados e comportamentos
A Programação Orientada a Objetos (OOP) é um paradigma que organiza código em objetos - estruturas que
combinam dados (propriedades) e funções (métodos) que operam sobre esses dados. JavaScript suporta OOP de
forma única, usando protótipos em vez de classes tradicionais, embora também ofereça sintaxe de classes
modernas.
Conceitos Fundamentais da OOP
1. Encapsulamento - Agrupa dados e métodos relacionados - Controla acesso aos dados internos - Esconde
detalhes de implementação
2. Herança - Permite que objetos herdem propriedades de outros objetos - Promove reutilização de código -
Cria hierarquias de objetos
3. Polimorfismo - Objetos diferentes podem responder à mesma interface - Métodos podem ser sobrescritos
em subclasses - Flexibilidade na implementação
4. Abstração - Esconde complexidade interna - Fornece interface simples e clara - Foca no “o que” não no
“como”
JavaScript: Prototype-based OOP (ES5 e antes)
JavaScript usa protótipos em vez de classes tradicionais. Cada objeto pode servir como protótipo para outros
objetos:
// Constructor function - função construtora
function Animal(name, species) {
// Propriedades de instância
this.name = name;
this.species = species;
this.energy = 100;
}
// Métodos compartilhados no prototype
Animal.prototype.speak = function() {
this.energy -= 5;
return `${this.name} makes a sound (energy: ${this.energy})`;
};
Animal.prototype.eat = function(food) {
this.energy += 10;
return `${this.name} eats ${food} and gains energy (${this.energy})`;
};
Animal.prototype.sleep = function() {
this.energy = 100;
return `${this.name} sleeps and recovers full energy`;
};
Animal.prototype.getInfo = function() {
return {
name: this.name,
species: this.species,
energy: this.energy,
isHealthy: this.energy > 50
};
};
// Herança via prototype chain
function Dog(name, breed, size) {
// Chama o constructor da classe pai
Animal.call(this, name, 'Dog');
this.breed = breed;
this.size = size;
this.tricks = [];
}
// Estabelece herança do prototype
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
// Sobrescrevendo método (polimorfismo)
Dog.prototype.speak = function() {
this.energy -= 3; // Cães gastam menos energia latindo
return `${this.name} barks loudly! Woof! (energy: ${this.energy})`;
};
// Método específico de Dog
Dog.prototype.fetch = function(item) {
this.energy -= 15;
return `${this.name} fetches the ${item} and brings it back! (energy: ${this.energy})`;
};
Dog.prototype.learnTrick = function(trick) {
this.tricks.push(trick);
return `${this.name} learned to ${trick}! Total tricks: ${this.tricks.length}`;
};
Dog.prototype.performTrick = function() {
if (this.tricks.length === 0) {
return `${this.name} doesn't know any tricks yet`;
}
const randomTrick = this.tricks[Math.floor(Math.random() * this.tricks.length)];
this.energy -= 10;
return `${this.name} performs ${randomTrick}! (energy: ${this.energy})`;
};
// Subclasse específica de Dog
function ServiceDog(name, breed, serviceType) {
Dog.call(this, name, breed, 'Large');
this.serviceType = serviceType;
this.isWorking = false;
}
ServiceDog.prototype = Object.create(Dog.prototype);
ServiceDog.prototype.constructor = ServiceDog;
ServiceDog.prototype.startWork = function() {
this.isWorking = true;
return `${this.name} starts ${this.serviceType} service`;
};
ServiceDog.prototype.assist = function(task) {
if (!this.isWorking) {
return `${this.name} is not currently working`;
}
this.energy -= 20;
return `${this.name} assists with ${task} (${this.serviceType} dog)`;
};
// Usando as classes
const animal = new Animal("Generic Animal", "Unknown");
console.log(animal.speak());
console.log(animal.eat("grass"));
console.log(animal.getInfo());
const dog = new Dog("Rex", "Golden Retriever", "Large");
console.log(dog.speak()); // Method override
console.log(dog.fetch("stick")); // Dog-specific method
console.log(dog.learnTrick("sit"));
console.log(dog.learnTrick("roll over"));
console.log(dog.performTrick());
console.log(dog.getInfo()); // Inherited method
const serviceDog = new ServiceDog("Buddy", "German Shepherd", "Guide");
console.log(serviceDog.startWork());
console.log(serviceDog.assist("navigation"));
console.log(serviceDog.speak()); // Inherited from Dog
console.log(serviceDog.getInfo()); // Inherited from Animal
// Demonstrando polimorfismo
const animals = [
new Animal("Wild Fox", "Fox"),
new Dog("Max", "Beagle", "Medium"),
new ServiceDog("Luna", "Labrador", "Therapy")
];
console.log("\n=== POLIMORFISMO ===");
animals.forEach(animal => {
console.log(animal.speak()); // Cada um implementa speak() diferente
console.log(`${animal.name} info:`, animal.getInfo());
});
JavaScript: Class-based OOP (ES6+)
ES6 introduziu sintaxe de classes mais familiar, mas ainda usa protótipos internamente:
// Class syntax moderna
class Vehicle {
constructor(make, model, year) {
this.make = make;
this.model = model;
this.year = year;
this.mileage = 0;
this.isRunning = false;
}
// Método público
start() {
if (!this.isRunning) {
this.isRunning = true;
return `${this.make} ${this.model} started`;
}
return `${this.make} ${this.model} is already running`;
}
stop() {
if (this.isRunning) {
this.isRunning = false;
return `${this.make} ${this.model} stopped`;
}
return `${this.make} ${this.model} is already stopped`;
}
drive(distance) {
if (!this.isRunning) {
return `Cannot drive - ${this.model} is not running`;
}
this.mileage += distance;
return `Drove ${distance} miles. Total mileage: ${this.mileage}`;
}
// Getter
get age() {
return new Date().getFullYear() - this.year;
}
get info() {
return {
vehicle: `${this.year} ${this.make} ${this.model}`,
age: this.age,
mileage: this.mileage,
status: this.isRunning ? 'Running' : 'Stopped'
};
}
// Setter
set currentMileage(miles) {
if (miles >= this.mileage) {
this.mileage = miles;
} else {
throw new Error('Cannot decrease mileage');
}
}
// Static method - pertence à classe, não à instância
static compare(vehicle1, vehicle2) {
const age1 = vehicle1.age;
const age2 = vehicle2.age;
if (age1 < age2) return `${vehicle1.model} is newer`;
if (age1 > age2) return `${vehicle2.model} is newer`;
return 'Both vehicles are the same age';
}
}
// Herança com extends
class Car extends Vehicle {
constructor(make, model, year, doors, fuelType = 'gasoline') {
super(make, model, year); // Chama constructor da classe pai
this.doors = doors;
this.fuelType = fuelType;
this.fuel = 100; // Tank cheio
}
// Override do método drive
drive(distance) {
if (!this.isRunning) {
return `Cannot drive - ${this.model} is not running`;
}
const fuelNeeded = distance * 0.1; // 0.1 fuel per mile
if (this.fuel < fuelNeeded) {
return `Not enough fuel to drive ${distance} miles. Current fuel: ${this.fuel}%`;
}
this.mileage += distance;
this.fuel -= fuelNeeded;
return `Drove ${distance} miles. Fuel: ${this.fuel.toFixed(1)}%, Mileage: ${this.mileage}`;
}
refuel() {
this.fuel = 100;
return `${this.model} refueled to 100%`;
}
// Método específico de Car
honk() {
return `${this.model} goes BEEP BEEP!`;
}
// Override do getter info
get info() {
return {
...super.info, // Usa info da classe pai
doors: this.doors,
fuelType: this.fuelType,
fuel: `${this.fuel.toFixed(1)}%`
};
}
}
// Subclasse mais específica
class ElectricCar extends Car {
constructor(make, model, year, doors, batteryCapacity) {
super(make, model, year, doors, 'electric');
this.batteryCapacity = batteryCapacity; // kWh
this.charge = 100; // Bateria cheia
this.fuel = this.charge; // Para compatibilidade com classe pai
}
// Override específico para carros elétricos
drive(distance) {
if (!this.isRunning) {
return `Cannot drive - ${this.model} is not running`;
}
const chargeNeeded = distance * 0.2; // 0.2% charge per mile
if (this.charge < chargeNeeded) {
return `Not enough charge to drive ${distance} miles. Current charge: ${this.charge}%`;
}
this.mileage += distance;
this.charge -= chargeNeeded;
this.fuel = this.charge; // Sync para compatibilidade
return `Drove ${distance} miles electrically. Charge: ${this.charge.toFixed(1)}%, Mileage: ${this.mileage}`;
}
recharge() {
this.charge = 100;
this.fuel = this.charge;
return `${this.model} recharged to 100%`;
}
// Método específico
regenerativeBrake() {
const recovered = Math.min(5, 100 - this.charge);
this.charge += recovered;
this.fuel = this.charge;
return `Regenerative braking recovered ${recovered}% charge. Current: ${this.charge}%`;
}
get info() {
return {
...super.info,
batteryCapacity: `${this.batteryCapacity} kWh`,
charge: `${this.charge.toFixed(1)}%`,
range: `${((this.charge / 0.2)).toFixed(0)} miles`
};
}
}
// Testando as classes
const vehicle = new Vehicle('Generic', 'Vehicle', 2020);
console.log(vehicle.start());
console.log(vehicle.drive(50));
console.log(vehicle.info);
const car = new Car('Toyota', 'Camry', 2021, 4);
console.log(car.start());
console.log(car.drive(100));
console.log(car.honk());
console.log(car.refuel());
console.log(car.info);
const tesla = new ElectricCar('Tesla', 'Model 3', 2023, 4, 75);
console.log(tesla.start());
console.log(tesla.drive(150));
console.log(tesla.regenerativeBrake());
console.log(tesla.recharge());
console.log(tesla.info);
// Polimorfismo com classes modernas
const vehicles = [
new Vehicle('Generic', 'Machine', 2020),
new Car('Honda', 'Civic', 2022, 4),
new ElectricCar('BMW', 'i3', 2023, 4, 42)
];
console.log('\n=== POLIMORFISMO COM CLASSES ===');
vehicles.forEach(v => {
console.log(v.start());
console.log(v.drive(25)); // Cada classe implementa diferente
console.log(v.info);
console.log('---');
});
// Static methods
console.log(Vehicle.compare(car, tesla));
Encapsulamento Moderno com Private Fields
ES2022 introduziu fields privados reais com sintaxe #:
class BankAccount {
// Private fields - só acessíveis dentro da classe
#balance = 0;
#accountNumber;
#transactionHistory = [];
#isActive = true;
// Public fields
accountType = 'checking';
createdAt = new Date();
constructor(accountNumber, initialBalance = 0, accountType = 'checking') {
this.#accountNumber = accountNumber;
this.#balance = initialBalance;
this.accountType = accountType;
this.#addTransaction('ACCOUNT_CREATED', initialBalance, 'Account opened');
}
// Public methods
deposit(amount, description = '') {
if (!this.#isActive) {
throw new Error('Account is closed');
}
if (!this.#isValidAmount(amount)) {
throw new Error('Invalid deposit amount');
}
this.#balance += amount;
this.#addTransaction('DEPOSIT', amount, description);
return {
success: true,
newBalance: this.#balance,
message: `Deposited $${amount}`
};
}
withdraw(amount, description = '') {
if (!this.#isActive) {
throw new Error('Account is closed');
}
if (!this.#isValidAmount(amount)) {
throw new Error('Invalid withdrawal amount');
}
if (!this.#hasSufficientFunds(amount)) {
return {
success: false,
message: `Insufficient funds. Available: $${this.#balance}`
};
}
this.#balance -= amount;
this.#addTransaction('WITHDRAWAL', -amount, description);
return {
success: true,
newBalance: this.#balance,
message: `Withdrew $${amount}`
};
}
transfer(targetAccount, amount, description = '') {
const withdrawal = this.withdraw(amount, `Transfer to ${targetAccount.accountNumber}: ${description}`);
if (!withdrawal.success) {
return withdrawal;
}
try {
const deposit = targetAccount.deposit(amount, `Transfer from ${this.#accountNumber}: ${description}`);
return {
success: true,
message: `Transferred $${amount} to account ${targetAccount.accountNumber}`,
fromBalance: this.#balance,
toBalance: deposit.newBalance
};
} catch (error) {
// Rollback if target deposit fails
this.#balance += amount;
this.#addTransaction('REVERSAL', amount, 'Transfer reversal due to target account error');
throw new Error(`Transfer failed: ${error.message}`);
}
}
closeAccount() {
if (this.#balance > 0) {
throw new Error('Cannot close account with positive balance');
}
this.#isActive = false;
this.#addTransaction('ACCOUNT_CLOSED', 0, 'Account closed by customer');
return { success: true, message: 'Account closed successfully' };
}
// Getters - controlled access to private data
get balance() {
return this.#isActive ? this.#balance : 0;
}
get accountNumber() {
return this.#accountNumber;
}
get isActive() {
return this.#isActive;
}
get summary() {
return {
accountNumber: this.#accountNumber,
accountType: this.accountType,
balance: this.#balance,
isActive: this.#isActive,
totalTransactions: this.#transactionHistory.length,
createdAt: this.createdAt
};
}
getStatement(limit = 10) {
if (!this.#isActive) {
throw new Error('Cannot generate statement for closed account');
}
return {
accountNumber: this.#accountNumber,
currentBalance: this.#balance,
transactions: this.#transactionHistory
.slice(-limit)
.reverse()
.map(t => ({
date: t.date.toLocaleDateString(),
type: t.type,
amount: t.amount,
description: t.description,
balance: t.balanceAfter
}))
};
}
// Private methods - só acessíveis dentro da classe
#isValidAmount(amount) {
return typeof amount === 'number' && amount > 0 && isFinite(amount);
}
#hasSufficientFunds(amount) {
return this.#balance >= amount;
}
#addTransaction(type, amount, description) {
this.#transactionHistory.push({
id: this.#generateTransactionId(),
type,
amount,
description,
date: new Date(),
balanceAfter: this.#balance
});
}
#generateTransactionId() {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
}
// Subclasse com recursos especiais
class SavingsAccount extends BankAccount {
#interestRate;
#minimumBalance;
constructor(accountNumber, initialBalance = 0, interestRate = 0.02, minimumBalance = 100) {
super(accountNumber, initialBalance, 'savings');
this.#interestRate = interestRate;
this.#minimumBalance = minimumBalance;
}
// Override withdraw com regra de saldo mínimo
withdraw(amount, description = '') {
const futureBalance = this.balance - amount;
if (futureBalance < this.#minimumBalance) {
return {
success: false,
message: `Withdrawal would violate minimum balance requirement of $${this.#minimumBalance}`
};
}
return super.withdraw(amount, description);
}
calculateInterest() {
const interest = this.balance * (this.#interestRate / 12); // Monthly interest
if (interest > 0) {
const result = this.deposit(interest, `Monthly interest at ${(this.#interestRate * 100)}% APR`);
return {
...result,
interestEarned: interest,
newBalance: result.newBalance
};
}
return { interestEarned: 0, message: 'No interest earned' };
}
get accountDetails() {
return {
...this.summary,
interestRate: `${(this.#interestRate * 100)}% APR`,
minimumBalance: this.#minimumBalance
};
}
}
// Testando encapsulamento
console.log('=== TESTANDO ENCAPSULAMENTO ===');
const checkingAccount = new BankAccount('CHK-001', 500);
const savingsAccount = new SavingsAccount('SAV-001', 1000, 0.025, 500);
console.log('Checking account created:', checkingAccount.summary);
console.log('Savings account created:', savingsAccount.accountDetails);
// Operações normais
console.log(checkingAccount.deposit(200, 'Salary deposit'));
console.log(checkingAccount.withdraw(100, 'ATM withdrawal'));
// Tentativa de acesso a campo privado falha
try {
console.log(checkingAccount.#balance); // SyntaxError!
} catch (error) {
console.log('Cannot access private field directly');
}
// Mas podemos acessar via getter público
console.log('Balance via getter:', checkingAccount.balance);
// Transferência entre contas
console.log(checkingAccount.transfer(savingsAccount, 150, 'Emergency fund'));
// Interest calculation em savings account
console.log(savingsAccount.calculateInterest());
// Statement
console.log('Checking account statement:', checkingAccount.getStatement(5));
console.log('Final balances:');
console.log('Checking:', checkingAccount.balance);
console.log('Savings:', savingsAccount.balance);
A Programação Orientada a Objetos em JavaScript oferece uma forma poderosa de organizar e estruturar
código, especialmente para sistemas complexos que modelam entidades do mundo real. A evolução de
protótipos para classes modernas com campos privados tornou o JavaScript uma linguagem OOP muito mais
robusta e expressiva.
1.2.4 Event-Driven Programming
Responsivo a eventos do sistema ou usuário
// Event-driven com EventEmitter pattern
class EventEmitter {
constructor() {
this.events = {};
}
on(eventName, callback) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
this.events[eventName].push(callback);
}
emit(eventName, data) {
if (this.events[eventName]) {
this.events[eventName].forEach(callback => callback(data));
}
}
off(eventName, callbackToRemove) {
if (this.events[eventName]) {
this.events[eventName] = this.events[eventName]
.filter(callback => callback !== callbackToRemove);
}
}
}
// Uso prático
class UserService extends EventEmitter {
createUser(userData) {
// Simular criação
const user = { id: Date.now(), ...userData };
// Emit events
this.emit('user:created', user);
this.emit('notification:send', {
type: 'welcome',
userId: user.id
});
return user;
}
}
const userService = new UserService();
// Listeners
userService.on('user:created', (user) => {
console.log(`User created: ${user.name}`);
});
userService.on('notification:send', (notification) => {
console.log(`Sending ${notification.type} notification to ${notification.userId}`);
});
// Uso
userService.createUser({ name: 'John Doe', email: '[email protected]' });
1.2.5 Comparação Prática dos Paradigmas
Implementando o mesmo problema com diferentes abordagens
Problema: Sistema de carrinho de compras
Abordagem Imperativa:
function ShoppingCartImperative() {
this.items = [];
this.total = 0;
}
ShoppingCartImperative.prototype.addItem = function(item) {
this.items.push(item);
this.total += item.price;
console.log(`Added ${item.name} - Total: $${this.total}`);
};
ShoppingCartImperative.prototype.removeItem = function(itemId) {
for (let i = 0; i < this.items.length; i++) {
if (this.items[i].id === itemId) {
this.total -= this.items[i].price;
this.items.splice(i, 1);
break;
}
}
};
ShoppingCartImperative.prototype.getTotal = function() {
let total = 0;
for (let i = 0; i < this.items.length; i++) {
total += this.items[i].price;
}
return total;
};
Abordagem Funcional:
// Estado imutável
const createEmptyCart = () => ({ items: [], total: 0 });
const addItem = (cart, item) => ({
items: [...cart.items, item],
total: cart.total + item.price
});
const removeItem = (cart, itemId) => {
const filteredItems = cart.items.filter(item => item.id !== itemId);
const newTotal = filteredItems.reduce((sum, item) => sum + item.price, 0);
return { items: filteredItems, total: newTotal };
};
const getTotal = (cart) =>
cart.items.reduce((sum, item) => sum + item.price, 0);
const applyDiscount = (cart, discountPercent) => ({
...cart,
total: cart.total * (1 - discountPercent / 100)
});
// Pipe operations
const pipe = (...fns) => (value) => fns.reduce((acc, fn) => fn(acc), value);
// Uso funcional
let cart = createEmptyCart();
cart = addItem(cart, { id: 1, name: 'Laptop', price: 1000 });
cart = addItem(cart, { id: 2, name: 'Mouse', price: 20 });
cart = applyDiscount(cart, 10);
Abordagem OOP:
class ShoppingCart {
#items = [];
#discountRate = 0;
addItem(item) {
this.#items.push(item);
this.#notifyItemAdded(item);
return this;
}
removeItem(itemId) {
const index = this.#items.findIndex(item => item.id === itemId);
if (index !== -1) {
const removedItem = this.#items.splice(index, 1)[0];
this.#notifyItemRemoved(removedItem);
}
return this;
}
get total() {
const subtotal = this.#items.reduce((sum, item) => sum + item.price, 0);
return subtotal * (1 - this.#discountRate);
}
get itemCount() {
return this.#items.length;
}
applyDiscount(rate) {
this.#discountRate = rate / 100;
return this;
}
#notifyItemAdded(item) {
console.log(`Added ${item.name} to cart`);
}
#notifyItemRemoved(item) {
console.log(`Removed ${item.name} from cart`);
}
getItems() {
return [...this.#items]; // Return copy to maintain encapsulation
}
}
// Uso OOP
const cart = new ShoppingCart()
.addItem({ id: 1, name: 'Laptop', price: 1000 })
.addItem({ id: 2, name: 'Mouse', price: 20 })
.applyDiscount(10);
console.log(cart.total); // 918
1.2.6 Quando Usar Cada Paradigma
Baseado na pesquisa realizada
Use Programação Funcional quando:
Transformação de dados é central
Precisa de código altamente testável
Trabalhando com operações matemáticas/científicas
Aplicações com alta concorrência
Frameworks como React (estado imutável)
Processamento de listas e arrays
Exemplos de uso: - Transformação de dados em APIs - Validação de formulários - Processamento de arrays -
Cálculos financeiros - Pipeline de dados
Use OOP quando:
Modelagem de entidades do mundo real
Sistemas complexos com muitas interações
Encapsulamento de lógica de negócio
Reutilização através de herança/composição
Sistemas com estados complexos
Exemplos de uso: - Sistemas de gerenciamento - Games e simulações - Frameworks e bibliotecas - APIs REST
com models - Interfaces de usuário complexas
Use Programação Imperativa quando:
Performance é crítica
Controle fino sobre recursos
Algoritmos de baixo nível
Manipulação direta de memória
Sistemas embarcados
Exemplos de uso: - Algoritmos de ordenação customizados - Processamento de imagens - Operações
matemáticas intensivas - Otimizações específicas - Parsers e compiladores
Abordagem Híbrida (Recomendada):
JavaScript permite combinar paradigmas conforme necessário:
// Híbrido: OOP + Functional
class DataProcessor {
constructor(data) {
this.data = data;
}
// Método usando programação funcional
transform(transformers) {
return transformers.reduce((acc, transformer) =>
transformer(acc), this.data);
}
// Método usando programação imperativa (performance crítica)
fastSort() {
// Quick sort implementation for performance
for (let i = 0; i < this.data.length - 1; i++) {
for (let j = 0; j < this.data.length - i - 1; j++) {
if (this.data[j] > this.data[j + 1]) {
[this.data[j], this.data[j + 1]] = [this.data[j + 1], this.data[j]];
}
}
}
return this;
}
}
// Uso híbrido
const processor = new DataProcessor([3, 1, 4, 1, 5, 9]);
const result = processor
.fastSort() // Imperativo para performance
.transform([ // Funcional para transformações
arr => arr.map(x => x * 2),
arr => arr.filter(x => x > 5),
arr => arr.reduce((sum, x) => sum + x, 0)
]);
1.2.4 Event-Driven Programming em JavaScript
Como JavaScript gerencia eventos e programação assíncrona
A Programação Orientada a Eventos é um paradigma onde o fluxo do programa é determinado por eventos -
como cliques, chegada de dados, timers, ou interações do usuário. JavaScript foi projetado com este paradigma
em mente, sendo fundamentalmente event-driven e single-threaded com event loop.
Conceitos Fundamentais
1. Event Loop e Call Stack
// JavaScript é single-threaded mas não-bloqueante
console.log("1 - Início");
setTimeout(() => {
console.log("3 - Timeout executado");
}, 0);
console.log("2 - Fim");
// Output: 1 - Início, 2 - Fim, 3 - Timeout executado
// Mesmo com delay 0, o setTimeout vai para a queue de eventos
2. Listeners de Eventos
// Event listeners tradicionais
const button = document.getElementById('myButton');
// Método 1: addEventListener (recomendado)
button.addEventListener('click', function(event) {
console.log('Botão clicado!', event.target);
});
// Método 2: Propriedade onclick
button.onclick = function(event) {
console.log('Clique via propriedade');
};
// Método 3: HTML inline (não recomendado)
// <button onclick="handleClick()">Click me</button>
Event-Driven com DOM
Delegação de Eventos:
// Em vez de adicionar listener para cada item
document.getElementById('list').addEventListener('click', function(e) {
if (e.target.tagName === 'LI') {
console.log('Item clicado:', e.target.textContent);
}
});
// Funciona mesmo para elementos adicionados dinamicamente
const newItem = document.createElement('li');
newItem.textContent = 'Novo item';
document.getElementById('list').appendChild(newItem);
Custom Events:
// Criando eventos customizados
class ShoppingCart {
constructor() {
this.items = [];
this.element = document.createElement('div');
}
addItem(item) {
this.items.push(item);
// Disparar evento customizado
const event = new CustomEvent('itemAdded', {
detail: { item, totalItems: this.items.length }
});
this.element.dispatchEvent(event);
}
}
// Usando o evento customizado
const cart = new ShoppingCart();
cart.element.addEventListener('itemAdded', function(e) {
console.log('Item adicionado:', e.detail.item);
console.log('Total de itens:', e.detail.totalItems);
});
cart.addItem({ name: 'Produto A', price: 99.99 });
Event-Driven com Node.js
EventEmitter Pattern:
const EventEmitter = require('events');
class OrderProcessor extends EventEmitter {
processOrder(order) {
console.log('Processando pedido:', order.id);
// Simular processamento
setTimeout(() => {
if (Math.random() > 0.1) {
this.emit('orderProcessed', order);
} else {
this.emit('orderFailed', order, new Error('Falha no processamento'));
}
}, 1000);
}
}
// Uso do EventEmitter
const processor = new OrderProcessor();
processor.on('orderProcessed', (order) => {
console.log(' Pedido processado com sucesso:', order.id);
});
processor.on('orderFailed', (order, error) => {
console.log('❌ Falha no pedido:', order.id, error.message);
});
processor.processOrder({ id: 'ORDER-123', amount: 299.99 });
Programação Assíncrona Event-Driven
Promises com Events:
class ApiClient {
constructor() {
this.eventTarget = new EventTarget();
}
async fetchData(url) {
this.eventTarget.dispatchEvent(new Event('fetchStart'));
try {
const response = await fetch(url);
const data = await response.json();
this.eventTarget.dispatchEvent(new CustomEvent('fetchSuccess', {
detail: data
}));
return data;
} catch (error) {
this.eventTarget.dispatchEvent(new CustomEvent('fetchError', {
detail: error
}));
throw error;
}
}
on(event, callback) {
this.eventTarget.addEventListener(event, callback);
}
}
// Uso com monitoring de eventos
const api = new ApiClient();
api.on('fetchStart', () => console.log(' Iniciando requisição...'));
api.on('fetchSuccess', (e) => console.log(' Dados recebidos:', e.detail));
api.on('fetchError', (e) => console.log('❌ Erro na requisição:', e.detail));
api.fetchData('https://jsonplaceholder.typicode.com/posts/1');
Observer Pattern com Events
Implementação do Observer:
class Observable {
constructor() {
this.observers = new Map();
}
subscribe(event, callback) {
if (!this.observers.has(event)) {
this.observers.set(event, []);
}
this.observers.get(event).push(callback);
// Retorna função de unsubscribe
return () => {
const callbacks = this.observers.get(event);
const index = callbacks.indexOf(callback);
if (index > -1) {
callbacks.splice(index, 1);
}
};
}
emit(event, data) {
const callbacks = this.observers.get(event) || [];
callbacks.forEach(callback => callback(data));
}
}
// Sistema de notificações usando Observer
class NotificationSystem extends Observable {
constructor() {
super();
this.notifications = [];
}
addNotification(notification) {
this.notifications.push(notification);
this.emit('notificationAdded', notification);
if (notification.priority === 'urgent') {
this.emit('urgentNotification', notification);
}
}
markAsRead(id) {
const notification = this.notifications.find(n => n.id === id);
if (notification) {
notification.read = true;
this.emit('notificationRead', notification);
}
}
}
// Uso do sistema de notificações
const notifications = new NotificationSystem();
// Subscribers diferentes para diferentes eventos
const unsubscribeGeneral = notifications.subscribe('notificationAdded', (notification) => {
console.log(' Nova notificação:', notification.message);
});
const unsubscribeUrgent = notifications.subscribe('urgentNotification', (notification) => {
console.log(' URGENTE:', notification.message);
// Mostrar popup ou alerta
});
notifications.subscribe('notificationRead', (notification) => {
console.log(' Notificação lida:', notification.message);
});
// Testando o sistema
notifications.addNotification({
id: 1,
message: 'Você tem uma nova mensagem',
priority: 'normal'
});
notifications.addNotification({
id: 2,
message: 'Sistema será reiniciado em 5 minutos',
priority: 'urgent'
});
notifications.markAsRead(1);
Event-Driven com Streams
Streams em Node.js:
const { Readable, Transform, Writable } = require('stream');
// Readable stream que emite eventos
class NumberGenerator extends Readable {
constructor(options) {
super(options);
this.current = 0;
this.max = 10;
}
_read() {
if (this.current < this.max) {
this.push(String(this.current++));
} else {
this.push(null); // End stream
}
}
}
// Transform stream para processar dados
class SquareTransform extends Transform {
_transform(chunk, encoding, callback) {
const number = parseInt(chunk.toString());
const squared = number * number;
this.push(`${number}² = ${squared}\n`);
callback();
}
}
// Writable stream para output
class ConsoleWriter extends Writable {
_write(chunk, encoding, callback) {
console.log('Stream output:', chunk.toString().trim());
callback();
}
}
// Pipeline event-driven
const generator = new NumberGenerator();
const transformer = new SquareTransform();
const writer = new ConsoleWriter();
// Event listeners para monitoring
generator.on('data', () => console.log(' Número gerado'));
transformer.on('data', () => console.log(' Número transformado'));
writer.on('finish', () => console.log(' Stream finalizada'));
// Pipeline
generator
.pipe(transformer)
.pipe(writer);
Event-Driven Architecture Patterns
Pub/Sub com Message Bus:
class MessageBus {
constructor() {
this.channels = new Map();
}
publish(channel, message) {
const subscribers = this.channels.get(channel) || [];
subscribers.forEach(callback => {
try {
callback(message);
} catch (error) {
console.error(`Error in subscriber for ${channel}:`, error);
}
});
}
subscribe(channel, callback) {
if (!this.channels.has(channel)) {
this.channels.set(channel, []);
}
this.channels.get(channel).push(callback);
return () => {
const subscribers = this.channels.get(channel);
const index = subscribers.indexOf(callback);
if (index > -1) {
subscribers.splice(index, 1);
}
};
}
subscribeOnce(channel, callback) {
const unsubscribe = this.subscribe(channel, (message) => {
callback(message);
unsubscribe();
});
return unsubscribe;
}
}
// Sistema de e-commerce usando Message Bus
class ECommerceSystem {
constructor() {
this.bus = new MessageBus();
this.setupEventHandlers();
}
setupEventHandlers() {
// Sistema de inventory
this.bus.subscribe('order.created', (order) => {
console.log(' Verificando estoque para pedido:', order.id);
// Lógica de verificação de estoque
});
// Sistema de pagamento
this.bus.subscribe('order.created', (order) => {
console.log(' Processando pagamento para pedido:', order.id);
// Lógica de processamento de pagamento
});
// Sistema de notificação
this.bus.subscribe('order.created', (order) => {
console.log(' Enviando email de confirmação para:', order.customerEmail);
});
// Sistema de analytics
this.bus.subscribe('order.created', (order) => {
console.log(' Registrando evento no analytics');
});
}
createOrder(order) {
// Lógica de criação do pedido
console.log(' Pedido criado:', order.id);
// Publicar evento para todos os sistemas interessados
this.bus.publish('order.created', order);
}
}
// Uso do sistema
const ecommerce = new ECommerceSystem();
ecommerce.createOrder({
id: 'ORDER-456',
customerEmail: '[email protected]',
items: [
{ id: 'PROD-1', name: 'Produto A', quantity: 2 },
{ id: 'PROD-2', name: 'Produto B', quantity: 1 }
],
total: 299.99
});
Event-Driven vs Outros Paradigmas
Comparação Prática:
// IMPERATIVO: Processamento sequencial
function processOrderImperative(order) {
console.log('Validating order...');
if (!validateOrder(order)) {
throw new Error('Invalid order');
}
console.log('Checking inventory...');
if (!checkInventory(order.items)) {
throw new Error('Insufficient inventory');
}
console.log('Processing payment...');
const paymentResult = processPayment(order.payment);
if (!paymentResult.success) {
throw new Error('Payment failed');
}
console.log('Order processed successfully');
return { success: true, orderId: order.id };
}
// EVENT-DRIVEN: Processamento assíncrono com eventos
class EventDrivenOrderProcessor extends EventEmitter {
constructor() {
super();
this.setupHandlers();
}
setupHandlers() {
this.on('order.validate', this.validateOrder.bind(this));
this.on('order.checkInventory', this.checkInventory.bind(this));
this.on('order.processPayment', this.processPayment.bind(this));
}
processOrder(order) {
this.emit('order.validate', order);
}
validateOrder(order) {
setTimeout(() => {
if (order.items.length > 0) {
this.emit('order.checkInventory', order);
} else {
this.emit('order.failed', order, 'No items in order');
}
}, 100);
}
checkInventory(order) {
setTimeout(() => {
// Simular verificação assíncrona
this.emit('order.processPayment', order);
}, 200);
}
processPayment(order) {
setTimeout(() => {
this.emit('order.completed', order);
}, 300);
}
}
// Uso event-driven permite melhor responsividade
const processor = new EventDrivenOrderProcessor();
processor.on('order.completed', (order) => {
console.log(' Pedido processado:', order.id);
});
processor.on('order.failed', (order, reason) => {
console.log('❌ Pedido falhou:', reason);
});
processor.processOrder({ id: 'ORDER-789', items: ['item1'], payment: {} });
Vantagens do Event-Driven Programming
1. Desacoplamento: - Sistemas podem comunicar sem conhecer uns aos outros - Facilita manutenção e
escalabilidade - Permite adicionar novos listeners sem modificar publishers
2. Responsividade: - Não bloqueia a thread principal - Interface de usuário permanece responsiva - Melhor
experiência do usuário
3. Flexibilidade: - Fácil adicionar/remover comportamentos - Sistema extensível via eventos - Permite
diferentes tratamentos para o mesmo evento
4. Monitoramento e Debugging: - Events podem ser logados facilmente - Permite debugging distribuído -
Facilita monitoring de sistemas
Desvantagens e Cuidados
1. Complexidade de Debug:
// Pode ser difícil rastrear fluxo de eventos
emitter.on('start', () => {
console.log('Started');
emitter.emit('middle');
});
emitter.on('middle', () => {
console.log('Middle');
emitter.emit('end');
});
emitter.on('end', () => {
console.log('End');
});
emitter.emit('start'); // Fluxo não é óbvio olhando o código
2. Memory Leaks com Listeners:
// RUIM: Listeners não removidos
function createHandler() {
const handler = (data) => console.log(data);
eventEmitter.on('data', handler);
// Se essa função for chamada muitas vezes, teremos memory leak
}
// BOM: Sempre remover listeners quando não precisar
function createHandlerSafe() {
const handler = (data) => console.log(data);
eventEmitter.on('data', handler);
// Retornar função de cleanup
return () => eventEmitter.removeListener('data', handler);
}
3. Error Handling:
// Eventos não tratados podem quebrar a aplicação
const emitter = new EventEmitter();
emitter.on('error', (error) => {
console.error('Error handled:', error.message);
});
// Sem o listener 'error', isso crasharia a aplicação
emitter.emit('error', new Error('Something went wrong'));
Melhores Práticas
1. Sempre remover event listeners:
class Component {
constructor() {
this.handleClick = this.handleClick.bind(this);
}
mount() {
document.addEventListener('click', this.handleClick);
}
unmount() {
document.removeEventListener('click', this.handleClick);
}
handleClick(e) {
console.log('Click handled');
}
}
2. Use AbortController para controlar listeners:
const controller = new AbortController();
element.addEventListener('click', handleClick, {
signal: controller.signal
});
// Remove todos os listeners associados
controller.abort();
3. Namespacing de eventos:
// Em vez de eventos genéricos
emitter.emit('update', data);
// Use namespaces específicos
emitter.emit('user.profile.update', data);
emitter.emit('order.status.update', data);
O Event-Driven Programming é essencial em JavaScript, especialmente para interfaces de usuário, APIs
assíncronas, e sistemas distribuídos. Dominar este paradigma é fundamental para criar aplicações JavaScript
responsivas e escaláveis.
1.2.5 Comparação Prática dos Paradigmas ⚖
Quando usar cada paradigma e como combiná-los efetivamente
Cada paradigma tem seus pontos fortes e contextos ideais de aplicação. Na prática, JavaScript moderno
combina todos os paradigmas de forma estratégica para criar código eficiente, legível e maintível.
Análise Comparativa por Contexto
1. Performance e Eficiência:
// IMPERATIVO: Melhor para loops de performance crítica
function findPrimesImperative(limit) {
const primes = [];
const isPrime = new Array(limit + 1).fill(true);
isPrime[0] = isPrime[1] = false;
for (let i = 2; i <= limit; i++) {
if (isPrime[i]) {
primes.push(i);
// Otimização imperativa direta
for (let j = i * i; j <= limit; j += i) {
isPrime[j] = false;
}
}
}
return primes;
}
// FUNCIONAL: Mais expressivo mas menos eficiente para este caso
function findPrimesFunctional(limit) {
return Array.from({ length: limit - 1 }, (_, i) => i + 2)
.filter(num =>
Array.from({ length: Math.sqrt(num) - 1 }, (_, i) => i + 2)
.every(divisor => num % divisor !== 0)
);
}
// Benchmark
console.time('Imperative');
findPrimesImperative(10000);
console.timeEnd('Imperative'); // ~5ms
console.time('Functional');
findPrimesFunctional(1000); // Menor limite devido à complexidade O(n²)
console.timeEnd('Functional'); // ~150ms
2. Legibilidade e Manutenibilidade:
// FUNCIONAL: Muito legível para transformações de dados
const processUserData = (users) =>
users
.filter(user => user.active)
.map(user => ({
id: user.id,
displayName: `${user.firstName} ${user.lastName}`,
email: user.email.toLowerCase(),
avatar: user.avatar || '/default-avatar.png'
}))
.sort((a, b) => a.displayName.localeCompare(b.displayName));
// IMPERATIVO: Mais verboso mas controle total
function processUserDataImperative(users) {
const result = [];
for (let i = 0; i < users.length; i++) {
const user = users[i];
if (user.active) {
const processedUser = {
id: user.id,
displayName: user.firstName + ' ' + user.lastName,
email: user.email.toLowerCase(),
avatar: user.avatar || '/default-avatar.png'
};
result.push(processedUser);
}
}
// Sort manual para controle total
for (let i = 0; i < result.length - 1; i++) {
for (let j = i + 1; j < result.length; j++) {
if (result[i].displayName > result[j].displayName) {
[result[i], result[j]] = [result[j], result[i]];
}
}
}
return result;
}
3. Escalabilidade e Organização:
// OOP: Excelente para sistemas complexos com estado
class GameEngine {
constructor() {
this.entities = new Map();
this.systems = [];
this.running = false;
}
addEntity(entity) {
this.entities.set(entity.id, entity);
return this;
}
addSystem(system) {
this.systems.push(system);
return this;
}
update(deltaTime) {
// Cada system processa suas entities relevantes
this.systems.forEach(system => {
const relevantEntities = Array.from(this.entities.values())
.filter(entity => system.canProcess(entity));
system.update(relevantEntities, deltaTime);
});
}
}
class MovementSystem {
canProcess(entity) {
return entity.components.has('position') && entity.components.has('velocity');
}
update(entities, deltaTime) {
entities.forEach(entity => {
const position = entity.components.get('position');
const velocity = entity.components.get('velocity');
position.x += velocity.x * deltaTime;
position.y += velocity.y * deltaTime;
});
}
}
// FUNCIONAL: Bom para transformações stateless
const gameStateReducer = (state, action) => {
switch (action.type) {
case 'MOVE_PLAYER':
return {
...state,
player: {
...state.player,
position: {
x: state.player.position.x + action.payload.dx,
y: state.player.position.y + action.payload.dy
}
}
};
case 'ADD_SCORE':
return {
...state,
score: state.score + action.payload.points
};
default:
return state;
}
};
Matriz de Decisão: Qual Paradigma Usar
Cenário Imperativo Funcional OOP Event-Driven
Performance crítica
Transformação de dados
Estado complexo
UI Interativa
Sistemas distribuídos
Matemática/Algoritmos
APIs e I/O
Testing
Padrões Híbridos Efetivos
1. Functional Core + Imperative Shell:
// Core funcional para lógica de negócio
const calculateOrderTotal = (items, discounts = [], taxes = []) =>
pipe(
items,
items => items.reduce((total, item) => total + (item.price * item.quantity), 0),
subtotal => discounts.reduce((total, discount) => total - discount.amount, subtotal),
discountedTotal => taxes.reduce((total, tax) => total + (total * tax.rate), discountedTotal),
total => Math.round(total * 100) / 100
);
// Shell imperativo para efeitos colaterais
class OrderService {
async processOrder(orderData) {
try {
// Validação imperativa
this.validateOrderData(orderData);
// Lógica funcional pura
const total = calculateOrderTotal(
orderData.items,
orderData.discounts,
orderData.taxes
);
// Efeitos colaterais imperativos
const order = await this.saveOrderToDatabase({
...orderData,
total,
createdAt: new Date()
});
await this.sendConfirmationEmail(order);
return order;
} catch (error) {
this.logError(error);
throw error;
}
}
validateOrderData(data) {
if (!data.items || data.items.length === 0) {
throw new Error('Order must have at least one item');
}
// Mais validações...
}
}
2. OOP Structure + Functional Operations:
class DataProcessor {
constructor(config) {
this.config = config;
this.middlewares = [];
}
// OOP para estrutura e encapsulamento
use(middleware) {
this.middlewares.push(middleware);
return this;
}
// Funcional para transformações
process(data) {
return this.middlewares.reduce(
(result, middleware) => middleware(result, this.config),
data
);
}
}
// Middlewares funcionais puros
const validateData = (data, config) => {
if (!Array.isArray(data)) {
throw new Error('Data must be an array');
}
return data;
};
const filterValid = (data, config) =>
data.filter(item => item && typeof item === 'object');
const transformItems = (data, config) =>
data.map(item => ({
...item,
processed: true,
timestamp: Date.now()
}));
const sortByField = (data, config) =>
data.sort((a, b) => a[config.sortField] - b[config.sortField]);
// Uso híbrido
const processor = new DataProcessor({ sortField: 'priority' });
const result = processor
.use(validateData)
.use(filterValid)
.use(transformItems)
.use(sortByField)
.process([
{ id: 1, priority: 3, name: 'Task A' },
{ id: 2, priority: 1, name: 'Task B' },
{ id: 3, priority: 2, name: 'Task C' }
]);
3. Event-Driven + Functional Reactive:
class ReactiveStore {
constructor() {
this.state = {};
this.listeners = new Map();
this.middlewares = [];
}
// Event-driven para reatividade
subscribe(selector, callback) {
const key = selector.toString();
if (!this.listeners.has(key)) {
this.listeners.set(key, []);
}
this.listeners.get(key).push(callback);
return () => {
const callbacks = this.listeners.get(key);
const index = callbacks.indexOf(callback);
if (index > -1) callbacks.splice(index, 1);
};
}
// Funcional para transformações de estado
dispatch(action) {
const newState = this.middlewares.reduce(
(state, middleware) => middleware(state, action),
this.state
);
const changed = JSON.stringify(newState) !== JSON.stringify(this.state);
if (changed) {
this.state = newState;
this.notifyListeners();
}
}
use(middleware) {
this.middlewares.push(middleware);
return this;
}
notifyListeners() {
this.listeners.forEach((callbacks, selectorString) => {
const selector = eval(`(${selectorString})`);
const selectedState = selector(this.state);
callbacks.forEach(callback => callback(selectedState));
});
}
}
// Middleware funcional para logging
const loggingMiddleware = (state, action) => {
console.log('Action:', action.type, 'Payload:', action.payload);
return state;
};
// Reducer funcional
const counterReducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: (state.count || 0) + 1 };
case 'DECREMENT':
return { ...state, count: (state.count || 0) - 1 };
default:
return state;
}
};
// Uso híbrido
const store = new ReactiveStore();
store.use(loggingMiddleware).use(counterReducer);
// Reatividade event-driven
store.subscribe(state => state.count, (count) => {
console.log('Count changed:', count);
});
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'DECREMENT' });
Critérios para Escolha de Paradigma
Performance:
// Use imperativo quando performance é crítica
const fastSort = (arr) => {
// QuickSort imperativo
if (arr.length <= 1) return arr;
const pivot = arr[Math.floor(arr.length / 2)];
const left = [];
const right = [];
const equal = [];
for (let i = 0; i < arr.length; i++) {
if (arr[i] < pivot) left.push(arr[i]);
else if (arr[i] > pivot) right.push(arr[i]);
else equal.push(arr[i]);
}
return [...fastSort(left), ...equal, ...fastSort(right)];
};
// Use funcional quando clareza é mais importante
const readableSort = (arr, compareFn = (a, b) => a - b) =>
[...arr].sort(compareFn);
Complexidade de Estado:
// OOP para estado complexo e comportamentos relacionados
class GamePlayer {
constructor(name) {
this.name = name;
this.health = 100;
this.inventory = new Map();
this.skills = new Set();
this.position = { x: 0, y: 0 };
this.experience = 0;
}
takeDamage(amount, source) {
this.health = Math.max(0, this.health - amount);
if (this.health === 0) {
this.onDeath(source);
}
return this;
}
addItem(item) {
const current = this.inventory.get(item.type) || 0;
this.inventory.set(item.type, current + item.quantity);
return this;
}
levelUp() {
this.experience = 0;
this.health += 20;
return this;
}
}
// Funcional para estado simples e transformações
const updatePlayerStats = (player, action) => ({
...player,
health: action.type === 'DAMAGE'
? Math.max(0, player.health - action.amount)
: player.health,
experience: action.type === 'GAIN_XP'
? player.experience + action.amount
: player.experience
});
Interatividade e Eventos:
// Event-driven para UIs interativas
class InteractiveChart {
constructor(container, data) {
this.container = container;
this.data = data;
this.eventEmitter = new EventTarget();
this.setupEventListeners();
}
setupEventListeners() {
this.container.addEventListener('mouseover', (e) => {
if (e.target.classList.contains('data-point')) {
this.eventEmitter.dispatchEvent(new CustomEvent('pointHover', {
detail: { element: e.target, data: this.getDataForElement(e.target) }
}));
}
});
this.container.addEventListener('click', (e) => {
if (e.target.classList.contains('data-point')) {
this.eventEmitter.dispatchEvent(new CustomEvent('pointClick', {
detail: { element: e.target, data: this.getDataForElement(e.target) }
}));
}
});
}
on(event, callback) {
this.eventEmitter.addEventListener(event, callback);
return this;
}
}
Recomendações Gerais
Para Aplicações Web: - Frontend UI: Event-driven + Functional (React/Vue style) - State Management:
Functional + OOP - Business Logic: Functional core + Imperative shell - Performance Critical: Imperativo
Para APIs e Backend: - Request Handling: Event-driven + Functional - Data Processing: Functional -
Complex Domain Logic: OOP + Functional - Database Operations: Imperativo + Event-driven
Para Bibliotecas: - Utilities: Funcional - Frameworks: OOP + Event-driven - Performance Libraries:
Imperativo - Reactive Libraries: Event-driven + Functional
A chave para código JavaScript efetivo é combinar paradigmas de forma intencional, usando cada um em
seu contexto ideal, criando código que é ao mesmo tempo performante, maintível e expressivo.
1.3 SOLID PRINCIPLES EM JAVASCRIPT
Como aplicar princípios de design sólido em JavaScript
1.3.1 Single Responsibility Principle (SRP)
“Uma classe deve ter apenas uma razão para mudar”
Violação do SRP:
// RUIM: Muitas responsabilidades
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
// Responsabilidade 1: Gerenciar dados do usuário
getName() {
return this.name;
}
// Responsabilidade 2: Validação
validateEmail() {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.email);
}
// Responsabilidade 3: Persistência
saveToDatabase() {
// Código para salvar no banco
console.log('Saving to database...');
}
// Responsabilidade 4: Envio de email
sendWelcomeEmail() {
// Código para enviar email
console.log('Sending welcome email...');
}
}
Aplicando SRP corretamente:
// BOM: Responsabilidade única por classe
class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
getName() {
return this.name;
}
getEmail() {
return this.email;
}
}
class EmailValidator {
static validate(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
}
class UserRepository {
save(user) {
// Código para salvar no banco
console.log(`Saving user ${user.getName()} to database...`);
return true;
}
findById(id) {
// Código para buscar por ID
return null;
}
}
class EmailService {
sendWelcomeEmail(user) {
if (EmailValidator.validate(user.getEmail())) {
console.log(`Sending welcome email to ${user.getEmail()}`);
}
}
}
// Uso
const user = new User("John Doe", "[email protected]");
const repository = new UserRepository();
const emailService = new EmailService();
repository.save(user);
emailService.sendWelcomeEmail(user);
1.3.2 Open-Closed Principle (OCP)
“Aberto para extensão, fechado para modificação”
Violação do OCP:
// RUIM: Precisa modificar a classe para adicionar novos tipos
class DiscountCalculator {
calculate(customer, amount) {
if (customer.type === 'regular') {
return amount * 0.1;
} else if (customer.type === 'premium') {
return amount * 0.2;
} else if (customer.type === 'vip') { // Nova adição requer modificação
return amount * 0.3;
}
return 0;
}
}
Aplicando OCP corretamente:
// BOM: Extensível sem modificação
class DiscountStrategy {
calculate(amount) {
throw new Error('Must implement calculate method');
}
}
class RegularCustomerDiscount extends DiscountStrategy {
calculate(amount) {
return amount * 0.1;
}
}
class PremiumCustomerDiscount extends DiscountStrategy {
calculate(amount) {
return amount * 0.2;
}
}
class VipCustomerDiscount extends DiscountStrategy {
calculate(amount) {
return amount * 0.3;
}
}
class DiscountCalculator {
constructor(strategy) {
this.strategy = strategy;
}
calculate(amount) {
return this.strategy.calculate(amount);
}
setStrategy(strategy) {
this.strategy = strategy;
}
}
// Uso - Extensível sem modificar código existente
const regularCalculator = new DiscountCalculator(new RegularCustomerDiscount());
const premiumCalculator = new DiscountCalculator(new PremiumCustomerDiscount());
console.log(regularCalculator.calculate(100)); // 10
console.log(premiumCalculator.calculate(100)); // 20
// Nova estratégia sem modificar código existente
class StudentDiscount extends DiscountStrategy {
calculate(amount) {
return amount * 0.5;
}
}
const studentCalculator = new DiscountCalculator(new StudentDiscount());
console.log(studentCalculator.calculate(100)); // 50
1.3.3 Liskov Substitution Principle (LSP)
“Objetos de uma superclasse devem ser substituíveis por objetos de suas subclasses”
Violação do LSP:
// RUIM: Square viola o comportamento esperado de Rectangle
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
setWidth(width) {
this.width = width;
}
setHeight(height) {
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
class Square extends Rectangle {
constructor(size) {
super(size, size);
}
setWidth(width) {
this.width = width;
this.height = width; // Quebra o comportamento esperado
}
setHeight(height) {
this.height = height;
this.width = height; // Quebra o comportamento esperado
}
}
// Teste que falha com LSP violation
function testRectangle(rectangle) {
rectangle.setWidth(5);
rectangle.setHeight(4);
console.log(`Expected area: 20, Actual area: ${rectangle.getArea()}`);
// Rectangle: area = 20 ✓
// Square: area = 16 ✗ (LSP violation)
}
Aplicando LSP corretamente:
// BOM: Hierarquia que respeita LSP
class Shape {
getArea() {
throw new Error('Must implement getArea method');
}
}
class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
setDimensions(width, height) {
this.width = width;
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
class Square extends Shape {
constructor(size) {
super();
this.size = size;
}
setSize(size) {
this.size = size;
}
getArea() {
return this.size * this.size;
}
}
// Agora ambos podem ser usados como Shape sem problemas
function printArea(shape) {
console.log(`Area: ${shape.getArea()}`);
}
const rectangle = new Rectangle(5, 4);
const square = new Square(4);
printArea(rectangle); // Area: 20
printArea(square); // Area: 16
1.3.4 Interface Segregation Principle (ISP)
“Clientes não devem depender de interfaces que não usam”
Violação do ISP:
// RUIM: Interface muito grande força implementações desnecessárias
class MultiFunctionDevice {
print(document) {
throw new Error('Must implement');
}
scan(document) {
throw new Error('Must implement');
}
fax(document) {
throw new Error('Must implement');
}
photocopy(document) {
throw new Error('Must implement');
}
}
class SimplePrinter extends MultiFunctionDevice {
print(document) {
console.log(`Printing: ${document}`);
}
// Forçado a implementar métodos que não usa
scan(document) {
throw new Error('Scan not supported');
}
fax(document) {
throw new Error('Fax not supported');
}
photocopy(document) {
throw new Error('Photocopy not supported');
}
}
Aplicando ISP corretamente:
// BOM: Interfaces menores e específicas
class Printer {
print(document) {
throw new Error('Must implement print');
}
}
class Scanner {
scan(document) {
throw new Error('Must implement scan');
}
}
class FaxMachine {
fax(document) {
throw new Error('Must implement fax');
}
}
// Implementações específicas
class SimplePrinter extends Printer {
print(document) {
console.log(`Printing: ${document}`);
}
}
class MultiFunctionPrinter extends Printer {
constructor() {
super();
this.scanner = new SimpleScanner();
this.faxMachine = new SimpleFax();
}
print(document) {
console.log(`Printing: ${document}`);
}
scan(document) {
return this.scanner.scan(document);
}
fax(document) {
return this.faxMachine.fax(document);
}
}
class SimpleScanner extends Scanner {
scan(document) {
console.log(`Scanning: ${document}`);
return `Scanned version of ${document}`;
}
}
class SimpleFax extends FaxMachine {
fax(document) {
console.log(`Faxing: ${document}`);
}
}
// Uso - cada classe só implementa o que precisa
const printer = new SimplePrinter();
const multiFunction = new MultiFunctionPrinter();
printer.print("Document1.pdf");
multiFunction.print("Document2.pdf");
multiFunction.scan("Document3.pdf");
1.3.5 Dependency Inversion Principle (DIP)
“Dependa de abstrações, não de implementações concretas”
Violação do DIP:
// RUIM: Alto nível depende de baixo nível
class MySQLDatabase {
save(data) {
console.log(`Saving to MySQL: ${JSON.stringify(data)}`);
}
}
class OrderService {
constructor() {
this.database = new MySQLDatabase(); // Dependência concreta
}
createOrder(orderData) {
// Lógica de negócio
const order = {
id: Date.now(),
...orderData,
createdAt: new Date()
};
// Dependente de implementação específica
this.database.save(order);
return order;
}
}
Aplicando DIP corretamente:
// BOM: Dependência de abstração
class DatabaseInterface {
save(data) {
throw new Error('Must implement save method');
}
findById(id) {
throw new Error('Must implement findById method');
}
}
class MySQLDatabase extends DatabaseInterface {
save(data) {
console.log(`Saving to MySQL: ${JSON.stringify(data)}`);
return true;
}
findById(id) {
console.log(`Finding in MySQL by ID: ${id}`);
return { id, found: true };
}
}
class PostgreSQLDatabase extends DatabaseInterface {
save(data) {
console.log(`Saving to PostgreSQL: ${JSON.stringify(data)}`);
return true;
}
findById(id) {
console.log(`Finding in PostgreSQL by ID: ${id}`);
return { id, found: true };
}
}
class MongoDatabase extends DatabaseInterface {
save(data) {
console.log(`Saving to MongoDB: ${JSON.stringify(data)}`);
return true;
}
findById(id) {
console.log(`Finding in MongoDB by ID: ${id}`);
return { id, found: true };
}
}
// Serviço depende de abstração, não implementação
class OrderService {
constructor(database) {
if (!(database instanceof DatabaseInterface)) {
throw new Error('Database must implement DatabaseInterface');
}
this.database = database;
}
createOrder(orderData) {
const order = {
id: Date.now(),
...orderData,
createdAt: new Date()
};
this.database.save(order);
return order;
}
getOrder(id) {
return this.database.findById(id);
}
}
// Uso - Inversão de dependência via injeção
const mysqlDb = new MySQLDatabase();
const postgresDb = new PostgreSQLDatabase();
const mongoDb = new MongoDatabase();
const orderService1 = new OrderService(mysqlDb);
const orderService2 = new OrderService(postgresDb);
const orderService3 = new OrderService(mongoDb);
// Todos funcionam da mesma forma
orderService1.createOrder({ product: 'Laptop', price: 1000 });
orderService2.createOrder({ product: 'Mouse', price: 50 });
orderService3.createOrder({ product: 'Keyboard', price: 100 });
1.3.6 SOLID em Arquitetura JavaScript Moderna
Aplicando SOLID em contextos reais
// Exemplo completo: Sistema de notificações com SOLID
// SRP: Cada classe tem uma responsabilidade
class Notification {
constructor(message, recipient) {
this.message = message;
this.recipient = recipient;
this.timestamp = new Date();
}
}
// ISP: Interfaces específicas
class NotificationSender {
send(notification) {
throw new Error('Must implement send method');
}
}
class NotificationFormatter {
format(notification) {
throw new Error('Must implement format method');
}
}
// OCP: Extensível sem modificação
class EmailNotificationSender extends NotificationSender {
send(notification) {
console.log(` Email sent to ${notification.recipient}: ${notification.message}`);
}
}
class SMSNotificationSender extends NotificationSender {
send(notification) {
console.log(` SMS sent to ${notification.recipient}: ${notification.message}`);
}
}
class PushNotificationSender extends NotificationSender {
send(notification) {
console.log(` Push sent to ${notification.recipient}: ${notification.message}`);
}
}
// LSP: Formatters substituíveis
class SimpleFormatter extends NotificationFormatter {
format(notification) {
return `${notification.message}`;
}
}
class RichFormatter extends NotificationFormatter {
format(notification) {
return `
[${notification.timestamp.toISOString()}]
To: ${notification.recipient}
Message: ${notification.message}
`.trim();
}
}
// DIP: Service depende de abstrações
class NotificationService {
constructor(sender, formatter = new SimpleFormatter()) {
if (!(sender instanceof NotificationSender)) {
throw new Error('Sender must implement NotificationSender');
}
if (!(formatter instanceof NotificationFormatter)) {
throw new Error('Formatter must implement NotificationFormatter');
}
this.sender = sender;
this.formatter = formatter;
}
sendNotification(message, recipient) {
const notification = new Notification(message, recipient);
notification.message = this.formatter.format(notification);
this.sender.send(notification);
}
}
// Factory para criação (também segue SOLID)
class NotificationServiceFactory {
static createEmailService() {
return new NotificationService(
new EmailNotificationSender(),
new RichFormatter()
);
}
static createSMSService() {
return new NotificationService(
new SMSNotificationSender(),
new SimpleFormatter()
);
}
static createPushService() {
return new NotificationService(
new PushNotificationSender(),
new SimpleFormatter()
);
}
}
// Uso
const emailService = NotificationServiceFactory.createEmailService();
const smsService = NotificationServiceFactory.createSMSService();
const pushService = NotificationServiceFactory.createPushService();
emailService.sendNotification("Welcome to our platform!", "[email protected]");
smsService.sendNotification("Your code is: 123456", "+1234567890");
pushService.sendNotification("You have a new message", "user123");
1.3.7 Benefícios dos SOLID Principles
Por que aplicar SOLID em JavaScript
Vantagens: - Manutenibilidade: Código mais fácil de manter e modificar - Testabilidade: Classes com
responsabilidades únicas são mais testáveis - Flexibilidade: Fácil extensão sem quebrar código existente -
Reutilização: Componentes bem definidos são mais reutilizáveis - Debugging: Problemas isolados em
responsabilidades específicas
Quando aplicar SOLID: - Projetos de médio e grande porte - Código que será mantido por equipes - Sistemas
com requisitos que mudam frequentemente - Aplicações que precisam de alta testabilidade - Arquiteturas que
exigem baixo acoplamento
1.4 TYPESCRIPT VS JAVASCRIPT: QUANDO USAR
Análise detalhada para tomar a decisão correta
1.4.1 JavaScript: Características e Limitações
Tipagem Dinâmica:
// JavaScript - Tipagem dinâmica
let value = 42; // number
value = "Hello"; // string - OK em runtime
value = true; // boolean - OK em runtime
value = null; // null - OK em runtime
function add(a, b) {
return a + b; // Sem garantia de tipos
}
console.log(add(5, 10)); // 15 (number + number)
console.log(add("5", 10)); // "510" (string + number = concatenação!)
console.log(add(5, "10")); // "510" (coerção automática)
console.log(add("5", "10")); // "510" (string + string)
Problemas Comuns em JavaScript:
// 1. Erros de tipos não detectados
function calculateDiscount(price, percentage) {
return price - (price * percentage / 100);
}
// Funciona mas pode dar erro
calculateDiscount("100", "10"); // "100" - NaN
calculateDiscount(null, 10); // NaN
calculateDiscount(100); // NaN (percentage undefined)
// 2. Propriedades inexistentes
const user = { name: "John", age: 30 };
console.log(user.naem); // undefined (typo não detectado)
// 3. Parâmetros incorretos
function formatCurrency(amount, currency) {
return `${currency}${amount.toFixed(2)}`;
}
formatCurrency("abc", "$"); // Runtime error: toFixed is not a function
1.4.2 TypeScript: Tipagem Estática
Same Code with TypeScript:
// TypeScript - Tipagem estática
let value: number = 42;
// value = "Hello"; // ❌ Error: Type 'string' is not assignable to type 'number'
// value = true; // ❌ Error: Type 'boolean' is not assignable to type 'number'
function add(a: number, b: number): number {
return a + b;
}
console.log(add(5, 10)); // 15
// console.log(add("5", 10)); // ❌ Compile error
// console.log(add(5, "10")); // ❌ Compile error
// Função com tipos seguros
function calculateDiscount(price: number, percentage: number): number {
return price - (price * percentage / 100);
}
// calculateDiscount("100", "10"); // ❌ Compile error
// calculateDiscount(null, 10); // ❌ Compile error
// calculateDiscount(100); // ❌ Error: Expected 2 arguments but got 1
Interfaces e Tipos Complexos:
interface User {
id: number;
name: string;
email: string;
age?: number; // Optional property
isActive: boolean;
}
interface UserWithMethods extends User {
getName(): string;
setEmail(email: string): void;
}
class UserClass implements UserWithMethods {
constructor(
public id: number,
public name: string,
public email: string,
public isActive: boolean = true,
public age?: number
) {}
getName(): string {
return this.name;
}
setEmail(email: string): void {
if (this.isValidEmail(email)) {
this.email = email;
}
}
private isValidEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
}
// Uso com type safety
const createUser = (userData: User): UserClass => {
return new UserClass(
userData.id,
userData.name,
userData.email,
userData.isActive,
userData.age
);
};
const user = createUser({
id: 1,
name: "John Doe",
email: "
[email protected]",
isActive: true
});
// console.log(user.naem); // ❌ Property 'naem' does not exist on type 'UserClass'
console.log(user.name); // Works fine
Generics para Reutilização:
// Generic types para flexibilidade com type safety
interface ApiResponse<T> {
data: T;
success: boolean;
message?: string;
}
interface Product {
id: number;
name: string;
price: number;
}
interface Order {
id: number;
userId: number;
products: Product[];
total: number;
}
class ApiClient {
async get<T>(url: string): Promise<ApiResponse<T>> {
// Simulated API call
return {
data: {} as T,
success: true,
message: "Success"
};
}
async post<TRequest, TResponse>(
url: string,
data: TRequest
): Promise<ApiResponse<TResponse>> {
return {
data: {} as TResponse,
success: true,
message: "Created"
};
}
}
// Usage with full type safety
const client = new ApiClient();
const getProduct = async (id: number): Promise<Product | null> => {
const response = await client.get<Product>(`/products/${id}`);
return response.success ? response.data : null;
};
const createOrder = async (orderData: Omit<Order, 'id'>): Promise<Order | null> => {
const response = await client.post<Omit<Order, 'id'>, Order>('/orders', orderData);
return response.success ? response.data : null;
};
1.4.3 Comparação Prática: JavaScript vs TypeScript
Cenário: Sistema de E-commerce
JavaScript Version:
// JavaScript - Sem type safety
class ShoppingCart {
constructor() {
this.items = [];
this.discount = 0;
}
addItem(item) {
// Sem garantia do formato de item
this.items.push(item);
}
calculateTotal() {
let total = 0;
for (let item of this.items) {
// Pode falhar se item não tiver price ou quantity
total += item.price * item.quantity;
}
return total * (1 - this.discount);
}
applyDiscount(discount) {
// Sem validação do tipo
this.discount = discount;
}
}
// Uso - erros só aparecerão em runtime
const cart = new ShoppingCart();
cart.addItem({ name: "Laptop" }); // Sem price e quantity!
cart.applyDiscount("10%"); // String ao invés de number!
console.log(cart.calculateTotal()); // NaN - erro só em runtime
TypeScript Version:
// TypeScript - Type safety completa
interface CartItem {
id: number;
name: string;
price: number;
quantity: number;
}
interface DiscountRule {
type: 'percentage' | 'fixed';
value: number;
minAmount?: number;
}
class ShoppingCart {
private items: CartItem[] = [];
private discountRules: DiscountRule[] = [];
addItem(item: CartItem): void {
const existingItem = this.items.find(i => i.id === item.id);
if (existingItem) {
existingItem.quantity += item.quantity;
} else {
this.items.push({ ...item });
}
}
removeItem(itemId: number): boolean {
const index = this.items.findIndex(item => item.id === itemId);
if (index > -1) {
this.items.splice(index, 1);
return true;
}
return false;
}
calculateSubtotal(): number {
return this.items.reduce((total, item) => {
return total + (item.price * item.quantity);
}, 0);
}
applyDiscount(rule: DiscountRule): number {
const subtotal = this.calculateSubtotal();
if (rule.minAmount && subtotal < rule.minAmount) {
return subtotal;
}
if (rule.type === 'percentage') {
return subtotal * (1 - rule.value / 100);
} else {
return Math.max(0, subtotal - rule.value);
}
}
getItems(): readonly CartItem[] {
return [...this.items]; // Return immutable copy
}
getTotalItems(): number {
return this.items.reduce((total, item) => total + item.quantity, 0);
}
}
// Uso - Erros detectados em compile time
const cart = new ShoppingCart();
cart.addItem({
id: 1,
name: "Laptop",
price: 1000,
quantity: 1
});
// cart.addItem({ name: "Mouse" }); // ❌ Error: Missing required properties
const discountRule: DiscountRule = {
type: 'percentage',
value: 10,
minAmount: 500
};
const total = cart.applyDiscount(discountRule);
console.log(`Total: $${total.toFixed(2)}`);
// cart.applyDiscount("10%"); // ❌ Error: Argument of type 'string' is not assignable
1.4.4 Quando Usar JavaScript
Cenários Ideais para JavaScript:
1. Prototipagem Rápida
2. Scripts Simples (< 100 linhas)
3. Projetos Pessoais Pequenos
4. Experimentação e Learning
5. Constraints de Setup (ambiente simples)
Exemplo - Script de Automação:
// JavaScript é ideal para scripts simples
const fs = require('fs');
const path = require('path');
// Renomear arquivos em massa
const renameFiles = (directory, pattern, replacement) => {
const files = fs.readdirSync(directory);
files.forEach(file => {
if (file.includes(pattern)) {
const oldPath = path.join(directory, file);
const newName = file.replace(pattern, replacement);
const newPath = path.join(directory, newName);
fs.renameSync(oldPath, newPath);
console.log(`Renamed: ${file} -> ${newName}`);
}
});
};
renameFiles('./images', 'IMG_', 'photo_');
1.4.5 Quando Usar TypeScript
Cenários Ideais para TypeScript:
1. Projetos Grandes (> 1000 linhas)
2. Equipes Múltiplas
3. Código Crítico (financeiro, médico, etc.)
4. APIs Complexas
5. Longo Prazo (manutenção > 1 ano)
6. Bibliotecas Públicas
Exemplo - Sistema Bancário:
// TypeScript é essencial para sistemas críticos
interface Account {
readonly id: string;
readonly customerId: string;
balance: number;
currency: 'USD' | 'EUR' | 'BRL';
status: 'active' | 'suspended' | 'closed';
createdAt: Date;
updatedAt: Date;
}
interface Transaction {
readonly id: string;
readonly accountId: string;
type: 'deposit' | 'withdrawal' | 'transfer';
amount: number;
description: string;
timestamp: Date;
}
interface TransferRequest {
fromAccountId: string;
toAccountId: string;
amount: number;
description?: string;
}
class BankingService {
private accounts = new Map<string, Account>();
private transactions: Transaction[] = [];
createAccount(customerId: string, initialBalance = 0): Account {
const account: Account = {
id: this.generateId(),
customerId,
balance: initialBalance,
currency: 'USD',
status: 'active',
createdAt: new Date(),
updatedAt: new Date()
};
this.accounts.set(account.id, account);
return account;
}
deposit(accountId: string, amount: number, description = ''): Transaction {
if (amount <= 0) {
throw new Error('Deposit amount must be positive');
}
const account = this.getAccount(accountId);
account.balance += amount;
account.updatedAt = new Date();
const transaction = this.createTransaction({
accountId,
type: 'deposit',
amount,
description
});
return transaction;
}
withdraw(accountId: string, amount: number, description = ''): Transaction {
if (amount <= 0) {
throw new Error('Withdrawal amount must be positive');
}
const account = this.getAccount(accountId);
if (account.balance < amount) {
throw new Error('Insufficient funds');
}
if (account.status !== 'active') {
throw new Error('Account is not active');
}
account.balance -= amount;
account.updatedAt = new Date();
return this.createTransaction({
accountId,
type: 'withdrawal',
amount,
description
});
}
transfer({ fromAccountId, toAccountId, amount, description = '' }: TransferRequest): {
debitTransaction: Transaction;
creditTransaction: Transaction;
} {
if (fromAccountId === toAccountId) {
throw new Error('Cannot transfer to the same account');
}
const fromAccount = this.getAccount(fromAccountId);
const toAccount = this.getAccount(toAccountId);
if (fromAccount.currency !== toAccount.currency) {
throw new Error('Currency mismatch not supported');
}
// Atomic transaction
const debitTransaction = this.withdraw(fromAccountId, amount, `Transfer to ${toAccountId}: ${description}`);
const creditTransaction = this.deposit(toAccountId, amount, `Transfer from ${fromAccountId}: ${description}`);
return { debitTransaction, creditTransaction };
}
private getAccount(accountId: string): Account {
const account = this.accounts.get(accountId);
if (!account) {
throw new Error(`Account ${accountId} not found`);
}
return account;
}
private createTransaction(data: Omit<Transaction, 'id' | 'timestamp'>): Transaction {
const transaction: Transaction = {
...data,
id: this.generateId(),
timestamp: new Date()
};
this.transactions.push(transaction);
return transaction;
}
private generateId(): string {
return Date.now().toString(36) + Math.random().toString(36).substr(2);
}
getAccountBalance(accountId: string): number {
return this.getAccount(accountId).balance;
}
getTransactionHistory(accountId: string, limit = 10): Transaction[] {
return this.transactions
.filter(t => t.accountId === accountId)
.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime())
.slice(0, limit);
}
}
// Uso - Type safety previne erros críticos
const banking = new BankingService();
const account1 = banking.createAccount('customer1', 1000);
const account2 = banking.createAccount('customer2', 500);
try {
const transfer = banking.transfer({
fromAccountId: account1.id,
toAccountId: account2.id,
amount: 200,
description: 'Payment for services'
});
console.log('Transfer successful:', transfer);
console.log('Account 1 balance:', banking.getAccountBalance(account1.id));
console.log('Account 2 balance:', banking.getAccountBalance(account2.id));
} catch (error) {
console.error('Transfer failed:', error.message);
}
// banking.transfer("invalid"); // ❌ Compile error: Missing required properties
// banking.deposit(account1.id, -100); // Runtime error caught by validation
1.4.6 Migração JavaScript → TypeScript
Estratégia Gradual:
// 1. JavaScript original
function processOrders(orders) {
return orders
.filter(order => order.status === 'pending')
.map(order => ({
...order,
total: order.items.reduce((sum, item) => sum + item.price * item.quantity, 0)
}));
}
// 2. TypeScript com any (primeira migração)
function processOrders(orders: any): any {
return orders
.filter((order: any) => order.status === 'pending')
.map((order: any) => ({
...order,
total: order.items.reduce((sum: any, item: any) => sum + item.price * item.quantity, 0)
}));
}
// 3. TypeScript com tipos específicos (refinamento)
interface OrderItem {
id: number;
name: string;
price: number;
quantity: number;
}
interface Order {
id: number;
status: 'pending' | 'processing' | 'completed' | 'cancelled';
items: OrderItem[];
customerId: number;
createdAt: Date;
total?: number;
}
function processOrders(orders: Order[]): Order[] {
return orders
.filter(order => order.status === 'pending')
.map(order => ({
...order,
total: order.items.reduce((sum, item) => sum + item.price * item.quantity, 0)
}));
}
1.4.7 Conclusão: JavaScript vs TypeScript 2024
Use JavaScript quando: - Projetos pequenos (< 500 linhas) - Prototipagem rápida - Scripts de automação -
Aprendizado inicial - Setup mínimo necessário
Use TypeScript quando: - Projetos médios/grandes (> 1000 linhas) - Equipes múltiplas - Código de longa
duração - APIs complexas - Sistemas críticos - Bibliotecas públicas
TypeScript não é obrigatório, mas é altamente recomendado para: - Aplicações empresariais - Projetos
open source - Sistemas que crescerão com o tempo - Quando type safety reduz custos de manutenção
A escolha entre JavaScript e TypeScript deve ser baseada no contexto do projeto, não em preferência pessoal
ou modismo.
CONCLUSÃO DA PARTE 1
Você completou o estudo profundo dos Fundamentos JavaScript! Agora você entende:
Como JavaScript funciona como linguagem multiparadigma
Paradigmas em prática: Imperativo, Funcional, OOP, Event-driven
SOLID Principles aplicados em JavaScript
JavaScript vs TypeScript: Quando usar cada um
Próximos Passos
A Parte 2 deste guia cobrirá: - Estruturas de dados em JavaScript - Algoritmos e padrões LeetCode -
Preparação para entrevistas FAANG - 85+ exercícios progressivos
PARTE 2: ALGORITMOS E ESTRUTURAS DE
DADOS
Do básico ao avançado - Preparação completa para entrevistas técnicas
2.1 ESTRUTURAS DE DADOS EM JAVASCRIPT
Dominando as estruturas fundamentais para algoritmos eficientes
2.1.1 Arrays - A Base de Tudo
Arrays são a estrutura de dados mais fundamental em JavaScript. Entender suas operações e complexidades é
crucial para entrevistas técnicas.
Criação e Inicialização
// Diferentes formas de criar arrays
const arr1 = []; // Array vazio
const arr2 = [1, 2, 3, 4, 5]; // Array literal
const arr3 = new Array(5); // Array com 5 slots vazios
const arr4 = new Array(1, 2, 3); // Array com elementos
const arr5 = Array.from({length: 5}, (_, i) => i + 1); // [1, 2, 3, 4, 5]
const arr6 = [...Array(5)].map((_, i) => i * 2); // [0, 2, 4, 6, 8]
// Array de tipos mistos (comum em JS)
const mixedArray = [1, "hello", {name: "John"}, [1, 2, 3], true];
Operações Básicas e Complexidade
// ACESSO - O(1)
const numbers = [10, 20, 30, 40, 50];
console.log(numbers[0]); // 10
console.log(numbers[2]); // 30
console.log(numbers.length); // 5
// INSERÇÃO
numbers.push(60); // O(1) - Adiciona no final
numbers.unshift(0); // O(n) - Adiciona no início (move todos)
numbers.splice(2, 0, 25); // O(n) - Insere no meio
console.log(numbers); // [0, 10, 20, 25, 30, 40, 50, 60]
// REMOÇÃO
const last = numbers.pop(); // O(1) - Remove do final
const first = numbers.shift(); // O(n) - Remove do início
const removed = numbers.splice(2, 1); // O(n) - Remove do meio
// BUSCA
const index = numbers.indexOf(30); // O(n) - Busca linear
const exists = numbers.includes(40); // O(n) - Verifica existência
const found = numbers.find(x => x > 35); // O(n) - Busca com condição
Métodos Funcionais Avançados
const data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// MAP - Transformação O(n)
const doubled = data.map(x => x * 2);
// [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
// FILTER - Filtragem O(n)
const evens = data.filter(x => x % 2 === 0);
// [2, 4, 6, 8, 10]
// REDUCE - Agregação O(n)
const sum = data.reduce((acc, curr) => acc + curr, 0);
// 55
const product = data.reduce((acc, curr) => acc * curr, 1);
// 3628800
// Reduce complexo: agrupar por propriedade
const people = [
{name: "Alice", age: 25, city: "SP"},
{name: "Bob", age: 30, city: "RJ"},
{name: "Carol", age: 25, city: "SP"}
];
const groupedByAge = people.reduce((groups, person) => {
const age = person.age;
if (!groups[age]) groups[age] = [];
groups[age].push(person);
return groups;
}, {});
// {25: [{name: "Alice"...}, {name: "Carol"...}], 30: [{name: "Bob"...}]}
Array Multidimensional
// Matriz 2D
const matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
// Acesso O(1)
console.log(matrix[1][2]); // 6
// Criação dinâmica de matriz
function createMatrix(rows, cols, defaultValue = 0) {
return Array.from({length: rows}, () =>
Array.from({length: cols}, () => defaultValue)
);
}
const gameBoard = createMatrix(3, 3, null);
// [[null, null, null], [null, null, null], [null, null, null]]
// Percorrer matriz
function traverseMatrix(matrix) {
const result = [];
for (let i = 0; i < matrix.length; i++) {
for (let j = 0; j < matrix[i].length; j++) {
result.push(matrix[i][j]);
}
}
return result;
}
// Transposta de matriz
function transpose(matrix) {
return matrix[0].map((_, colIndex) =>
matrix.map(row => row[colIndex])
);
}
const transposed = transpose(matrix);
// [[1, 4, 7], [2, 5, 8], [3, 6, 9]]
2.1.2 Objects - Hash Tables Nativas
Objects em JavaScript são essencialmente hash tables/dictionaries, fundamentais para muitos algoritmos.
Operações Básicas
// Criação
const obj1 = {}; // Object literal
const obj2 = new Object(); // Constructor
const obj3 = Object.create(null); // Sem prototype
// Inserção e Acesso - O(1) average
const person = {
name: "John",
age: 30,
city: "New York"
};
person.country = "USA"; // Dot notation
person["zip-code"] = "10001"; // Bracket notation
person[Symbol("id")] = 12345; // Symbol key
// Verificação de existência
console.log("name" in person); // true
console.log(person.hasOwnProperty("age")); // true
console.log(Object.hasOwn(person, "city")); // true (ES2022)
// Remoção - O(1)
delete person.country;
// Iteração
for (const key in person) {
if (person.hasOwnProperty(key)) {
console.log(`${key}: ${person[key]}`);
}
}
// Métodos modernos
Object.keys(person).forEach(key => {
console.log(`${key}: ${person[key]}`);
});
Object.entries(person).forEach(([key, value]) => {
console.log(`${key}: ${value}`);
});
Casos de Uso Avançados
// 1. Contador de frequência
function countFrequency(arr) {
const frequency = {};
for (const item of arr) {
frequency[item] = (frequency[item] || 0) + 1;
}
return frequency;
}
const letters = ['a', 'b', 'a', 'c', 'b', 'a'];
console.log(countFrequency(letters)); // {a: 3, b: 2, c: 1}
// 2. Cache/Memoização
function memoize(fn) {
const cache = {};
return function(...args) {
const key = JSON.stringify(args);
if (key in cache) {
return cache[key];
}
const result = fn.apply(this, args);
cache[key] = result;
return result;
};
}
const fibonacci = memoize(function(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
});
// 3. Índice reverso
function buildReverseIndex(documents) {
const index = {};
documents.forEach((doc, docIndex) => {
const words = doc.toLowerCase().split(/\W+/);
words.forEach(word => {
if (word) {
if (!index[word]) index[word] = [];
index[word].push(docIndex);
}
});
});
return index;
}
const docs = [
"JavaScript is awesome",
"Python is also awesome",
"JavaScript and Python are programming languages"
];
const reverseIndex = buildReverseIndex(docs);
// {javascript: [0, 2], is: [0, 1], awesome: [0, 1], ...}
2.1.3 Map e Set - Estruturas Modernas
Map - Hash Table Aprimorada
// Criação e operações básicas
const map = new Map();
// Inserção - O(1) average
map.set('name', 'Alice');
map.set('age', 30);
map.set(42, 'answer');
map.set(true, 'boolean key');
// Acesso - O(1) average
console.log(map.get('name')); // "Alice"
console.log(map.get(42)); // "answer"
// Verificação
console.log(map.has('age')); // true
console.log(map.size); // 4
// Remoção - O(1) average
map.delete('age');
console.log(map.size); // 3
// Iteração (mantém ordem de inserção!)
for (const [key, value] of map) {
console.log(`${key} => ${value}`);
}
// Map vs Object - diferenças importantes
const objectMap = {};
const realMap = new Map();
// Object: chaves sempre strings
objectMap[1] = "one";
objectMap["1"] = "ONE";
console.log(Object.keys(objectMap)); // ["1"] - apenas uma chave!
// Map: qualquer tipo de chave
realMap.set(1, "one");
realMap.set("1", "ONE");
console.log(realMap.size); // 2 - chaves diferentes!
// Map com objetos como chaves
const user1 = {id: 1};
const user2 = {id: 2};
const userPreferences = new Map();
userPreferences.set(user1, {theme: "dark", lang: "en"});
userPreferences.set(user2, {theme: "light", lang: "pt"});
console.log(userPreferences.get(user1)); // {theme: "dark", lang: "en"}
Set - Coleção de Valores Únicos
// Criação
const set = new Set();
const setFromArray = new Set([1, 2, 3, 2, 1]); // Automaticamente remove duplicatas
console.log(setFromArray); // Set(3) {1, 2, 3}
// Operações básicas - todas O(1) average
set.add(1);
set.add(2);
set.add(2); // Ignorado - valor já existe
console.log(set.size); // 2
console.log(set.has(1)); // true
console.log(set.has(3)); // false
set.delete(1);
console.log(set.has(1)); // false
// Casos de uso práticos
function removeDuplicates(arr) {
return [...new Set(arr)];
}
const duplicated = [1, 2, 2, 3, 3, 3, 4];
console.log(removeDuplicates(duplicated)); // [1, 2, 3, 4]
// Operações de conjunto
function intersection(set1, set2) {
return new Set([...set1].filter(x => set2.has(x)));
}
function union(set1, set2) {
return new Set([...set1, ...set2]);
}
function difference(set1, set2) {
return new Set([...set1].filter(x => !set2.has(x)));
}
const setA = new Set([1, 2, 3, 4]);
const setB = new Set([3, 4, 5, 6]);
console.log(intersection(setA, setB)); // Set(2) {3, 4}
console.log(union(setA, setB)); // Set(6) {1, 2, 3, 4, 5, 6}
console.log(difference(setA, setB)); // Set(2) {1, 2}
2.1.4 Strings - Estrutura Imutável
Strings são arrays de caracteres, mas imutáveis. Entender isso é crucial para otimização.
Operações e Complexidades
// Criação
const str1 = "Hello World";
const str2 = 'JavaScript';
const str3 = `Template ${str2}`;
// Acesso - O(1)
console.log(str1[0]); // "H"
console.log(str1.charAt(6)); // "W"
console.log(str1.length); // 11
// Busca - O(n)
console.log(str1.indexOf('o')); // 4 (primeira ocorrência)
console.log(str1.lastIndexOf('o')); // 7 (última ocorrência)
console.log(str1.includes('World')); // true
console.log(str1.startsWith('Hello')); // true
console.log(str1.endsWith('World')); // true
// Substring - O(n) para criar nova string
console.log(str1.slice(0, 5)); // "Hello"
console.log(str1.substring(6, 11)); // "World"
console.log(str1.substr(6, 5)); // "World" (deprecated)
// Transformações - todas O(n)
console.log(str1.toLowerCase()); // "hello world"
console.log(str1.toUpperCase()); // "HELLO WORLD"
console.log(" trim me ".trim()); // "trim me"
console.log(str2.split('')); // ['J','a','v','a','S','c','r','i','p','t']
// String building eficiente
// RUIM - O(n²) devido à imutabilidade
let result1 = "";
for (let i = 0; i < 1000; i++) {
result1 += i.toString(); // Cada += cria uma nova string!
}
// BOM - O(n) usando array
const parts = [];
for (let i = 0; i < 1000; i++) {
parts.push(i.toString());
}
const result2 = parts.join('');
Padrões e Algoritmos com Strings
// 1. Palíndromo
function isPalindrome(s) {
const cleaned = s.toLowerCase().replace(/[^a-zA-Z0-9]/g, '');
let left = 0, right = cleaned.length - 1;
while (left < right) {
if (cleaned[left] !== cleaned[right]) {
return false;
}
left++;
right--;
}
return true;
}
console.log(isPalindrome("A man, a plan, a canal: Panama")); // true
// 2. Anagrama
function areAnagrams(str1, str2) {
if (str1.length !== str2.length) return false;
const count = {};
// Conta caracteres da primeira string
for (const char of str1) {
count[char] = (count[char] || 0) + 1;
}
// Subtrai caracteres da segunda string
for (const char of str2) {
if (!count[char]) return false;
count[char]--;
}
return true;
}
console.log(areAnagrams("listen", "silent")); // true
// 3. Substring patterns - KMP Algorithm
function findPattern(text, pattern) {
const matches = [];
let textIndex = 0;
while (textIndex <= text.length - pattern.length) {
let patternIndex = 0;
let tempTextIndex = textIndex;
while (patternIndex < pattern.length &&
text[tempTextIndex] === pattern[patternIndex]) {
patternIndex++;
tempTextIndex++;
}
if (patternIndex === pattern.length) {
matches.push(textIndex);
}
textIndex++;
}
return matches;
}
console.log(findPattern("ababcababa", "aba")); // [0, 5, 7]
// 4. Longest Common Subsequence
function longestCommonSubsequence(str1, str2) {
const m = str1.length, n = str2.length;
const dp = Array(m + 1).fill().map(() => Array(n + 1).fill(0));
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (str1[i - 1] === str2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[m][n];
}
console.log(longestCommonSubsequence("ABCDGH", "AEDFHR")); // 3 (ADH)
2.1.5 Linked Lists - Estruturas Encadeadas
JavaScript não tem linked lists nativas, mas são fundamentais em entrevistas.
Implementação Básica
class ListNode {
constructor(val, next = null) {
this.val = val;
this.next = next;
}
}
class LinkedList {
constructor() {
this.head = null;
this.size = 0;
}
// Inserção no início - O(1)
prepend(val) {
const newNode = new ListNode(val, this.head);
this.head = newNode;
this.size++;
return this;
}
// Inserção no final - O(n)
append(val) {
const newNode = new ListNode(val);
if (!this.head) {
this.head = newNode;
} else {
let current = this.head;
while (current.next) {
current = current.next;
}
current.next = newNode;
}
this.size++;
return this;
}
// Inserção em posição específica - O(n)
insertAt(index, val) {
if (index < 0 || index > this.size) {
throw new Error('Index out of bounds');
}
if (index === 0) {
return this.prepend(val);
}
const newNode = new ListNode(val);
let current = this.head;
for (let i = 0; i < index - 1; i++) {
current = current.next;
}
newNode.next = current.next;
current.next = newNode;
this.size++;
return this;
}
// Remoção - O(n)
remove(val) {
if (!this.head) return false;
if (this.head.val === val) {
this.head = this.head.next;
this.size--;
return true;
}
let current = this.head;
while (current.next && current.next.val !== val) {
current = current.next;
}
if (current.next) {
current.next = current.next.next;
this.size--;
return true;
}
return false;
}
// Busca - O(n)
find(val) {
let current = this.head;
let index = 0;
while (current) {
if (current.val === val) {
return index;
}
current = current.next;
index++;
}
return -1;
}
// Conversão para array
toArray() {
const result = [];
let current = this.head;
while (current) {
result.push(current.val);
current = current.next;
}
return result;
}
// Reverso - O(n)
reverse() {
let prev = null;
let current = this.head;
while (current) {
const nextTemp = current.next;
current.next = prev;
prev = current;
current = nextTemp;
}
this.head = prev;
return this;
}
}
// Uso da LinkedList
const list = new LinkedList();
list.append(1).append(2).append(3).prepend(0);
console.log(list.toArray()); // [0, 1, 2, 3]
list.insertAt(2, 1.5);
console.log(list.toArray()); // [0, 1, 1.5, 2, 3]
list.remove(1.5);
console.log(list.toArray()); // [0, 1, 2, 3]
list.reverse();
console.log(list.toArray()); // [3, 2, 1, 0]
Problemas Clássicos com Linked Lists
// 1. Detectar ciclo (Floyd's Cycle Detection)
function hasCycle(head) {
if (!head || !head.next) return false;
let slow = head;
let fast = head.next;
while (fast && fast.next) {
if (slow === fast) return true;
slow = slow.next;
fast = fast.next.next;
}
return false;
}
// 2. Encontrar o meio da lista
function findMiddle(head) {
if (!head) return null;
let slow = head;
let fast = head;
while (fast.next && fast.next.next) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
// 3. Merge duas listas ordenadas
function mergeSortedLists(l1, l2) {
const dummy = new ListNode(0);
let current = dummy;
while (l1 && l2) {
if (l1.val <= l2.val) {
current.next = l1;
l1 = l1.next;
} else {
current.next = l2;
l2 = l2.next;
}
current = current.next;
}
// Anexa o restante
current.next = l1 || l2;
return dummy.next;
}
// 4. Remover n-ésimo nó do final
function removeNthFromEnd(head, n) {
const dummy = new ListNode(0, head);
let first = dummy;
let second = dummy;
// Move first n+1 passos à frente
for (let i = 0; i <= n; i++) {
first = first.next;
}
// Move ambos até first chegar ao fim
while (first) {
first = first.next;
second = second.next;
}
// Remove o nó
second.next = second.next.next;
return dummy.next;
}
2.1.6 Stack - LIFO (Last In, First Out)
class Stack {
constructor() {
this.items = [];
}
// Inserção - O(1)
push(item) {
this.items.push(item);
return this;
}
// Remoção - O(1)
pop() {
if (this.isEmpty()) {
throw new Error('Stack is empty');
}
return this.items.pop();
}
// Visualizar topo - O(1)
peek() {
if (this.isEmpty()) {
throw new Error('Stack is empty');
}
return this.items[this.items.length - 1];
}
// Verificações
isEmpty() {
return this.items.length === 0;
}
size() {
return this.items.length;
}
// Limpar
clear() {
this.items = [];
return this;
}
// Conversão para array
toArray() {
return [...this.items];
}
}
// Casos de uso do Stack
// 1. Verificar parênteses balanceados
function isBalancedParentheses(str) {
const stack = new Stack();
const pairs = {'(': ')', '[': ']', '{': '}'};
for (const char of str) {
if (char in pairs) {
stack.push(char);
} else if (Object.values(pairs).includes(char)) {
if (stack.isEmpty() || pairs[stack.pop()] !== char) {
return false;
}
}
}
return stack.isEmpty();
}
console.log(isBalancedParentheses("({[]})")); // true
console.log(isBalancedParentheses("({[}])")); // false
// 2. Avaliação de expressão pós-fixa (RPN)
function evaluateRPN(tokens) {
const stack = new Stack();
const operators = {
'+': (a, b) => a + b,
'-': (a, b) => a - b,
'*': (a, b) => a * b,
'/': (a, b) => Math.trunc(a / b)
};
for (const token of tokens) {
if (token in operators) {
const b = stack.pop();
const a = stack.pop();
stack.push(operators[token](a, b));
} else {
stack.push(parseInt(token));
}
}
return stack.pop();
}
console.log(evaluateRPN(["2", "1", "+", "3", "*"])); // 9 (2+1)*3
// 3. Próximo elemento maior
function nextGreaterElement(nums) {
const result = new Array(nums.length).fill(-1);
const stack = new Stack();
for (let i = 0; i < nums.length; i++) {
while (!stack.isEmpty() && nums[i] > nums[stack.peek()]) {
const index = stack.pop();
result[index] = nums[i];
}
stack.push(i);
}
return result;
}
console.log(nextGreaterElement([2, 1, 2, 4, 3, 1])); // [4, 2, 4, -1, -1, -1]
2.1.7 Queue - FIFO (First In, First Out)
class Queue {
constructor() {
this.items = [];
this.front = 0;
this.rear = 0;
}
// Inserção - O(1)
enqueue(item) {
this.items[this.rear] = item;
this.rear++;
return this;
}
// Remoção - O(1)
dequeue() {
if (this.isEmpty()) {
throw new Error('Queue is empty');
}
const item = this.items[this.front];
delete this.items[this.front];
this.front++;
// Reset se a queue está vazia
if (this.front === this.rear) {
this.front = 0;
this.rear = 0;
}
return item;
}
// Visualizar primeiro - O(1)
peek() {
if (this.isEmpty()) {
throw new Error('Queue is empty');
}
return this.items[this.front];
}
// Verificações
isEmpty() {
return this.front === this.rear;
}
size() {
return this.rear - this.front;
}
// Conversão para array
toArray() {
const result = [];
for (let i = this.front; i < this.rear; i++) {
result.push(this.items[i]);
}
return result;
}
}
// Casos de uso da Queue
// 1. BFS (Breadth-First Search)
function bfsTraversal(graph, start) {
const visited = new Set();
const queue = new Queue();
const result = [];
queue.enqueue(start);
visited.add(start);
while (!queue.isEmpty()) {
const node = queue.dequeue();
result.push(node);
for (const neighbor of graph[node] || []) {
if (!visited.has(neighbor)) {
visited.add(neighbor);
queue.enqueue(neighbor);
}
}
}
return result;
}
const graph = {
'A': ['B', 'C'],
'B': ['D', 'E'],
'C': ['F'],
'D': [],
'E': ['F'],
'F': []
};
console.log(bfsTraversal(graph, 'A')); // ['A', 'B', 'C', 'D', 'E', 'F']
// 2. Sliding Window Maximum
function slidingWindowMaximum(nums, k) {
const result = [];
const deque = []; // Armazena índices
for (let i = 0; i < nums.length; i++) {
// Remove elementos fora da janela
while (deque.length > 0 && deque[0] <= i - k) {
deque.shift();
}
// Remove elementos menores que o atual
while (deque.length > 0 && nums[deque[deque.length - 1]] < nums[i]) {
deque.pop();
}
deque.push(i);
// Se a janela tem tamanho k, adiciona o máximo
if (i >= k - 1) {
result.push(nums[deque[0]]);
}
}
return result;
}
console.log(slidingWindowMaximum([1,3,-1,-3,5,3,6,7], 3)); // [3, 3, 5, 5, 6, 7]
2.2 ALGORITMOS FUNDAMENTAIS ⚡
Sorting, Searching, Recursion - Os algoritmos que todo programador deve dominar
2.2.1 Algoritmos de Ordenação (Sorting)
Bubble Sort - O(n²)
O algoritmo mais básico, mas ineficiente para grandes datasets.
function bubbleSort(arr) {
const n = arr.length;
const result = [...arr]; // Não modifica o array original
for (let i = 0; i < n - 1; i++) {
let swapped = false;
for (let j = 0; j < n - i - 1; j++) {
if (result[j] > result[j + 1]) {
// Swap
[result[j], result[j + 1]] = [result[j + 1], result[j]];
swapped = true;
}
}
// Otimização: se não houve swaps, array já está ordenado
if (!swapped) break;
}
return result;
}
// Versão com tracking de operações
function bubbleSortWithTracking(arr) {
const result = [...arr];
let comparisons = 0;
let swaps = 0;
for (let i = 0; i < result.length - 1; i++) {
for (let j = 0; j < result.length - i - 1; j++) {
comparisons++;
if (result[j] > result[j + 1]) {
[result[j], result[j + 1]] = [result[j + 1], result[j]];
swaps++;
}
}
}
return {
sorted: result,
comparisons,
swaps,
complexity: 'O(n²)'
};
}
const unsorted = [64, 34, 25, 12, 22, 11, 90];
console.log(bubbleSortWithTracking(unsorted));
// { sorted: [11, 12, 22, 25, 34, 64, 90], comparisons: 21, swaps: 13 }
Selection Sort - O(n²)
Encontra o menor elemento e o coloca na posição correta.
function selectionSort(arr) {
const result = [...arr];
for (let i = 0; i < result.length - 1; i++) {
let minIndex = i;
// Encontra o índice do menor elemento
for (let j = i + 1; j < result.length; j++) {
if (result[j] < result[minIndex]) {
minIndex = j;
}
}
// Swap se necessário
if (minIndex !== i) {
[result[i], result[minIndex]] = [result[minIndex], result[i]];
}
}
return result;
}
// Versão que encontra min e max simultaneamente
function doubleSelectionSort(arr) {
const result = [...arr];
let left = 0;
let right = result.length - 1;
while (left < right) {
let minIndex = left;
let maxIndex = right;
// Encontra min e max na mesma passada
for (let i = left; i <= right; i++) {
if (result[i] < result[minIndex]) {
minIndex = i;
}
if (result[i] > result[maxIndex]) {
maxIndex = i;
}
}
// Coloca min no início
[result[left], result[minIndex]] = [result[minIndex], result[left]];
// Ajusta maxIndex se ele estava na posição left
if (maxIndex === left) {
maxIndex = minIndex;
}
// Coloca max no final
[result[right], result[maxIndex]] = [result[maxIndex], result[right]];
left++;
right--;
}
return result;
}
console.log(selectionSort([29, 10, 14, 37, 13])); // [10, 13, 14, 29, 37]
Insertion Sort - O(n²) mas eficiente para arrays pequenos
Constrói a lista ordenada um elemento por vez.
function insertionSort(arr) {
const result = [...arr];
for (let i = 1; i < result.length; i++) {
const current = result[i];
let j = i - 1;
// Move elementos maiores para a direita
while (j >= 0 && result[j] > current) {
result[j + 1] = result[j];
j--;
}
// Insere o elemento na posição correta
result[j + 1] = current;
}
return result;
}
// Versão binária (Binary Insertion Sort)
function binaryInsertionSort(arr) {
const result = [...arr];
for (let i = 1; i < result.length; i++) {
const current = result[i];
// Busca binária para encontrar posição de inserção
let left = 0;
let right = i;
while (left < right) {
const mid = Math.floor((left + right) / 2);
if (result[mid] > current) {
right = mid;
} else {
left = mid + 1;
}
}
// Move elementos para a direita
for (let j = i - 1; j >= left; j--) {
result[j + 1] = result[j];
}
// Insere elemento
result[left] = current;
}
return result;
}
console.log(insertionSort([5, 2, 4, 6, 1, 3])); // [1, 2, 3, 4, 5, 6]
Merge Sort - O(n log n)
Algoritmo divide e conquista, estável e eficiente.
function mergeSort(arr) {
if (arr.length <= 1) return arr;
const mid = Math.floor(arr.length / 2);
const left = arr.slice(0, mid);
const right = arr.slice(mid);
return merge(mergeSort(left), mergeSort(right));
}
function merge(left, right) {
const result = [];
let leftIndex = 0;
let rightIndex = 0;
// Combina arrays ordenados
while (leftIndex < left.length && rightIndex < right.length) {
if (left[leftIndex] <= right[rightIndex]) {
result.push(left[leftIndex]);
leftIndex++;
} else {
result.push(right[rightIndex]);
rightIndex++;
}
}
// Adiciona elementos restantes
return result
.concat(left.slice(leftIndex))
.concat(right.slice(rightIndex));
}
// Merge Sort in-place (mais eficiente em memória)
function mergeSortInPlace(arr, start = 0, end = arr.length - 1) {
if (start >= end) return arr;
const mid = Math.floor((start + end) / 2);
mergeSortInPlace(arr, start, mid);
mergeSortInPlace(arr, mid + 1, end);
mergeInPlace(arr, start, mid, end);
return arr;
}
function mergeInPlace(arr, start, mid, end) {
const temp = [];
let i = start, j = mid + 1, k = 0;
while (i <= mid && j <= end) {
if (arr[i] <= arr[j]) {
temp[k++] = arr[i++];
} else {
temp[k++] = arr[j++];
}
}
while (i <= mid) {
temp[k++] = arr[i++];
}
while (j <= end) {
temp[k++] = arr[j++];
}
for (let i = start; i <= end; i++) {
arr[i] = temp[i - start];
}
}
const large = Array.from({length: 1000}, () => Math.floor(Math.random() * 1000));
console.time('Merge Sort');
const sorted = mergeSort(large);
console.timeEnd('Merge Sort'); // ~2-3ms para 1000 elementos
Quick Sort - O(n log n) average, O(n²) worst
Algoritmo muito rápido na prática, usado nativamente pelo JavaScript.
function quickSort(arr, left = 0, right = arr.length - 1) {
if (left < right) {
const pivotIndex = partition(arr, left, right);
quickSort(arr, left, pivotIndex - 1);
quickSort(arr, pivotIndex + 1, right);
}
return arr;
}
function partition(arr, left, right) {
const pivot = arr[right]; // Usa último elemento como pivot
let i = left - 1;
for (let j = left; j < right; j++) {
if (arr[j] <= pivot) {
i++;
[arr[i], arr[j]] = [arr[j], arr[i]];
}
}
[arr[i + 1], arr[right]] = [arr[right], arr[i + 1]];
return i + 1;
}
// Quick Sort com pivot aleatório (melhor para casos desfavoráveis)
function randomizedQuickSort(arr, left = 0, right = arr.length - 1) {
if (left < right) {
const pivotIndex = randomizedPartition(arr, left, right);
randomizedQuickSort(arr, left, pivotIndex - 1);
randomizedQuickSort(arr, pivotIndex + 1, right);
}
return arr;
}
function randomizedPartition(arr, left, right) {
const randomIndex = left + Math.floor(Math.random() * (right - left + 1));
[arr[randomIndex], arr[right]] = [arr[right], arr[randomIndex]];
return partition(arr, left, right);
}
// Quick Sort iterativo (evita stack overflow)
function iterativeQuickSort(arr) {
const stack = [];
const result = [...arr];
stack.push(0);
stack.push(result.length - 1);
while (stack.length > 0) {
const right = stack.pop();
const left = stack.pop();
if (left < right) {
const pivotIndex = partition(result, left, right);
// Push sub-arrays maiores primeiro (otimização)
if (pivotIndex - left > right - pivotIndex) {
stack.push(left);
stack.push(pivotIndex - 1);
stack.push(pivotIndex + 1);
stack.push(right);
} else {
stack.push(pivotIndex + 1);
stack.push(right);
stack.push(left);
stack.push(pivotIndex - 1);
}
}
}
return result;
}
// Teste de performance
const testData = Array.from({length: 10000}, () => Math.floor(Math.random() * 10000));
console.time('Native Sort');
const nativeSorted = [...testData].sort((a, b) => a - b);
console.timeEnd('Native Sort');
console.time('Quick Sort');
const quickSorted = quickSort([...testData]);
console.timeEnd('Quick Sort');
Heap Sort - O(n log n)
Usa uma heap (priority queue) para ordenar.
class MinHeap {
constructor() {
this.heap = [];
}
parent(index) {
return Math.floor((index - 1) / 2);
}
leftChild(index) {
return 2 * index + 1;
}
rightChild(index) {
return 2 * index + 2;
}
swap(i, j) {
[this.heap[i], this.heap[j]] = [this.heap[j], this.heap[i]];
}
insert(value) {
this.heap.push(value);
this.heapifyUp();
}
heapifyUp() {
let index = this.heap.length - 1;
while (index > 0 && this.heap[this.parent(index)] > this.heap[index]) {
this.swap(index, this.parent(index));
index = this.parent(index);
}
}
extractMin() {
if (this.heap.length === 0) return null;
if (this.heap.length === 1) return this.heap.pop();
const min = this.heap[0];
this.heap[0] = this.heap.pop();
this.heapifyDown();
return min;
}
heapifyDown() {
let index = 0;
while (this.leftChild(index) < this.heap.length) {
let smallerChildIndex = this.leftChild(index);
if (this.rightChild(index) < this.heap.length &&
this.heap[this.rightChild(index)] < this.heap[this.leftChild(index)]) {
smallerChildIndex = this.rightChild(index);
}
if (this.heap[index] <= this.heap[smallerChildIndex]) {
break;
}
this.swap(index, smallerChildIndex);
index = smallerChildIndex;
}
}
}
function heapSort(arr) {
const heap = new MinHeap();
const result = [];
// Insere todos elementos na heap
for (const value of arr) {
heap.insert(value);
}
// Extrai elementos em ordem
while (heap.heap.length > 0) {
result.push(heap.extractMin());
}
return result;
}
// Heap Sort in-place
function heapSortInPlace(arr) {
buildMaxHeap(arr);
for (let i = arr.length - 1; i > 0; i--) {
[arr[0], arr[i]] = [arr[i], arr[0]];
maxHeapify(arr, 0, i);
}
return arr;
}
function buildMaxHeap(arr) {
for (let i = Math.floor(arr.length / 2) - 1; i >= 0; i--) {
maxHeapify(arr, i, arr.length);
}
}
function maxHeapify(arr, index, heapSize) {
const left = 2 * index + 1;
const right = 2 * index + 2;
let largest = index;
if (left < heapSize && arr[left] > arr[largest]) {
largest = left;
}
if (right < heapSize && arr[right] > arr[largest]) {
largest = right;
}
if (largest !== index) {
[arr[index], arr[largest]] = [arr[largest], arr[index]];
maxHeapify(arr, largest, heapSize);
}
}
console.log(heapSort([4, 10, 3, 5, 1])); // [1, 3, 4, 5, 10]
2.2.2 Algoritmos de Busca (Searching)
Linear Search - O(n)
Busca sequencial básica.
function linearSearch(arr, target) {
for (let i = 0; i < arr.length; i++) {
if (arr[i] === target) {
return i;
}
}
return -1;
}
// Busca com callback personalizado
function linearSearchCallback(arr, callback) {
for (let i = 0; i < arr.length; i++) {
if (callback(arr[i], i, arr)) {
return { index: i, value: arr[i] };
}
}
return null;
}
// Busca múltiplas ocorrências
function findAll(arr, target) {
const indices = [];
for (let i = 0; i < arr.length; i++) {
if (arr[i] === target) {
indices.push(i);
}
}
return indices;
}
const numbers = [1, 3, 5, 7, 3, 9, 3];
console.log(linearSearch(numbers, 7)); // 3
console.log(findAll(numbers, 3)); // [1, 4, 6]
const people = [
{name: "Alice", age: 25},
{name: "Bob", age: 30},
{name: "Charlie", age: 25}
];
console.log(linearSearchCallback(people, person => person.age === 25));
// {index: 0, value: {name: "Alice", age: 25}}
Binary Search - O(log n)
Busca eficiente em arrays ordenados.
function binarySearch(arr, target) {
let left = 0;
let right = arr.length - 1;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (arr[mid] === target) {
return mid;
} else if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return -1;
}
// Binary Search recursivo
function binarySearchRecursive(arr, target, left = 0, right = arr.length - 1) {
if (left > right) return -1;
const mid = Math.floor((left + right) / 2);
if (arr[mid] === target) {
return mid;
} else if (arr[mid] < target) {
return binarySearchRecursive(arr, target, mid + 1, right);
} else {
return binarySearchRecursive(arr, target, left, mid - 1);
}
}
// Encontrar primeira/última ocorrência
function findFirstOccurrence(arr, target) {
let left = 0;
let right = arr.length - 1;
let result = -1;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (arr[mid] === target) {
result = mid;
right = mid - 1; // Continue procurando à esquerda
} else if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return result;
}
function findLastOccurrence(arr, target) {
let left = 0;
let right = arr.length - 1;
let result = -1;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
if (arr[mid] === target) {
result = mid;
left = mid + 1; // Continue procurando à direita
} else if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
return result;
}
// Busca por faixa (range search)
function searchRange(arr, target) {
const first = findFirstOccurrence(arr, target);
if (first === -1) return [-1, -1];
const last = findLastOccurrence(arr, target);
return [first, last];
}
const sortedNumbers = [1, 2, 2, 2, 3, 4, 4, 5];
console.log(binarySearch(sortedNumbers, 4)); // 5 ou 6
console.log(searchRange(sortedNumbers, 2)); // [1, 3]
console.log(searchRange(sortedNumbers, 4)); // [5, 6]
Interpolation Search - O(log log n) para dados uniformemente distribuídos
Melhora a binary search estimando a posição.
function interpolationSearch(arr, target) {
let left = 0;
let right = arr.length - 1;
while (left <= right && target >= arr[left] && target <= arr[right]) {
if (left === right) {
return arr[left] === target ? left : -1;
}
// Estimativa da posição
const pos = left + Math.floor(
((target - arr[left]) / (arr[right] - arr[left])) * (right - left)
);
if (arr[pos] === target) {
return pos;
} else if (arr[pos] < target) {
left = pos + 1;
} else {
right = pos - 1;
}
}
return -1;
}
// Teste com dados uniformes
const uniformData = Array.from({length: 1000}, (_, i) => i * 2);
console.log(interpolationSearch(uniformData, 500)); // 250
Jump Search - O(√n)
Combina linear e binary search.
function jumpSearch(arr, target) {
const n = arr.length;
const step = Math.floor(Math.sqrt(n));
let prev = 0;
// Encontra o bloco onde o elemento pode estar
while (arr[Math.min(step, n) - 1] < target) {
prev = step;
step += Math.floor(Math.sqrt(n));
if (prev >= n) {
return -1;
}
}
// Linear search no bloco
while (arr[prev] < target) {
prev++;
if (prev === Math.min(step, n)) {
return -1;
}
}
return arr[prev] === target ? prev : -1;
}
console.log(jumpSearch([0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144], 55)); // 10
2.2.3 Recursão - A Arte de Resolver Problemas
Conceitos Fundamentais
// Anatomia de uma função recursiva
function recursiveFunction(input) {
// 1. CASO BASE - condição de parada
if (baseCondition) {
return baseValue;
}
// 2. CASO RECURSIVO - chamada para subproblema menor
return recursiveFunction(smallerInput);
}
// Exemplo clássico: Fatorial
function factorial(n) {
// Caso base
if (n <= 1) return 1;
// Caso recursivo
return n * factorial(n - 1);
}
console.log(factorial(5)); // 120
// Versão com memoização para evitar recálculos
function factorialMemo() {
const cache = {};
return function factorial(n) {
if (n in cache) return cache[n];
if (n <= 1) return 1;
cache[n] = n * factorial(n - 1);
return cache[n];
};
}
const fastFactorial = factorialMemo();
console.log(fastFactorial(100)); // Resultado instantâneo
Fibonacci - Problema Clássico
// Versão ingênua - O(2^n) - MUITO LENTA
function fibonacciNaive(n) {
if (n <= 1) return n;
return fibonacciNaive(n - 1) + fibonacciNaive(n - 2);
}
// Versão com memoização - O(n)
function fibonacciMemo() {
const cache = {};
return function fib(n) {
if (n in cache) return cache[n];
if (n <= 1) return n;
cache[n] = fib(n - 1) + fib(n - 2);
return cache[n];
};
}
// Versão iterativa - O(n), O(1) space
function fibonacciIterative(n) {
if (n <= 1) return n;
let a = 0, b = 1;
for (let i = 2; i <= n; i++) {
[a, b] = [b, a + b];
}
return b;
}
// Versão matrix exponentiation - O(log n)
function fibonacciMatrix(n) {
if (n <= 1) return n;
function matrixMultiply(a, b) {
return [
[a[0][0] * b[0][0] + a[0][1] * b[1][0], a[0][0] * b[0][1] + a[0][1] * b[1][1]],
[a[1][0] * b[0][0] + a[1][1] * b[1][0], a[1][0] * b[0][1] + a[1][1] * b[1][1]]
];
}
function matrixPower(matrix, power) {
if (power === 1) return matrix;
if (power % 2 === 0) {
const half = matrixPower(matrix, power / 2);
return matrixMultiply(half, half);
}
return matrixMultiply(matrix, matrixPower(matrix, power - 1));
}
const baseMatrix = [[1, 1], [1, 0]];
const result = matrixPower(baseMatrix, n);
return result[0][1];
}
// Comparação de performance
console.time('Naive Fibonacci(35)');
// console.log(fibonacciNaive(35)); // ~2-3 segundos
console.timeEnd('Naive Fibonacci(35)');
const memoFib = fibonacciMemo();
console.time('Memo Fibonacci(35)');
console.log(memoFib(35)); // Instantâneo
console.timeEnd('Memo Fibonacci(35)');
console.time('Matrix Fibonacci(35)');
console.log(fibonacciMatrix(35)); // Instantâneo
console.timeEnd('Matrix Fibonacci(35)');
Tree Traversal - Percorrer Árvores
class TreeNode {
constructor(val, left = null, right = null) {
this.val = val;
this.left = left;
this.right = right;
}
}
// Pre-order: Root → Left → Right
function preorderTraversal(root) {
if (!root) return [];
return [
root.val,
...preorderTraversal(root.left),
...preorderTraversal(root.right)
];
}
// In-order: Left → Root → Right
function inorderTraversal(root) {
if (!root) return [];
return [
...inorderTraversal(root.left),
root.val,
...inorderTraversal(root.right)
];
}
// Post-order: Left → Right → Root
function postorderTraversal(root) {
if (!root) return [];
return [
...postorderTraversal(root.left),
...postorderTraversal(root.right),
root.val
];
}
// Versões iterativas (mais eficientes)
function preorderIterative(root) {
if (!root) return [];
const result = [];
const stack = [root];
while (stack.length > 0) {
const node = stack.pop();
result.push(node.val);
if (node.right) stack.push(node.right);
if (node.left) stack.push(node.left);
}
return result;
}
function inorderIterative(root) {
const result = [];
const stack = [];
let current = root;
while (current || stack.length > 0) {
while (current) {
stack.push(current);
current = current.left;
}
current = stack.pop();
result.push(current.val);
current = current.right;
}
return result;
}
// Criar árvore de teste
// 1
// / \
// 2 3
// / \
// 4 5
const tree = new TreeNode(1,
new TreeNode(2,
new TreeNode(4),
new TreeNode(5)
),
new TreeNode(3)
);
console.log('Pre-order:', preorderTraversal(tree)); // [1, 2, 4, 5, 3]
console.log('In-order:', inorderTraversal(tree)); // [4, 2, 5, 1, 3]
console.log('Post-order:', postorderTraversal(tree)); // [4, 5, 2, 3, 1]
Backtracking - Tentativa e Erro
// N-Queens Problem
function solveNQueens(n) {
const solutions = [];
const board = Array(n).fill().map(() => Array(n).fill('.'));
function isSafe(row, col) {
// Verifica coluna
for (let i = 0; i < row; i++) {
if (board[i][col] === 'Q') return false;
}
// Verifica diagonal principal
for (let i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
if (board[i][j] === 'Q') return false;
}
// Verifica diagonal secundária
for (let i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
if (board[i][j] === 'Q') return false;
}
return true;
}
function backtrack(row) {
if (row === n) {
// Solução encontrada
solutions.push(board.map(row => row.join('')));
return;
}
for (let col = 0; col < n; col++) {
if (isSafe(row, col)) {
board[row][col] = 'Q';
backtrack(row + 1);
board[row][col] = '.'; // Backtrack
}
}
}
backtrack(0);
return solutions;
}
console.log(solveNQueens(4));
// Sudoku Solver
function solveSudoku(board) {
function isValid(board, row, col, num) {
// Verifica linha
for (let x = 0; x < 9; x++) {
if (board[row][x] === num) return false;
}
// Verifica coluna
for (let x = 0; x < 9; x++) {
if (board[x][col] === num) return false;
}
// Verifica subgrade 3x3
const startRow = row - row % 3;
const startCol = col - col % 3;
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
if (board[i + startRow][j + startCol] === num) {
return false;
}
}
}
return true;
}
function solve(board) {
for (let row = 0; row < 9; row++) {
for (let col = 0; col < 9; col++) {
if (board[row][col] === '.') {
for (let num = '1'; num <= '9'; num++) {
if (isValid(board, row, col, num)) {
board[row][col] = num;
if (solve(board)) {
return true;
}
board[row][col] = '.'; // Backtrack
}
}
return false;
}
}
}
return true;
}
solve(board);
return board;
}
// Generate Permutations
function permute(nums) {
const result = [];
function backtrack(current) {
if (current.length === nums.length) {
result.push([...current]);
return;
}
for (const num of nums) {
if (!current.includes(num)) {
current.push(num);
backtrack(current);
current.pop(); // Backtrack
}
}
}
backtrack([]);
return result;
}
console.log(permute([1, 2, 3]));
// [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
Divide and Conquer
// Maximum Subarray Sum (Kadane's Algorithm recursivo)
function maxSubarrayDC(arr, left = 0, right = arr.length - 1) {
if (left === right) return arr[left];
const mid = Math.floor((left + right) / 2);
// Máximo nas metades esquerda e direita
const leftMax = maxSubarrayDC(arr, left, mid);
const rightMax = maxSubarrayDC(arr, mid + 1, right);
// Máximo crossing the middle
let leftSum = Number.NEGATIVE_INFINITY;
let sum = 0;
for (let i = mid; i >= left; i--) {
sum += arr[i];
leftSum = Math.max(leftSum, sum);
}
let rightSum = Number.NEGATIVE_INFINITY;
sum = 0;
for (let i = mid + 1; i <= right; i++) {
sum += arr[i];
rightSum = Math.max(rightSum, sum);
}
const crossSum = leftSum + rightSum;
return Math.max(leftMax, rightMax, crossSum);
}
// Power function - O(log n)
function power(base, exp) {
if (exp === 0) return 1;
if (exp === 1) return base;
if (exp % 2 === 0) {
const half = power(base, exp / 2);
return half * half;
} else {
return base * power(base, exp - 1);
}
}
// Strassen's Matrix Multiplication
function strassenMultiply(A, B) {
const n = A.length;
if (n === 1) {
return [[A[0][0] * B[0][0]]];
}
const mid = n / 2;
// Divide matrices into quadrants
const A11 = getQuadrant(A, 0, 0, mid);
const A12 = getQuadrant(A, 0, mid, mid);
const A21 = getQuadrant(A, mid, 0, mid);
const A22 = getQuadrant(A, mid, mid, mid);
const B11 = getQuadrant(B, 0, 0, mid);
const B12 = getQuadrant(B, 0, mid, mid);
const B21 = getQuadrant(B, mid, 0, mid);
const B22 = getQuadrant(B, mid, mid, mid);
// Strassen's 7 multiplications
const M1 = strassenMultiply(add(A11, A22), add(B11, B22));
const M2 = strassenMultiply(add(A21, A22), B11);
const M3 = strassenMultiply(A11, subtract(B12, B22));
const M4 = strassenMultiply(A22, subtract(B21, B11));
const M5 = strassenMultiply(add(A11, A12), B22);
const M6 = strassenMultiply(subtract(A21, A11), add(B11, B12));
const M7 = strassenMultiply(subtract(A12, A22), add(B21, B22));
// Combine results
const C11 = add(subtract(add(M1, M4), M5), M7);
const C12 = add(M3, M5);
const C21 = add(M2, M4);
const C22 = add(subtract(add(M1, M3), M2), M6);
return combineQuadrants(C11, C12, C21, C22);
}
function getQuadrant(matrix, startRow, startCol, size) {
const quadrant = [];
for (let i = 0; i < size; i++) {
quadrant[i] = [];
for (let j = 0; j < size; j++) {
quadrant[i][j] = matrix[startRow + i][startCol + j];
}
}
return quadrant;
}
function add(A, B) {
const n = A.length;
const result = [];
for (let i = 0; i < n; i++) {
result[i] = [];
for (let j = 0; j < n; j++) {
result[i][j] = A[i][j] + B[i][j];
}
}
return result;
}
function subtract(A, B) {
const n = A.length;
const result = [];
for (let i = 0; i < n; i++) {
result[i] = [];
for (let j = 0; j < n; j++) {
result[i][j] = A[i][j] - B[i][j];
}
}
return result;
}
function combineQuadrants(C11, C12, C21, C22) {
const n = C11.length;
const result = [];
for (let i = 0; i < 2 * n; i++) {
result[i] = [];
}
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
result[i][j] = C11[i][j];
result[i][j + n] = C12[i][j];
result[i + n][j] = C21[i][j];
result[i + n][j + n] = C22[i][j];
}
}
return result;
}
// Testes
console.log(power(2, 10)); // 1024
console.log(maxSubarrayDC([-2, 1, -3, 4, -1, 2, 1, -5, 4])); // 6
2.3 PADRÕES LEETCODE - ALGORITMOS DE ENTREVISTA
Os padrões mais importantes para dominar entrevistas técnicas FAANG
2.3.1 Two Pointers (Dois Ponteiros)
O padrão Two Pointers é uma das técnicas mais importantes em entrevistas. Usado para problemas com
arrays/strings ordenados ou quando você precisa encontrar pares/triplas que satisfazem condições específicas.
Conceito Base
// Template básico do Two Pointers
function twoPointersTemplate(arr) {
let left = 0;
let right = arr.length - 1;
while (left < right) {
// Processa elementos arr[left] e arr[right]
if (conditionMet) {
// Faz algo e move ponteiros
left++;
right--;
} else if (needMoveLeft) {
left++;
} else {
right--;
}
}
}
Problema 1: Two Sum em Array Ordenado
// LeetCode 167: Two Sum II - Input array is sorted
function twoSumSorted(numbers, target) {
let left = 0;
let right = numbers.length - 1;
while (left < right) {
const sum = numbers[left] + numbers[right];
if (sum === target) {
return [left + 1, right + 1]; // 1-indexed
} else if (sum < target) {
left++; // Precisa de soma maior
} else {
right--; // Precisa de soma menor
}
}
return []; // Não encontrado
}
// Teste
console.log(twoSumSorted([2, 7, 11, 15], 9)); // [1, 2]
console.log(twoSumSorted([2, 3, 4], 6)); // [1, 3]
// Time: O(n), Space: O(1)
Problema 2: Container With Most Water
// LeetCode 11: Container With Most Water
function maxArea(height) {
let left = 0;
let right = height.length - 1;
let maxWater = 0;
while (left < right) {
const width = right - left;
const currentHeight = Math.min(height[left], height[right]);
const currentArea = width * currentHeight;
maxWater = Math.max(maxWater, currentArea);
// Move o ponteiro da altura menor
// Pois mover o maior não pode resultar em área maior
if (height[left] < height[right]) {
left++;
} else {
right--;
}
}
return maxWater;
}
console.log(maxArea([1,8,6,2,5,4,8,3,7])); // 49
// Time: O(n), Space: O(1)
Problema 3: 3Sum
// LeetCode 15: 3Sum
function threeSum(nums) {
const result = [];
nums.sort((a, b) => a - b); // Crucial: ordenar primeiro
for (let i = 0; i < nums.length - 2; i++) {
// Pula duplicatas para o primeiro número
if (i > 0 && nums[i] === nums[i - 1]) continue;
let left = i + 1;
let right = nums.length - 1;
while (left < right) {
const sum = nums[i] + nums[left] + nums[right];
if (sum === 0) {
result.push([nums[i], nums[left], nums[right]]);
// Pula duplicatas
while (left < right && nums[left] === nums[left + 1]) left++;
while (left < right && nums[right] === nums[right - 1]) right--;
left++;
right--;
} else if (sum < 0) {
left++;
} else {
right--;
}
}
}
return result;
}
console.log(threeSum([-1, 0, 1, 2, -1, -4])); // [[-1,-1,2],[-1,0,1]]
// Time: O(n²), Space: O(1) excluding output
Problema 4: Trapping Rain Water
// LeetCode 42: Trapping Rain Water
function trap(height) {
if (!height || height.length < 3) return 0;
let left = 0;
let right = height.length - 1;
let leftMax = 0;
let rightMax = 0;
let water = 0;
while (left < right) {
if (height[left] < height[right]) {
if (height[left] >= leftMax) {
leftMax = height[left];
} else {
water += leftMax - height[left];
}
left++;
} else {
if (height[right] >= rightMax) {
rightMax = height[right];
} else {
water += rightMax - height[right];
}
right--;
}
}
return water;
}
console.log(trap([0,1,0,2,1,0,1,3,2,1,2,1])); // 6
// Time: O(n), Space: O(1)
2.3.2 Sliding Window (Janela Deslizante)
Usado para problemas de substring/subarray com condições específicas de tamanho ou propriedades.
Fixed Size Window
// Template para janela de tamanho fixo
function fixedWindow(arr, k) {
if (arr.length < k) return [];
let windowSum = 0;
const result = [];
// Calcula soma da primeira janela
for (let i = 0; i < k; i++) {
windowSum += arr[i];
}
result.push(windowSum);
// Desliza a janela
for (let i = k; i < arr.length; i++) {
windowSum = windowSum - arr[i - k] + arr[i];
result.push(windowSum);
}
return result;
}
Problema 1: Maximum Sum Subarray of Size K
function maxSumSubarray(arr, k) {
if (arr.length < k) return -1;
let windowSum = 0;
let maxSum = 0;
// Primeira janela
for (let i = 0; i < k; i++) {
windowSum += arr[i];
}
maxSum = windowSum;
// Desliza a janela
for (let i = k; i < arr.length; i++) {
windowSum = windowSum - arr[i - k] + arr[i];
maxSum = Math.max(maxSum, windowSum);
}
return maxSum;
}
console.log(maxSumSubarray([2, 1, 5, 1, 3, 2], 3)); // 9 ([5,1,3])
// Time: O(n), Space: O(1)
Variable Size Window
// Template para janela de tamanho variável
function variableWindow(arr, condition) {
let left = 0;
let result = 0;
for (let right = 0; right < arr.length; right++) {
// Expande a janela incluindo arr[right]
while (/* condição violada */) {
// Contrai a janela removendo arr[left]
left++;
}
// Atualiza resultado se necessário
result = Math.max(result, right - left + 1);
}
return result;
}
Problema 2: Longest Substring Without Repeating Characters
// LeetCode 3: Longest Substring Without Repeating Characters
function lengthOfLongestSubstring(s) {
const charSet = new Set();
let left = 0;
let maxLength = 0;
for (let right = 0; right < s.length; right++) {
// Se caractere já existe, contrai janela
while (charSet.has(s[right])) {
charSet.delete(s[left]);
left++;
}
charSet.add(s[right]);
maxLength = Math.max(maxLength, right - left + 1);
}
return maxLength;
}
console.log(lengthOfLongestSubstring("abcabcbb")); // 3 ("abc")
console.log(lengthOfLongestSubstring("bbbbb")); // 1 ("b")
// Time: O(n), Space: O(min(m,n)) where m is charset size
Problema 3: Minimum Window Substring
// LeetCode 76: Minimum Window Substring
function minWindow(s, t) {
if (s.length < t.length) return "";
const targetCount = {};
for (const char of t) {
targetCount[char] = (targetCount[char] || 0) + 1;
}
const windowCount = {};
let left = 0;
let minLength = Infinity;
let minStart = 0;
let requiredChars = Object.keys(targetCount).length;
let formedChars = 0;
for (let right = 0; right < s.length; right++) {
const rightChar = s[right];
windowCount[rightChar] = (windowCount[rightChar] || 0) + 1;
if (targetCount[rightChar] && windowCount[rightChar] === targetCount[rightChar]) {
formedChars++;
}
// Tenta contrair janela até não ser mais válida
while (left <= right && formedChars === requiredChars) {
const leftChar = s[left];
// Atualiza resultado se esta janela é menor
if (right - left + 1 < minLength) {
minLength = right - left + 1;
minStart = left;
}
windowCount[leftChar]--;
if (targetCount[leftChar] && windowCount[leftChar] < targetCount[leftChar]) {
formedChars--;
}
left++;
}
}
return minLength === Infinity ? "" : s.substring(minStart, minStart + minLength);
}
console.log(minWindow("ADOBECODEBANC", "ABC")); // "BANC"
// Time: O(|s| + |t|), Space: O(|s| + |t|)
Problema 4: Sliding Window Maximum
// LeetCode 239: Sliding Window Maximum
function maxSlidingWindow(nums, k) {
const result = [];
const deque = []; // Armazena índices em ordem decrescente de valores
for (let i = 0; i < nums.length; i++) {
// Remove índices fora da janela
while (deque.length > 0 && deque[0] <= i - k) {
deque.shift();
}
// Remove elementos menores que o atual (nunca serão máximo)
while (deque.length > 0 && nums[deque[deque.length - 1]] < nums[i]) {
deque.pop();
}
deque.push(i);
// Se janela tem tamanho k, adiciona máximo ao resultado
if (i >= k - 1) {
result.push(nums[deque[0]]);
}
}
return result;
}
console.log(maxSlidingWindow([1,3,-1,-3,5,3,6,7], 3)); // [3,3,5,5,6,7]
// Time: O(n), Space: O(k)
2.3.3 Dynamic Programming (Programação Dinâmica)
DP é essencial para otimizar problemas recursivos com subproblemas sobrepostos.
Padrão 1: 1D DP
// Template básico 1D DP
function dpTemplate(n) {
const dp = new Array(n + 1);
// Casos base
dp[0] = /* valor base */;
dp[1] = /* valor base */;
// Preenchimento da tabela
for (let i = 2; i <= n; i++) {
dp[i] = /* relação de recorrência usando dp[i-1], dp[i-2], etc. */;
}
return dp[n];
}
Problema 1: Climbing Stairs
// LeetCode 70: Climbing Stairs
function climbStairs(n) {
if (n <= 2) return n;
const dp = new Array(n + 1);
dp[0] = 1; // Uma forma de não subir nenhum degrau
dp[1] = 1; // Uma forma de subir 1 degrau
dp[2] = 2; // Duas formas: 1+1 ou 2
for (let i = 3; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
// Versão otimizada O(1) space
function climbStairsOptimized(n) {
if (n <= 2) return n;
let prev2 = 1; // dp[i-2]
let prev1 = 2; // dp[i-1]
for (let i = 3; i <= n; i++) {
const current = prev1 + prev2;
prev2 = prev1;
prev1 = current;
}
return prev1;
}
console.log(climbStairs(5)); // 8
// Time: O(n), Space: O(1)
Problema 2: House Robber
// LeetCode 198: House Robber
function rob(nums) {
if (nums.length === 0) return 0;
if (nums.length === 1) return nums[0];
const dp = new Array(nums.length);
dp[0] = nums[0];
dp[1] = Math.max(nums[0], nums[1]);
for (let i = 2; i < nums.length; i++) {
// Opção 1: roubar casa atual + dp[i-2]
// Opção 2: não roubar casa atual, manter dp[i-1]
dp[i] = Math.max(nums[i] + dp[i - 2], dp[i - 1]);
}
return dp[nums.length - 1];
}
// Versão otimizada O(1) space
function robOptimized(nums) {
if (nums.length === 0) return 0;
if (nums.length === 1) return nums[0];
let prev2 = nums[0];
let prev1 = Math.max(nums[0], nums[1]);
for (let i = 2; i < nums.length; i++) {
const current = Math.max(nums[i] + prev2, prev1);
prev2 = prev1;
prev1 = current;
}
return prev1;
}
console.log(rob([2, 7, 9, 3, 1])); // 12 (2 + 9 + 1)
// Time: O(n), Space: O(1)
Padrão 2: 2D DP
// Template básico 2D DP
function dp2DTemplate(m, n) {
const dp = Array(m).fill().map(() => Array(n).fill(0));
// Inicializar casos base
for (let i = 0; i < m; i++) {
dp[i][0] = /* valor base */;
}
for (let j = 0; j < n; j++) {
dp[0][j] = /* valor base */;
}
// Preenchimento da tabela
for (let i = 1; i < m; i++) {
for (let j = 1; j < n; j++) {
dp[i][j] = /* relação de recorrência */;
}
}
return dp[m - 1][n - 1];
}
Problema 3: Unique Paths
// LeetCode 62: Unique Paths
function uniquePaths(m, n) {
const dp = Array(m).fill().map(() => Array(n).fill(0));
// Casos base: primeira linha e primeira coluna
for (let i = 0; i < m; i++) {
dp[i][0] = 1; // Só uma forma de chegar à primeira coluna
}
for (let j = 0; j < n; j++) {
dp[0][j] = 1; // Só uma forma de chegar à primeira linha
}
// Preenchimento: dp[i][j] = dp[i-1][j] + dp[i][j-1]
for (let i = 1; i < m; i++) {
for (let j = 1; j < n; j++) {
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
return dp[m - 1][n - 1];
}
// Versão otimizada O(n) space
function uniquePathsOptimized(m, n) {
let prev = Array(n).fill(1);
for (let i = 1; i < m; i++) {
const current = Array(n).fill(0);
current[0] = 1;
for (let j = 1; j < n; j++) {
current[j] = prev[j] + current[j - 1];
}
prev = current;
}
return prev[n - 1];
}
console.log(uniquePaths(3, 7)); // 28
// Time: O(m*n), Space: O(n)
Problema 4: Longest Common Subsequence
// LeetCode 1143: Longest Common Subsequence
function longestCommonSubsequence(text1, text2) {
const m = text1.length;
const n = text2.length;
const dp = Array(m + 1).fill().map(() => Array(n + 1).fill(0));
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (text1[i - 1] === text2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[m][n];
}
// Função para reconstruir a LCS
function printLCS(text1, text2) {
const m = text1.length;
const n = text2.length;
const dp = Array(m + 1).fill().map(() => Array(n + 1).fill(0));
// Preenchimento da tabela DP
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (text1[i - 1] === text2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
// Reconstroí a LCS
const lcs = [];
let i = m, j = n;
while (i > 0 && j > 0) {
if (text1[i - 1] === text2[j - 1]) {
lcs.unshift(text1[i - 1]);
i--;
j--;
} else if (dp[i - 1][j] > dp[i][j - 1]) {
i--;
} else {
j--;
}
}
return lcs.join('');
}
console.log(longestCommonSubsequence("abcde", "ace")); // 3
console.log(printLCS("abcde", "ace")); // "ace"
// Time: O(m*n), Space: O(m*n)
2.3.4 Fast & Slow Pointers (Floyd’s Cycle Detection)
Usado para detectar ciclos em linked lists e encontrar pontos específicos.
Problema 1: Linked List Cycle
// LeetCode 141: Linked List Cycle
function hasCycle(head) {
if (!head || !head.next) return false;
let slow = head;
let fast = head.next;
while (fast && fast.next) {
if (slow === fast) return true;
slow = slow.next;
fast = fast.next.next;
}
return false;
}
// Time: O(n), Space: O(1)
Problema 2: Find Cycle Start
// LeetCode 142: Linked List Cycle II
function detectCycle(head) {
if (!head || !head.next) return null;
// Fase 1: Detectar se há ciclo
let slow = head;
let fast = head;
while (fast && fast.next) {
slow = slow.next;
fast = fast.next.next;
if (slow === fast) break;
}
// Não há ciclo
if (!fast || !fast.next) return null;
// Fase 2: Encontrar início do ciclo
slow = head;
while (slow !== fast) {
slow = slow.next;
fast = fast.next;
}
return slow;
}
// Time: O(n), Space: O(1)
Problema 3: Middle of Linked List
// LeetCode 876: Middle of the Linked List
function findMiddle(head) {
let slow = head;
let fast = head;
while (fast && fast.next) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
// Time: O(n), Space: O(1)
2.3.5 Tree Traversal Patterns
DFS - Depth First Search
// Pre-order: Root → Left → Right
function preorder(root, result = []) {
if (!root) return result;
result.push(root.val);
preorder(root.left, result);
preorder(root.right, result);
return result;
}
// In-order: Left → Root → Right
function inorder(root, result = []) {
if (!root) return result;
inorder(root.left, result);
result.push(root.val);
inorder(root.right, result);
return result;
}
// Post-order: Left → Right → Root
function postorder(root, result = []) {
if (!root) return result;
postorder(root.left, result);
postorder(root.right, result);
result.push(root.val);
return result;
}
Problema 1: Maximum Depth of Binary Tree
// LeetCode 104: Maximum Depth of Binary Tree
function maxDepth(root) {
if (!root) return 0;
return 1 + Math.max(maxDepth(root.left), maxDepth(root.right));
}
// Versão iterativa (BFS)
function maxDepthIterative(root) {
if (!root) return 0;
const queue = [root];
let depth = 0;
while (queue.length > 0) {
const levelSize = queue.length;
depth++;
for (let i = 0; i < levelSize; i++) {
const node = queue.shift();
if (node.left) queue.push(node.left);
if (node.right) queue.push(node.right);
}
}
return depth;
}
// Time: O(n), Space: O(h) where h is height
Problema 2: Path Sum
// LeetCode 112: Path Sum
function hasPathSum(root, targetSum) {
if (!root) return false;
// Se é folha, verifica se o valor é igual ao targetSum
if (!root.left && !root.right) {
return root.val === targetSum;
}
// Recursivamente verifica subárvores com targetSum atualizado
const remainingSum = targetSum - root.val;
return hasPathSum(root.left, remainingSum) || hasPathSum(root.right, remainingSum);
}
// LeetCode 113: Path Sum II - Retorna todos os caminhos
function pathSum(root, targetSum) {
const result = [];
function dfs(node, currentPath, remainingSum) {
if (!node) return;
currentPath.push(node.val);
// Se é folha e soma é correta
if (!node.left && !node.right && remainingSum === node.val) {
result.push([...currentPath]); // Copia do caminho
} else {
dfs(node.left, currentPath, remainingSum - node.val);
dfs(node.right, currentPath, remainingSum - node.val);
}
currentPath.pop(); // Backtrack
}
dfs(root, [], targetSum);
return result;
}
// Time: O(n), Space: O(h)
2.3.6 Backtracking Patterns
Template Base
function backtrackTemplate(candidates, target) {
const result = [];
function backtrack(currentSolution, remainingChoices, otherParams) {
// Condição de parada (base case)
if (isValidSolution(currentSolution)) {
result.push([...currentSolution]); // Copia da solução
return;
}
// Explora todas as possibilidades
for (let i = 0; i < remainingChoices.length; i++) {
const choice = remainingChoices[i];
// Poda: pula escolhas inválidas
if (!isValidChoice(choice, currentSolution)) continue;
// Faz a escolha
currentSolution.push(choice);
// Recursão com escolhas restantes
backtrack(
currentSolution,
getNextChoices(remainingChoices, i),
updateParams(otherParams, choice)
);
// Desfaz a escolha (backtrack)
currentSolution.pop();
}
}
backtrack([], candidates, target);
return result;
}
Problema 1: Subsets
// LeetCode 78: Subsets
function subsets(nums) {
const result = [];
function backtrack(start, currentSubset) {
// Toda combinação parcial é uma solução válida
result.push([...currentSubset]);
// Explora adicionar cada número restante
for (let i = start; i < nums.length; i++) {
currentSubset.push(nums[i]);
backtrack(i + 1, currentSubset); // i + 1 para evitar duplicatas
currentSubset.pop();
}
}
backtrack(0, []);
return result;
}
console.log(subsets([1, 2, 3]));
// [[], [1], [2], [1,2], [3], [1,3], [2,3], [1,2,3]]
// Time: O(2^n), Space: O(n)
Problema 2: Combination Sum
// LeetCode 39: Combination Sum
function combinationSum(candidates, target) {
const result = [];
function backtrack(start, currentCombination, remainingSum) {
if (remainingSum === 0) {
result.push([...currentCombination]);
return;
}
if (remainingSum < 0) return; // Poda
for (let i = start; i < candidates.length; i++) {
const candidate = candidates[i];
currentCombination.push(candidate);
// Pode reutilizar o mesmo elemento (não incrementa start)
backtrack(i, currentCombination, remainingSum - candidate);
currentCombination.pop();
}
}
backtrack(0, [], target);
return result;
}
console.log(combinationSum([2, 3, 6, 7], 7)); // [[2,2,3],[7]]
// Time: O(2^target), Space: O(target)
A Parte 1 foi construída com base na pesquisa profunda sobre JavaScript multiparadigma, SOLID principles,
programação funcional vs OOP vs imperativo, e análise TypeScript vs JavaScript para decisões arquiteturais
sólidas.