12k
All articles

10 Preguntas de Entrevistas de JS y Qué Evalúan Realmente

10 preguntas de entrevista de JavaScript explicadas con hoisting, closures, this, el event loop y coerción, con resultados y lo que evalúan.

OpenReplay Team
OpenReplay Team
10 Preguntas de Entrevistas de JS y Qué Evalúan Realmente

La mayoría de las preguntas de entrevistas sobre JavaScript no buscan que respondas cuál es el output — buscan saber si comprendes el modelo que lo produce. Un entrevistador senior que escribe console.log(x); var x = 5; en la pizarra ya sabe que registra undefined. Lo que evalúa es si puedes explicar el porqué sin recurrir a la frase “el hoisting lo mueve al principio”, porque esa frase describe el modelo mental, no el mecanismo. Los candidatos que avanzan son quienes pueden nombrar el comportamiento en tiempo de ejecución, predecir el output y articular el razonamiento en voz alta en dos oraciones claras.

Este artículo aborda diez preguntas que corresponden a diez fundamentos distintos — el modelo de ejecución, la Temporal Dead Zone, los closures, el scope léxico, el binding de this, el event loop y la coerción — y desglosa cada una en cuatro partes: el fragmento de código, el output exacto y su razón, qué evalúa realmente el entrevistador, y el seguimiento que recibirás a continuación. Todos los outputs están definidos en la especificación y son reproducibles en cualquier motor actual. Los comportamientos en sí mismos son estables en todos los motores actuales — son semánticas del lenguaje, no peculiaridades de versiones específicas.

Puntos Clave

  • La pregunta sobre el hoisting de var no evalúa si sabes que el output es undefined — evalúa si entiendes que JavaScript crea los bindings de variables al entrar en un scope, antes de que se ejecute cualquier línea.
  • let y const también tienen hoisting, pero permanecen sin inicializar en la Temporal Dead Zone hasta su línea de declaración, por lo que leerlos antes lanza un ReferenceError en lugar de retornar undefined.
  • En el clásico bug de setTimeout dentro de un bucle for, var registra el valor final del bucle (3 3 3) porque todos los callbacks comparten un único binding con scope de función, mientras que let registra el valor de cada iteración (0 1 2) porque crea un binding nuevo por iteración.
  • this no se determina donde se escribe una función — se determina por cómo se llama la función; las arrow functions son la excepción porque no tienen this propio.
  • Los callbacks de Promise (microtareas) siempre se ejecutan antes que los callbacks de setTimeout (macrotareas), incluso con un delay de 0ms.

Preguntas de entrevistas de JavaScript sobre hoisting y el modelo de ejecución

1. ¿Por qué var registra undefined antes de la asignación?

console.log(x); // undefined
var x = 20;
console.log(x); // 20

La primera línea registra undefined, no un ReferenceError. La explicación popular es que la declaración se “mueve al principio”, pero eso es una metáfora. Lo que ocurre realmente: cuando el motor entra en un scope, instancia los bindings de ese scope antes de ejecutar cualquier instrucción, y un binding de var se inicializa a undefined en ese momento. La asignación = 20 permanece en su línea original y se ejecuta en orden. Este paso de creación de bindings está definido en la Especificación del Lenguaje ECMAScript bajo la instanciación del entorno de variables — nada se reubica físicamente.

Qué evalúan realmente: si entiendes que JS tiene una fase de creación antes de la fase de ejecución. La respuesta undefined es trivia; el modelo de ejecución en dos fases es la competencia que se evalúa. Consulta la entrada del glosario de Hoisting en MDN para el enfoque canónico.

Seguimiento probable: “¿Qué pasaría si fuera let en lugar de var?” — que es la pregunta 2.

2. ¿Por qué let lanza un error en lugar de registrar undefined?

console.log(y); // ReferenceError: Cannot access 'y' before initialization
let y = 20;

let y const tienen hoisting — el binding se crea al entrar en el scope — pero permanecen sin inicializar hasta que la ejecución alcanza la declaración. La ventana entre la entrada al scope y la línea de declaración es la Temporal Dead Zone, y leer un binding dentro de ella lanza ReferenceError: Cannot access 'y' before initialization. Esta es la distinción fundamental: var se inicializa a undefined en el momento de creación; let/const no se inicializan en absoluto hasta que se ejecuta su línea. MDN documenta esto en la sección de Temporal Dead Zone de let.

