0% acharam este documento útil (0 voto)
24 visualizações104 páginas

Javascript Algorithms Interview Guide

Direitos autorais
© © All Rights Reserved
Levamos muito a sério os direitos de conteúdo. Se você suspeita que este conteúdo é seu, reivindique-o aqui.
Formatos disponíveis
Baixe no formato PDF, TXT ou leia on-line no Scribd
0% acharam este documento útil (0 voto)
24 visualizações104 páginas

Javascript Algorithms Interview Guide

Direitos autorais
© © All Rights Reserved
Levamos muito a sério os direitos de conteúdo. Se você suspeita que este conteúdo é seu, reivindique-o aqui.
Formatos disponíveis
Baixe no formato PDF, TXT ou leia on-line no Scribd
Você está na página 1/ 104

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.

Você também pode gostar