Volver a Desarrollador

JavaScript

JavaScript esencial para testers y desarrolladores — fundamentos del lenguaje, asincronía y POO.

Fundamentos

const vs let vs var: ¿cuál usar y por qué?
Usa const por defecto (la asignación no se puede reasignar), let cuando necesites reasignar, y evita var — tiene ámbito de función y hoisting con comportamiento confuso, mientras const/let tienen ámbito de bloque. Ojo: const impide la reasignación, no la mutación: las propiedades de un objeto const sí pueden cambiar.
¿Qué hacen map, filter y reduce, y en qué se diferencian de forEach?
map transforma cada elemento y devuelve un nuevo array del mismo tamaño; filter devuelve un nuevo array solo con los elementos que pasan un predicado; reduce acumula el array en un único valor. forEach solo itera por efectos secundarios y no devuelve nada. map/filter/reduce son puros (no mutan el original); push/pop/splice/sort sí mutan.
¿Para qué sirven el destructuring y el operador spread?
El destructuring desempaqueta valores de arrays u objetos en variables: const { name, age } = user, o en parámetros de función function f({ id }). El spread (...) copia/fusiona: const merged = { ...base, env: 'prod' } o const all = [...a, ...b]. Las copias con spread son superficiales — los objetos anidados se comparten, no se clonan.
¿Qué hacen el optional chaining (?.) y el nullish coalescing (??)?
El optional chaining devuelve undefined en lugar de lanzar error cuando un valor intermedio es null/undefined: res.data?.user?.name. El nullish coalescing devuelve el lado derecho solo cuando el izquierdo es null o undefined (no para 0 o '', a diferencia de ||): const total = res.data?.order?.total ?? 0. Ambos son habituales al aseverar sobre la forma de respuestas de API.
En un desafío de código de React, un componente define su estado usando una variable `let` en lugar del mecanismo adecuado de React. ¿Qué está mal con este enfoque y cómo se corrige?
Usar una variable let para el estado en un componente de React no dispara re-renderizados cuando el valor cambia, por lo que la interfaz no se actualizará. La solución es reemplazar la variable let con el hook useState. Por ejemplo, cambiar let count = 0 a const [count, setCount] = useState(0), y actualizar el valor usando la función setter (setCount). Esto asegura que React rastree el cambio de estado y vuelva a renderizar el componente correctamente.
Escribe una función recursiva para calcular el factorial de un número.
Una función factorial recursiva funciona multiplicando el número por el factorial de (número - 1), con un caso base que retorna 1 cuando la entrada es 0 o 1: function factorial(n) { if (n == 1 || n == 0) return 1; else return n * factorial(n - 1); } Nota: En una implementación en producción, también se debe agregar una condición para manejar números negativos (por ejemplo, lanzar un error o retornar un valor apropiado).
¿Cómo llamas a los servicios API en una aplicación React?
En React, puedes llamar a los servicios API usando axios (una librería cliente HTTP basada en promesas) o la API nativa fetch. Ambos permiten hacer solicitudes HTTP a servicios externos desde tus componentes, típicamente dentro de un hook useEffect.
¿Cómo haces testing de los componentes en React?
Los componentes de React se pueden testear usando Jest como test runner y librería de aserciones, junto con React Testing Library para renderizar componentes y simular interacciones del usuario en un entorno de pruebas.
¿Cuáles son las áreas clave en las que enfocarse al prepararse para un desafío de código frontend en plataformas como HackerRank?
Las áreas clave incluyen la manipulación del DOM y el consumo de APIs para mostrar datos en la UI. Es esencial sentirse cómodo obteniendo datos de endpoints y actualizando dinámicamente la página para la mayoría de los desafíos de código frontend.
Implemente una solución de análisis de logs usando Node.js streams que lea un archivo de log y categorice las entradas por nivel (ERROR, WARN, DEBUG, INFO).
Hay dos enfoques comunes: **Enfoque simple usando readline:** javascript import fs from "fs"; import readline from "readline"; const stream = fs.createReadStream("app.log"); const rl = readline.createInterface({ input: stream, crlfDelay: Infinity }); const counters = { ERROR: 0, WARN: 0, DEBUG: 0, INFO: 0, UNKNOWN: 0 }; rl.on("line", (line) => { if (line.includes("[ERROR]")) counters.ERROR++; else if (line.includes("[WARN]")) counters.WARN++; else if (line.includes("[DEBUG]")) counters.DEBUG++; else if (line.includes("[INFO]")) counters.INFO++; else counters.UNKNOWN++; }); rl.on("close", () => { console.log("Resumen de logs:"); console.log(counters); }); **Usando Transform streams puros:** javascript import fs from "fs"; import { Transform } from "stream"; class LogParser extends Transform { constructor() { super({ readableObjectMode: true }); this.buffer = ""; } _transform(chunk, encoding, callback) { this.buffer += chunk.toString(); const lines = this.buffer.split("\n"); this.buffer = lines.pop(); // línea incompleta for (const line of lines) { this.push(this.parseLine(line)); } callback(); } _flush(callback) { if (this.buffer) { this.push(this.parseLine(this.buffer)); } callback(); } parseLine(line) { if (line.includes("[ERROR]")) return { level: "ERROR", line }; if (line.includes("[WARN]")) return { level: "WARN", line }; if (line.includes("[DEBUG]")) return { level: "DEBUG", line }; if (line.includes("[INFO]")) return { level: "INFO", line }; return { level: "UNKNOWN", line }; } } fs.createReadStream("app.log") .pipe(new LogParser()) .on("data", (log) => { console.log(log); }); **Mejora con regex para coincidencias más flexibles:** javascript const patterns = { ERROR: /\[ERROR\]/, WARN: /\[WARN(ING)?\]/, DEBUG: /\[DEBUG\]/, INFO: /\[INFO\]/ }; function detectLevel(line) { for (const [level, regex] of Object.entries(patterns)) { if (regex.test(line)) return level; } return "UNKNOWN"; } El enfoque con Transform streams es preferido porque procesa los datos fragmento por fragmento sin cargar todo el archivo en memoria, maneja líneas incompletas mediante un buffer y genera objetos estructurados para procesamiento posterior.
¿Cómo detectarías eventos superpuestos en una lista de eventos basados en tiempo usando JavaScript?
Ordena el arreglo de eventos por su hora de inicio, luego itera por el arreglo ordenado comparando la hora de inicio de cada evento con la hora de fin del evento anterior. Si el inicio del evento actual es anterior al fin del evento previo, hay una superposición. Aquí hay un ejemplo de implementación: const hasOverlappingEvents = (events) => { const sorted = [...events].sort((a, b) => a.start.localeCompare(b.start) ); for (let i = 1; i < sorted.length; i++) { if (sorted[i].start < sorted[i - 1].end) { return true; } } return false; }; Este enfoque tiene complejidad temporal O(n log n) debido al ordenamiento, seguido de un recorrido lineal O(n) por el arreglo.
¿Cuándo se debe usar event.preventDefault() en JavaScript?
event.preventDefault() se utiliza para detener el comportamiento por defecto de ciertos elementos HTML. El caso de uso más común es evitar que la página se recargue al hacer submit de un formulario, que es el comportamiento por defecto del evento submit del form HTML. Se necesita específicamente cuando implementas un onClick en elementos como un anchor tag (<a>) o un botón dentro de un form con type="submit". Muchos desarrolladores saben qué hace event.preventDefault(), pero la clave está en conocer el caso de uso correcto: se usa cuando quieres manejar un evento con lógica JavaScript personalizada mientras suprimes la respuesta integrada del navegador a ese evento.
¿Cuáles son las preguntas teóricas comunes de JavaScript en entrevistas frontend?
Las preguntas teóricas comunes de JavaScript en entrevistas frontend incluyen: 1) ¿Cómo funciona el hoisting en JS? 2) ¿Qué es y cómo funciona el event loop? 3) ¿Cuál es la diferencia entre var, let y const? 4) ¿Qué es un closure y cómo implementarías uno? 5) ¿Cómo funciona el 'this' en JS? 6) ¿Qué es la desestructuración y cómo se hace en objetos y arrays? 7) ¿Cuál es la diferencia entre map y forEach? 8) ¿Cuál es la diferencia entre find y some? 9) ¿Cuál es la diferencia entre == y ===? 10) ¿Cómo funcionan las promesas? 11) ¿Qué hace 'finally' en un try/catch? Son conceptos del core del lenguaje que ayudan a los entrevistadores a evaluar si el candidato se ha tomado el tiempo de comprender a fondo los fundamentos de JavaScript.
En una aplicación React, ¿por qué generalmente se recomienda usar clases CSS o un archivo CSS separado en lugar de estilos en línea?
Usar estilos en línea en React (mediante la sintaxis style={{}}) se considera una mala práctica. Incluso si solo necesitas un par de declaraciones de estilo, es mejor crear un archivo CSS o usar clases CSS. Los estilos en línea reducen la mantenibilidad, la reutilización y la separación de responsabilidades. Durante revisiones de código y entrevistas técnicas, usar estilos en línea puede ser visto negativamente porque indica falta de adherencia a las mejores prácticas en desarrollo frontend. Si ya hay un archivo CSS disponible (como en entornos como CodePen), se debe utilizar creando clases o IDs apropiados en lugar de aplicar estilos directamente en los elementos.
¿Cuáles son los ejercicios de live coding más comunes en entrevistas técnicas fullstack (React/Node)?
Los ejercicios de live coding varían, pero los reportados con más frecuencia incluyen: (1) Desafíos algorítmicos como la secuencia de Fibonacci, encontrar el camino más corto a un nodo, y manipulación de strings (por ejemplo, contar cuántas veces se puede formar una palabra a partir de un string dado). (2) Para entrevistas específicas de React: implementar un componente de lista, construir un scroll infinito, depurar y corregir un componente existente, refactorizar un componente para separar responsabilidades y eliminar renders innecesarios. (3) Ejercicios backend/generales: consumir una API con paginación e implementar una tabla con paginación del lado del servidor. Se recomienda practicar en plataformas como LeetCode o HackerRank en dificultad media a difícil, cubriendo edge cases y buscando la solución óptima.
¿Qué es `this` en JavaScript?
this en JavaScript es una referencia al objeto actual — específicamente, el objeto que está ejecutando o llamando a la función en el momento de la invocación. Dentro de clases, se utiliza para hacer referencia a las propiedades o métodos de esa misma instancia. Sin embargo, su comportamiento varía según el contexto: en funciones regulares, this se refiere al objeto que invocó la función, mientras que en arrow functions, this se hereda del ámbito léxico superior (un scope arriba). Esencialmente, this se resuelve basándose en el contexto de ejecución en tiempo de ejecución. El concepto es análogo a self en Python y this en C# o Java, donde referencia los atributos o métodos de la instancia actual del objeto.
¿Qué conceptos de React se preguntan con frecuencia en entrevistas de frontend?
Los temas de React que aparecen con frecuencia en entrevistas de frontend incluyen: el propósito de useState (gestionar el estado local de un componente) y useEffect (manejar efectos secundarios como peticiones de datos o suscripciones), la diferencia entre state y props (el state es interno y mutable dentro del componente, mientras que las props son entradas externas pasadas desde un componente padre y son de solo lectura), y el concepto del Virtual DOM (una representación ligera en memoria del DOM real que React utiliza para calcular el conjunto mínimo de cambios necesarios antes de actualizar el navegador).
Escribe una función que detecte si un arreglo contiene algún elemento duplicado. Proporciona soluciones tanto en TypeScript como en Java.
Se muestran dos enfoques, ambos con complejidad temporal O(n). **TypeScript (usando Set — idiomático y conciso):** typescript function isDuplicated(array: number[]): boolean { const seen = new Set<number>(); for (const value of array) { if (seen.has(value)) { return true; } seen.add(value); } return false; } Se itera el arreglo; si el valor actual ya existe en el Set, se encontró un duplicado y se retorna true de inmediato. De lo contrario, el valor se agrega al set. **Java (usando HashMap):** java private static boolean isDuplicated(Integer[] array) { Map<Integer, Integer> tempMemory = new HashMap<>(); for (int i = 0; i < array.length; i++) { // O(n) if (tempMemory.containsKey(array[i])) { return true; } else { tempMemory.put(array[i], 1); } } return false; } Cada elemento se almacena como clave en un HashMap. Si la clave ya existe, se detecta un duplicado. Ambas soluciones hacen cortocircuito tan pronto como se encuentra el primer duplicado.
¿Cómo implementarías una solución en Node.js que lea un archivo de log grande como stream, analice cada línea por nivel de log (ERROR, DEBUG, INFO, WARNING), escriba las líneas coincidentes en archivos de salida separados y aplique lógica condicional adicional (por ejemplo, marcar líneas WARNING donde el valor de CPU sea ≥ 98%)?
Usa un Transform stream de Node.js para procesar el archivo de log fragmento a fragmento sin cargar todo el archivo en memoria. Almacena temporalmente las líneas incompletas entre fragmentos, luego separa por saltos de línea y procesa cada línea completa. Extrae el nivel del log de cada línea y redirige las líneas coincidentes al stream de escritura correspondiente. Aplica cualquier lógica condicional adicional (como una coincidencia con regex para el porcentaje de CPU) antes de escribir. Ejemplo: js import fs from "fs"; import { Transform } from "stream"; const input = fs.createReadStream("./data/logfile.log", { encoding: "utf8" }); const outputs = { ERROR: fs.createWriteStream("./data/error.log"), DEBUG: fs.createWriteStream("./data/debug.log"), INFO: fs.createWriteStream("./data/info.log"), WARNING: fs.createWriteStream("./data/warning.log"), HIGHCPU: fs.createWriteStream("./data/highcpu.log"), }; const transformStream = new Transform({ decodeStrings: false, transform(chunk, _, cb) { this.buffer = (this.buffer || "") + chunk; const lines = this.buffer.split("\n"); this.buffer = lines.pop(); for (const line of lines) { if (!line.trim()) continue; const parts = line.split(" - "); if (parts.length < 3) continue; const status = parts[1].trim(); const message = parts.slice(2).join(" - ").trim(); if (outputs[status]) { outputs[status].write(message + "\n"); } if (status === "WARNING") { const match = message.match(/(\d+)%/); if (match && Number(match[1]) >= 98) { outputs.HIGHCPU.write(message + "\n"); } } this.push(line + "\n"); } cb(); }, }); transformStream.on("end", () => { Object.values(outputs).forEach(s => s.end()); }); input.pipe(transformStream); Para un resumen de solo lectura (conteo de líneas por nivel sin escribir archivos separados), la interfaz readline sobre un createReadStream es suficiente: js import fs from "fs"; import readline from "readline"; const rl = readline.createInterface({ input: fs.createReadStream("app.log"), crlfDelay: Infinity }); const counters = { ERROR: 0, WARN: 0, DEBUG: 0, INFO: 0, UNKNOWN: 0 }; const patterns = { ERROR: /\[ERROR\]/, WARN: /\[WARN(ING)?\]/, DEBUG: /\[DEBUG\]/, INFO: /\[INFO\]/ }; rl.on("line", line => { const level = Object.entries(patterns).find(([, re]) => re.test(line))?.[0] ?? "UNKNOWN"; counters[level]++; }); rl.on("close", () => console.log(counters)); Puntos clave: - Siempre almacena en buffer la última línea (potencialmente incompleta) y antepónla al siguiente fragmento. - Usa readableObjectMode: true en el Transform si deseas emitir objetos parseados en lugar de strings crudos. - Los patrones regex son más limpios que cadenas de llamadas includes y más fáciles de extender.
Implementa una función `merge_coverage(segments)` que recibe una lista de segmentos con la forma `(member_id, start_day, end_day)`. Para el mismo `member_id`, fusiona los segmentos que se superponen o se tocan en sus extremos (inclusive). Retorna un diccionario que mapea cada `member_id` a su lista de tuplas `(start_day, end_day)` fusionadas. Ejemplo: ``` segments = [ ("m1", 1, 3), ("m1", 2, 6), ("m1", 8, 10), ("m1", 15, 18), ] // Esperado: { "m1": [(1, 6), (8, 10), (15, 18)] } segments = [("m1", 1, 4), ("m1", 4, 5)] // Esperado: { "m1": [(1, 5)] } // los extremos que se tocan también se fusionan ```
El algoritmo tiene tres pasos: (1) agrupar los segmentos por member_id, (2) ordenar cada grupo por start_day, (3) iterar y fusionar cualquier segmento cuyo inicio sea menor o igual al final del segmento fusionado actual. javascript function mergeCoverage(segments) { // Paso 1: agrupar por member_id const groups = {}; for (const [memberId, start, end] of segments) { if (!groups[memberId]) groups[memberId] = []; groups[memberId].push([start, end]); } // Pasos 2 y 3: ordenar y fusionar const result = {}; for (const [memberId, segs] of Object.entries(groups)) { segs.sort((a, b) => a[0] - b[0]); const merged = [[...segs[0]]]; for (let i = 1; i < segs.length; i++) { const [start, end] = segs[i]; const last = merged[merged.length - 1]; if (start <= last[1]) { // superposición o contacto — extender el segmento actual last[1] = Math.max(last[1], end); } else { merged.push([start, end]); } } result[memberId] = merged; } return result; } Detalle clave: la condición de fusión es start <= last[1] (no estrictamente <) para que los extremos que se tocan, como [1,4] y [4,5], también se fusionen en [1,5]. Una función auxiliar relacionada — detectar si dos eventos se superponen (sin fusionar) — sigue el mismo patrón de ordenar y comparar: javascript const hasOverlappingEvents = (events) => { const sorted = [...events].sort((a, b) => a.start - b.start); for (let i = 1; i < sorted.length; i++) { if (sorted[i].start < sorted[i - 1].end) return true; } return false; }; Complejidad temporal: O(n log n) por miembro debido al ordenamiento; espacio: O(n).
Dado un arreglo de objetos de eventos, cada uno con un `userId`, timestamp de `start` y timestamp de `end`, escribe una función que retorne el total de minutos que cada usuario ha acumulado en todos sus eventos.
Usa Array.reduce() para acumular minutos por userId. Por cada evento, calcula la duración en minutos convirtiendo las cadenas de inicio y fin a objetos Date, restándolas para obtener milisegundos y dividiendo entre (1000 * 60). Agrega ese valor al total acumulado del userId correspondiente en el objeto acumulador. js // dateUtils.js const calcDiffInMinutes = (start, end) => { const startDate = new Date(start); const endDate = new Date(end); return (endDate - startDate) / (1000 * 60); }; // eventService.js const calculateMinutesByUser = (events) => { return events.reduce((acc, event) => { const minutes = calcDiffInMinutes(event.start, event.end); acc[event.userId] = (acc[event.userId] || 0) + minutes; return acc; }, {}); }; // Ejemplo const input = [ { userId: 1, start: '2024-01-01T10:00:00', end: '2024-01-01T10:30:00' }, { userId: 1, start: '2024-01-01T11:00:00', end: '2024-01-01T11:45:00' }, { userId: 2, start: '2024-01-01T09:00:00', end: '2024-01-01T10:00:00' } ]; console.log(calculateMinutesByUser(input)); // { '1': 75, '2': 60 }
Dado un arreglo de objetos de eventos, cada uno con un string `start` y `end`, escribe una función que retorne `true` si algún par de eventos se superpone, y `false` en caso contrario.
Ordena el arreglo de eventos por hora de inicio usando localeCompare (ya que los strings en formato ISO se ordenan lexicográficamente). Luego itera a partir del segundo elemento y verifica si el start de cada evento es anterior al end del evento anterior. Si es así, existe una superposición. js const hasOverlappingEvents = (events) => { const sorted = [...events].sort((a, b) => a.start.localeCompare(b.start) ); for (let i = 1; i < sorted.length; i++) { if (sorted[i].start < sorted[i - 1].end) { return true; } } return false; }; // Ejemplo const events = [ { start: '10:00', end: '11:00' }, { start: '10:30', end: '11:30' } ]; console.log(hasOverlappingEvents(events)); // true
Dada una cadena de texto, determinar si es un palíndromo. La solución debe manejar cadenas que contengan espacios, puntuación y letras en mayúsculas y minúsculas (por ejemplo, 'A man, a plan, a canal, Panama' debería devolver true).
Usar la estrategia de dos punteros: colocar un puntero al inicio de la cadena y otro al final. Mover ambos punteros hacia el centro, saltando los caracteres no alfanuméricos y comparando los caracteres restantes sin distinguir entre mayúsculas y minúsculas. Si todos los pares correspondientes coinciden cuando los punteros se encuentran, la cadena es un palíndromo.
¿Qué tipo de desafíos suelen presentarse en las entrevistas de live coding para posiciones de desarrollador frontend con React?
Los desafíos comunes de live coding en React incluyen: implementar un componente de lista y scroll infinito, encontrar y corregir errores en un componente existente (debug), refactorizar un componente para separar correctamente las responsabilidades, y optimizar el código para evitar renders innecesarios.
¿Cuál es la diferencia entre throttling y debounce en el manejo de eventos en JavaScript?
Tanto throttling como debounce son técnicas para controlar con qué frecuencia se ejecuta una función en respuesta a eventos. Throttling limita la ejecución bloqueando (ignorando) las llamadas que ocurren con mayor frecuencia que un intervalo especificado, garantizando que la función se ejecute como máximo una vez por período. Debounce, en cambio, introduce un retardo y espera hasta que haya transcurrido un tiempo determinado desde la última llamada antes de ejecutar la función.
Dado un string de caracteres en minúsculas, retorna el índice del primer carácter único (no repetido) en el string. Si no existe ninguno, retorna -1.
Usa un mapa de frecuencias para contar cuántas veces aparece cada carácter en el string. Luego itera por el string una segunda vez y retorna el índice del primer carácter cuyo conteo sea exactamente 1. Si no se encuentra ninguno, retorna -1. Un error común es guardar solo si el carácter fue visto (un booleano) en lugar de cuántas veces apareció (un conteo), lo que imposibilita distinguir caracteres verdaderamente únicos de los repetidos. javascript function firstUniqueChar(s) { const freq = new Map(); for (let i = 0; i < s.length; i++) { const char = s[i]; freq.set(char, (freq.get(char) || 0) + 1); } for (let i = 0; i < s.length; i++) { if (freq.get(s[i]) === 1) { return i; } } return -1; } // Ejemplo: firstUniqueChar("dummyadummy") => 5 (el carácter 'a' en el índice 5 aparece solo una vez) Complejidad de tiempo: O(n). Complejidad de espacio: O(1) dado que el conjunto de caracteres es acotado (26 letras minúsculas).
¿Cuál es la diferencia en el manejo de excepciones en promesas, callbacks y async/await?
Los callbacks usan la convención error-first: el primer argumento es el objeto de error (si existe) y se maneja manualmente en cada callback. Las promesas usan .catch() o el segundo argumento de .then() para manejar rechazos, y los errores se propagan por la cadena. Async/await permite usar bloques try/catch estándar alrededor de las llamadas await, haciendo que el manejo de errores se vea síncrono y lineal. Los rechazos de promesas no capturados pueden fallar silenciosamente, mientras que las excepciones no capturadas en async/await se lanzan dentro del ámbito del try/catch.
¿Qué es el debouncing y por qué es importante en un input de autocompletado?
El debouncing retrasa la ejecución de una función hasta que ha pasado un tiempo específico desde la última invocación. En un autocompletado, evita disparar una llamada API en cada tecla presionada; en cambio, la solicitud solo se envía después de que el usuario deja de escribir (por ejemplo, 300ms). Esto reduce la carga del servidor, evita resultados parpadeantes y mejora el rendimiento. Típicamente se implementa con setTimeout y clearTimeout dentro de un useEffect o un hook personalizado.
¿Qué característica de HTML5 permite crear un componente completamente funcional sin depender de frameworks externos?
Los Web Components combinan tres APIs del navegador: Custom Elements (para definir nuevas etiquetas HTML), Shadow DOM (para encapsular estilos y marcado) y HTML Templates. Juntos permiten construir componentes de UI reutilizables y autocontenidos de forma nativa sin ningún framework.
¿Qué característica de JavaScript permite rastrear cambios en datos y actualizar la UI de forma reactiva sin usar un framework?
El objeto Proxy permite interceptar y redefinir operaciones fundamentales sobre un objeto—lecturas de propiedades, asignaciones y eliminaciones. Al capturar estas operaciones se pueden detectar mutaciones y disparar actualizaciones en el DOM. Vue 3 usa exactamente este mecanismo para implementar su sistema de reactividad.
¿Qué es el Tree Shaking en los bundlers de JavaScript?
Tree Shaking es una técnica de eliminación de código muerto usada por los bundlers modernos (Webpack, Rollup, Vite) para eliminar exportaciones que no se importan en ningún lugar de la aplicación. Funciona con módulos ES6 (import/export) porque su grafo de dependencias es analizable estáticamente en tiempo de compilación, a diferencia de los require() de CommonJS que son dinámicos. El resultado es un bundle de producción más pequeño.
¿Cuál es la diferencia entre Microtasks y Macrotasks en el event loop de JavaScript?
Las microtasks (callbacks de Promises, queueMicrotask(), MutationObserver) se ejecutan inmediatamente después de que termina la tarea síncrona actual, antes de que el navegador renderice o procese alguna macrotask. Las macrotasks (setTimeout, setInterval, eventos de I/O, eventos de UI) se procesan de una en una por iteración del event loop tras vaciar todas las microtasks pendientes. El ciclo es: código síncrono → vaciar cola de microtasks → renderizar → una macrotask → repetir.
¿Cuál es la diferencia entre async/await y Promises en JavaScript?
Son capas diferentes del mismo mecanismo: internamente, toda función async devuelve una Promise, y cada expresión await es azúcar sintáctico sobre .then(). async/await produce código más legible y simplifican el manejo de errores con try/catch. Las Promises exponen combinadores como Promise.all(), Promise.race() y Promise.allSettled() que no tienen equivalente directo en async/await.
¿Qué causa las fugas de memoria en JavaScript y cómo se previenen?
JavaScript usa recolección de basura para liberar objetos inalcanzables. Las fugas ocurren cuando se retienen referencias involuntariamente: timers/intervalos olvidados, event listeners obsoletos, closures que capturan objetos grandes y nodos DOM desconectados. Las SPAs son especialmente propensas. Para prevenirlas: limpiar siempre los timers y eliminar los event listeners en las funciones de limpieza (cleanup de useEffect en React, AbortController para fetch).
¿Qué es un closure en JavaScript?
Un closure es una función que retiene acceso a las variables de su ámbito léxico externo incluso después de que esa función externa ha retornado. Esto proporciona a la función interna una referencia persistente a su contexto de creación. Los closures son fundamentales para patrones como la encapsulación de datos, funciones factory, memoización y los hooks de React, que dependen de closures para capturar el estado por render.
¿Cuál es la diferencia entre Service Workers y Web Workers?
Ambos ejecutan JavaScript fuera del hilo principal pero con propósitos distintos. Los Web Workers manejan computación intensiva en CPU (p. ej., procesamiento de imágenes) sin bloquear la UI; existen solo mientras la página está abierta y no pueden acceder al DOM. Los Service Workers actúan como proxy de red, habilitando caché offline, notificaciones push y sincronización en segundo plano; persisten después de cerrar todas las pestañas e interceptan los eventos fetch.
¿Qué es el Type Narrowing en TypeScript?
El Type Narrowing es el proceso de refinar una variable desde un tipo unión amplio a un tipo más específico basándose en comprobaciones condicionales en tiempo de ejecución como typeof, instanceof o in. Tras la comprobación, el compilador de TypeScript conoce el tipo preciso dentro de esa rama y aplica los métodos y propiedades correctos. Por ejemplo, después de if (typeof value === 'string'), TypeScript trata value como string dentro del bloque.
¿Cuál es la diferencia entre any, unknown y never en TypeScript?
any desactiva por completo la verificación de tipos—se puede llamar cualquier cosa sobre él; es conveniente pero inseguro y debe ser el último recurso. unknown es su contraparte segura: hay que hacer narrowing (typeof, instanceof o un type guard) antes de realizar operaciones. never representa valores que nunca pueden existir; se usa en funciones que siempre lanzan excepción y en comprobaciones exhaustivas de uniones discriminadas.
¿Cuál es la diferencia entre interface y type alias en TypeScript?
Ambos pueden describir formas de objetos, pero difieren en capacidades. Las interfaces soportan la fusión de declaraciones (dos declaraciones con el mismo nombre se combinan automáticamente) y usan extends para la herencia. Los type aliases usan la intersección (&) para la composición y pueden representar primitivos, tipos unión, tuplas y tipos mapeados. Se recomienda usar interface para contratos de API públicas y type para composiciones complejas o cuando la fusión no es deseada.
¿Cuál es la diferencia entre una Function Declaration y una Function Expression en JavaScript?
Una Function Declaration (function foo() {}) es completamente hoisted—el motor mueve tanto el nombre como el cuerpo al inicio del ámbito, por lo que puede llamarse antes de que aparezca en el código. Una Function Expression (const foo = function() {} o una arrow function) asigna una función a una variable; la variable se hoistea pero se inicializa a undefined, así que llamarla antes de la asignación lanza un TypeError. Las arrow functions son un tipo de function expression y además no tienen su propio binding de this.
¿Cómo implementarías una versión personalizada de `Promise.all` sin usar el método nativo `Promise.all`? La función debe aceptar un array de valores que pueden ser promesas o valores simples, y debe simular el mismo comportamiento de resolve/reject.
La función personalizada debe retornar una nueva Promise que se resuelva con un array de todos los valores resueltos una vez que cada entrada se haya completado exitosamente, y que rechace inmediatamente con la primera razón de rechazo encontrada. Cada elemento del array de entrada debe envolverse con Promise.resolve() para manejar correctamente los valores simples (no-promesas). Ejemplo de implementación usando forEach (más legible que reduce): javascript function myPromiseAll(items) { return new Promise((resolve, reject) => { const results = []; let resolvedCount = 0; if (items.length === 0) { resolve(results); return; } items.forEach((item, index) => { Promise.resolve(item) .then(value => { results[index] = value; resolvedCount++; if (resolvedCount === items.length) { resolve(results); } }) .catch(reject); }); }); } Puntos clave: - Promise.resolve(item) normaliza tanto entradas de tipo promesa como valores simples. - Los resultados se almacenan por índice para preservar el orden de entrada independientemente del orden de resolución. - La Promise externa rechaza en cuanto cualquier promesa interna rechaza. - También se puede usar async/await dentro de la función auxiliar si se prefiere.
Implementa una función `debounce` en JavaScript. Debe recibir una función y un tiempo de espera en milisegundos, y devolver una nueva función que retrase la ejecución de la original hasta que transcurra el tiempo indicado sin que se realicen nuevas llamadas.
Una función debounce utiliza un closure para mantener la referencia a una variable de temporizador. Cada vez que se invoca la función devuelta, el temporizador anterior se cancela con clearTimeout y se programa uno nuevo con setTimeout. La función original solo se ejecuta una vez que transcurre el período de espera completo sin otra llamada. javascript function debounce(fn, wait) { let timer; return function(...args) { clearTimeout(timer); timer = setTimeout(() => { fn.apply(this, args); }, wait); }; } Concepto clave: el closure preserva la referencia al timer entre llamadas. Cada nueva invocación reinicia la cuenta regresiva, de modo que la función envuelta solo se ejecuta tras un período de inactividad igual a wait milisegundos.
¿Cómo extenderías un método nativo del objeto Array en JavaScript, o cómo agregarías un método personalizado disponible en todas las instancias de array?
En JavaScript puedes extender objetos nativos como Array agregando propiedades o métodos directamente a su prototipo. Por ejemplo: Array.prototype.miMetodoPersonalizado = function() { /* implementación */ }; añade miMetodoPersonalizado a todas las instancias de array, ya que todos los arrays heredan de Array.prototype a través de la cadena de prototipos. Aunque esta técnica funciona, debe usarse con precaución en código de producción, ya que puede generar conflictos con métodos nativos futuros o con librerías de terceros que también modifiquen el mismo prototipo.
Escribe una función que detecte si un arreglo de números contiene valores duplicados.
Tanto un HashMap (Java) como un Set (TypeScript/JavaScript) permiten resolver esto con complejidad temporal O(n). Solución en TypeScript usando un Set (limpia e idiomática): typescript function isDuplicated(array: number[]): boolean { const seen = new Set<number>(); for (const value of array) { if (seen.has(value)) { return true; } seen.add(value); } return false; } Solución en Java usando un HashMap: java private static boolean isDuplicated(Integer[] array) { Map<Integer, Integer> tempMemory = new HashMap<>(); for (int i = 0; i < array.length; i++) { if (tempMemory.containsKey(array[i])) { return true; } else { tempMemory.put(array[i], 1); } } return false; } Ambos enfoques recorren el arreglo una sola vez almacenando cada elemento en una estructura basada en hash. En cuanto se encuentra un elemento repetido, la función retorna true de inmediato (salida temprana). Si el bucle finaliza sin encontrar duplicados, retorna false. La complejidad temporal es O(n) y la espacial es O(n) en el peor caso.
Dado un archivo de log donde cada línea sigue el formato `<timestamp> - <STATUS> - <message>`, implementa un stream `Transform` de Node.js llamado `transformStream` que lea el archivo y enrute cada línea a un archivo de salida diferente según su estado (ERROR, DEBUG, INFO, WARNING). Además, cualquier línea WARNING que contenga un porcentaje de CPU igual o superior al 98% debe escribirse también en un archivo HIGHCPU.
Usa la clase stream.Transform de Node.js para procesar el archivo chunk a chunk, acumulando líneas incompletas entre chunks dividiendo por saltos de línea y conservando el último segmento (potencialmente incompleto) en un buffer. Por cada línea completa, divide por - para extraer el campo de estado y el mensaje. Luego escribe el mensaje en el stream de escritura de salida correspondiente. Para las líneas WARNING, comprueba adicionalmente con una expresión regular (/\d+%/) si el porcentaje es >= 98 y, en ese caso, también escríbela en el stream HIGHCPU. Ejemplo de implementación: js function captureLogs(includeSignature = false) { const fs = require('fs'); const { Transform } = require('stream'); const input = fs.createReadStream('./data/logfile.log', { encoding: 'utf8' }); const outputs = { ERROR: fs.createWriteStream('./data/error.log'), DEBUG: fs.createWriteStream('./data/debug.log'), INFO: fs.createWriteStream('./data/info.log'), WARNING: fs.createWriteStream('./data/warning.log'), HIGHCPU: fs.createWriteStream('./data/highcpu.log'), }; // MUST be named transformStream const transformStream = new Transform({ decodeStrings: false, transform(chunk, _, cb) { this.buffer = (this.buffer || '') + chunk; const lines = this.buffer.split('\n'); this.buffer = lines.pop(); for (const line of lines) { if (!line.trim()) continue; const parts = line.split(' - '); if (parts.length < 3) continue; const status = parts[1].trim(); const message = parts.slice(2).join(' - ').trim(); if (outputs[status]) { outputs[status].write(message + '\n'); } if (status === 'WARNING') { const match = message.match(/(\d+)%/); if (match && Number(match[1]) >= 98) { outputs.HIGHCPU.write(message + '\n'); } } this.push(line + '\n'); } cb(); }, }); transformStream.on('end', () => { if (includeSignature) { const signature = { signature: generateSignature() }; fs.writeFileSync('./data/signature.json', JSON.stringify(signature)); } Object.values(outputs).forEach((s) => s.end()); }); input.pipe(transformStream); } Puntos clave: (1) Acumula chunks incompletos entre llamadas a transform para evitar cortar una línea a mitad. (2) Usa el lookup outputs[status] en lugar de una cadena larga de if-else. (3) La comprobación de HIGHCPU es aditiva: la línea también se escribe en el archivo WARNING. (4) Cierra todos los streams de escritura en el manejador del evento end.

Asincronía y POO

¿Qué es una Promise y qué estados puede tener?
Una Promise representa el resultado futuro de una operación asíncrona. Está pending mientras está en curso, fulfilled en éxito (se maneja con .then) o rejected en fallo (se maneja con .catch); .finally siempre se ejecuta. Promise.all corre muchas en paralelo y rechaza si alguna falla, Promise.allSettled espera a todas pase lo que pase, y Promise.race resuelve con la primera que termina.
¿Cómo se relaciona async/await con las Promises, y secuencial vs paralelo?
async/await es azúcar sintáctico sobre las Promises: una función async siempre devuelve una Promise, y await pausa dentro de ella hasta que la Promise termina, manejando errores con try/catch. Esperar en secuencia (await a; await b) es lento; para trabajo independiente ejecútalo en paralelo con await Promise.all([a, b]).
¿Qué hay que saber sobre las clases en JavaScript?
Una clase tiene un constructor que fija campos de instancia con this, métodos de instancia, y miembros estáticos a los que se accede en la propia clase (MathUtils.PI), no en las instancias. Los getters/setters exponen propiedades calculadas o protegidas (get fahrenheit(), set celsius(v) con validación). Los métodos factory estáticos como ApiResponse.ok(body) son una forma limpia de crear instancias comunes.
¿Cómo funcionan la herencia y el polimorfismo en JavaScript?
Una subclase usa extends y debe llamar a super(...) en su constructor antes de usar this; puede sobrescribir métodos y llamar a super.metodo() para extender el comportamiento. El polimorfismo significa que la misma llamada se comporta distinto según la clase — p. ej. una lista de Vehiculo/Coche/Moto que implementan info(). instanceof comprueba la cadena de prototipos.
¿Cómo se encapsula el estado en JavaScript?
Usa campos privados de clase con prefijo # (this.#saldo) para que no se puedan leer ni escribir desde fuera; expón acceso controlado mediante getters y métodos que validen la entrada. El enfoque clásico anterior a # es un closure (una función factory con una variable privada). Devolver una copia (p. ej. [...this.#historial]) evita filtrar referencias internas.
¿Qué patrones de diseño aparecen a menudo en frameworks de test?
Singleton (una instancia compartida, p. ej. un Config), Factory (construir objetos configurados, p. ej. un HttpClient por entorno), Builder (construcción fluida paso a paso, p. ej. un RequestBuilder) y Observer/EventEmitter (suscribirse a eventos, como un test runner que emite pass/fail). La inyección de dependencias — pasar los colaboradores en vez de fijarlos en el código — hace que todos sean fáciles de mockear.