Qué evalúan realmente: si separas la creación del binding de la inicialización del binding. Un candidato que dice “let no tiene hoisting” tiene el modelo equivocado; la respuesta correcta es que sí tiene hoisting, pero permanece sin inicializar.

Seguimiento probable: “¿Por qué existe la TDZ?” — di que hace que const sea aplicable y convierte el uso antes de la declaración en un error explícito en lugar de un undefined silencioso.

A continuación, la tabla de referencia que los entrevistadores esperan que puedas reconstruir verbalmente:

Comportamientovarletconst
Hoisting (binding creado al entrar al scope)
Inicializado en la creaciónundefinedNo (TDZ)No (TDZ)
Lectura antes de la declaraciónundefinedReferenceErrorReferenceError
ScopeFunciónBloqueBloque
Redeclarable en el mismo scopeNoNo
ReasignableNo

3. Hoisting de declaraciones de función vs. expresiones de función

foo(); // "I run"
bar(); // TypeError: bar is not a function

function foo() { console.log("I run"); }
var bar = function () { console.log("I don't"); };

Una declaración de función tiene hoisting completo — el nombre y el cuerpo — por lo que foo() funciona antes de su definición. Una expresión de función asignada a var bar solo eleva el binding de bar, inicializado a undefined. Llamar a undefined lanza un TypeError, no un ReferenceError — el binding existe, simplemente no es una función todavía. Identificar correctamente el tipo de error es la clave aquí. MDN cubre esta diferencia en la documentación de declaraciones de función.

Qué evalúan realmente: si comprendes que las declaraciones elevan valores mientras que las expresiones solo elevan bindings. Es la misma distinción que en las preguntas 1 y 2, aplicada a funciones.

Seguimiento probable: “¿Qué pasaría si bar se declarara con let?” — entonces la llamada anticipada lanza un ReferenceError desde la TDZ en lugar de un TypeError.

Preguntas de entrevistas de JavaScript sobre closures y scope

4. El clásico setTimeout dentro de un bucle

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0); // 3 3 3
}

for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0); // 0 1 2
}

El bucle con var registra 3 3 3. Existe un único i con scope de función; para cuando los callbacks se ejecutan (después de que el bucle síncrono termina), i vale 3, y los tres closures leen ese mismo binding. El bucle con let registra 0 1 2 porque let crea un binding nuevo para cada iteración, de modo que cada callback cierra sobre su propio i. Este es el fragmento más malinterpretado en la web, y combina tres conceptos: closures, scope y el event loop (los callbacks son diferidos, por lo que el bucle termina primero). La guía de Closures de MDN documenta el binding por iteración de let.

Qué evalúan realmente: si puedes razonar sobre cuándo un closure lee una variable versus cuándo fue creado. Esto no es trivia — es la misma clase de bug que llega a producción como estado de closure obsoleto en event handlers y efectos de React. Los session replays de handlers con closures obsoletos frecuentemente revelan que registran un valor capturado de un render anterior.

Seguimiento probable: “Corrígelo sin usar let.” — envuelve el cuerpo en un IIFE que tome i como argumento, creando un nuevo scope por iteración.

5. El contador privado

function makeCounter() {
  let count = 0;
  return {
    increment: () => ++count,
    value: () => count,
  };
}

const c = makeCounter();
c.increment(); // 1
c.increment(); // 2
c.value();     // 2
// count es inaccesible desde el exterior

Un closure es una función empaquetada junto con las variables a las que tenía acceso donde fue definida, no donde se llama — por eso los métodos retornados aún pueden acceder a count después de que makeCounter ha retornado. Como nada externo expone count directamente, es efectivamente privada. Esto es encapsulación construida a partir del scope, no de un campo #private.

Qué evalúan realmente: si entiendes los closures como una herramienta de memoria y encapsulación, no solo como una respuesta de examen. El seguimiento indaga si conoces el costo.

Seguimiento probable: “¿Esto genera una fuga de memoria?” — la variable count permanece viva mientras c sea alcanzable, porque el closure mantiene una referencia; eso es intencional aquí, pero los closures no controlados sobre objetos grandes son una fuente real de fugas.

6. Scope léxico: definición, no llamada

const x = 10;

function outer() {
  const x = 20;
  return inner;
}

function inner() {
  console.log(x); // 10
}

outer()(); // 10

inner registra 10, no 20. JavaScript resuelve las variables libres según el lugar donde una función está definida en el código fuente, no donde se llama. inner está definida en el nivel superior, por lo que su x se resuelve al 10 del nivel superior — la x dentro de outer le es irrelevante. Esto es el scope léxico (estático), y es lo que hace que los closures sean predecibles.

Qué evalúan realmente: si confundes el call stack con la cadena de scope. Los lenguajes con scope dinámico registrarían 20; JavaScript no lo hace.

Seguimiento probable: “Ahora mueve la declaración de inner dentro de outer.” — entonces se resuelve a 20, porque su sitio de definición cambió.

Preguntas de entrevistas de JavaScript sobre el binding de this

7. ¿A qué se refiere this aquí?

const user = {
  name: "Ada",
  greet() { return this.name; },
};

const fn = user.greet;
user.greet(); // "Ada"
fn();         // undefined (o lanza un error en strict mode)

this no se determina donde se escribe una función — se determina por cómo se llama la función. Llamada como user.greet(), el objeto en el call-site es user, por lo que this.name es "Ada". Asignada a fn y llamada de forma simple, no hay objeto en el call-site, por lo que this es el objeto global en sloppy mode (haciendo que this.name sea undefined) o undefined en strict mode (haciendo que this.name lance un error). Las cuatro reglas de binding se resumen en la referencia de this en MDN.

Qué evalúan realmente: si sabes que this es dinámico y está determinado por el call-site, no fijado léxicamente.

Seguimiento probable: “¿Cómo fijas this a user?” — fn.call(user), fn.apply(user), o user.greet.bind(user).

Forma de llamadaA qué se enlaza this
fn() (simple)undefined (strict) / objeto global (sloppy)
obj.fn() (método)obj (el objeto a la izquierda del punto)
fn.call(o) / fn.apply(o) / fn.bind(o)o (explícito)
new Fn()la instancia recién creada
arrow functionheredado del scope léxico envolvente

8. this dentro de un callback

const timer = {
  seconds: 0,
  startBroken() {
    setInterval(function () { this.seconds++; }, 1000); // this es incorrecto
  },
  startFixed() {
    setInterval(() => { this.seconds++; }, 1000); // this es timer
  },
};

En startBroken, la function simple pasada a setInterval es llamada por el mecanismo del temporizador sin un objeto en el call-site, por lo que this no es timerthis.seconds++ muta el objeto equivocado. En startFixed, la arrow function no tiene this propio y lo hereda del scope de startFixed, donde this es timer. Esta es la razón de libro de texto por la que existen las arrow functions para callbacks. Consulta MDN sobre arrow functions.

Qué evalúan realmente: si entiendes el this léxico y por qué las arrow functions resolvieron el antiguo workaround de var self = this. El bug de this perdido aparece constantemente en producción en class components y event listeners.

Seguimiento probable: “¿Por qué no puedes usar una arrow function como constructor o como método de objeto que necesite su propio this?” — porque no tiene un binding de this que asignar, por lo que new lanza un error y una arrow a nivel de método captura el this externo en lugar del objeto.

Preguntas de entrevistas de JavaScript sobre el modelo de ejecución en tiempo de ejecución

9. Predice el orden en la consola (event loop)

console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
console.log("4");
// Output: 1 4 3 2

Los callbacks de Promise (microtareas) siempre se ejecutan antes que los callbacks de setTimeout (macrotareas), incluso con un delay de 0ms. La traza de ejecución:

  1. console.log("1") se ejecuta sincrónicamente → 1.
  2. setTimeout programa su callback en la cola de macrotareas.
  3. Promise.resolve().then(...) programa su callback en la cola de microtareas.
  4. console.log("4") se ejecuta sincrónicamente → 4.
  5. El call stack está ahora vacío. El motor vacía la cola de microtareas completa antes de cualquier macrotarea → 3.
  6. Solo entonces recoge la siguiente macrotarea → 2.

La guía de microtareas de MDN describe este ordenamiento.

Qué evalúan realmente: si entiendes que el event loop tiene dos niveles de prioridad, no una sola cola. El event loop de dos niveles es el modelo detrás de cada bug de “mi estado se actualizó un tick tarde”.

Seguimiento probable: “¿Dónde encaja async/await?” — el código después de un await se ejecuta como una microtarea, por lo que se encola con la misma prioridad que un callback de .then().

10. == vs === y coerción

0 == "0";        // true
0 === "0";       // false
null == undefined; // true
NaN === NaN;     // false
[] == ![];       // true

== realiza coerción de tipos antes de comparar, mientras que === no lo hace, por eso 0 == "0" es true pero 0 === "0" es false. Los casos contraintuitivos: null == undefined es true por una regla especial (y no son iguales a nada más bajo ==); NaN nunca es igual a nada, incluyendo a sí mismo; y [] == ![] es true porque ![] es false, que se coerciona a 0, y [] se coerciona a "" y luego a 0. El algoritmo completo está en la guía de comparaciones de igualdad de MDN.

Qué evalúan realmente: si puedes predecir la coerción — no si puedes recitar “usa siempre ===”. El entrevistador quiere el mecanismo, luego la regla general.

Seguimiento probable: “¿Qué pasa al asignar a una variable no declarada?” — si value = 42 (sin var/let/const) lanza un error o crea silenciosamente una variable global depende del modo: el strict mode y los módulos ES lanzan un ReferenceError; los scripts en sloppy mode crean un global implícito. El mismo fragmento, dos respuestas correctas — señalar esa distinción es en sí mismo una señal de nivel senior.

Qué distingue a un candidato contratado de uno descartado

El hilo conductor de las diez preguntas: los entrevistadores evalúan la explicación, no solo el output. Predecir 3 3 3 o 1 4 3 2 demuestra que has visto el problema antes; nombrar el modelo de binding, la cadena de scope, la regla del call-site y el event loop de dos niveles demuestra que entiendes el lenguaje lo suficientemente bien como para depurarlo bajo presión. Practica estas diez hasta que puedas explicar el porqué en dos oraciones sin notas — y dado que ninguno de estos comportamientos depende de la versión, puedes verificar cada fragmento tú mismo en cualquier motor actual y confiar en el resultado.

Preguntas Frecuentes

¿Cuál es la diferencia entre la Temporal Dead Zone y una variable que simplemente es undefined?

Una variable en la Temporal Dead Zone ha tenido hoisting pero aún no ha sido inicializada, por lo que leerla lanza un ReferenceError, mientras que un var con valor undefined ha tenido hoisting y ha sido inicializado al valor undefined, por lo que leerlo retorna undefined. Solo let y const crean una TDZ; esta abarca desde la entrada al scope hasta que se ejecuta la línea de declaración. La distinción es creación del binding versus inicialización del binding.

¿Por qué las arrow functions fallan cuando se usan como métodos de objeto o constructores?

Las arrow functions no tienen un binding de this propio, por lo que no pueden cumplir roles que lo requieran. Usada como método de objeto, una arrow captura this del scope léxico envolvente en lugar del objeto, por lo que this.property no apunta al objeto. Usada con new, el motor lanza un TypeError porque no hay un binding de this al que asignar la nueva instancia. Usa funciones regulares en ambos casos.

¿El bug de setTimeout en el bucle se comporta igual en todos los motores de JavaScript?

Sí. La versión con var registra el valor final del bucle en todos los motores actuales porque var crea un único binding con scope de función compartido por todos los callbacks, y la versión con let registra los valores por iteración en todos lados porque la especificación define un binding nuevo por iteración para let en un bucle for. Este comportamiento está definido en la especificación en ECMA-262, no es específico de ningún motor, por lo que Node.js, V8, SpiderMonkey y JavaScriptCore producen un output idéntico.

¿async y await cambian la prioridad en el event loop en comparación con Promise.then?

No. El código después de un await se ejecuta como una microtarea, con exactamente la misma prioridad que un callback de Promise.then, por lo que se ejecuta antes que cualquier macrotarea de setTimeout. Un await efectivamente pausa la función y programa la continuación en la cola de microtareas cuando el valor esperado se resuelve. Esto significa que una continuación de await y un callback de then encolados en el mismo punto se resuelven en orden de aparición en el código fuente, y ambos se ejecutan antes que cualquier callback de temporizador configurado a cero milisegundos.

Open-source session replay

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.