wandres.dev
CONSOLE II · Utilidades y contexto

El contexto de ejecución: iframes, workers y por qué tu selector devuelve null

El selector de contexto de la consola, qué mundos existen en una página, y cómo evaluar código en el sitio correcto.

⏱ 15 min

Una pestaña del navegador no tiene un contexto de JavaScript: tiene tantos como iframes, workers, worklets y extensiones haya en juego, y cada uno con su propio objeto global, su propio documento y su propio conjunto de variables. La consola evalúa en uno solo de ellos, elegido en un desplegable que la mayoría de la gente no ha tocado nunca, y esa elección explica el “pero si el elemento está ahí y querySelector me devuelve null” que todo el mundo ha vivido.

🎯 Al terminar esta lección sabrás
  • Enumerar los tipos de contexto que pueden coexistir en una pestaña.
  • Cambiar el contexto de evaluación de la consola y verificar en cuál estás.
  • Evaluar código dentro de un iframe, incluidos los de origen distinto y sus límites.
  • Depurar código que se ejecuta en un worker.

Los mundos que conviven

El contexto principal del documento de nivel superior. Es el que la consola usa por defecto y el que la gente asume siempre.

Un contexto por cada iframe. Cada uno con su window, su document y sus variables. Un document.querySelector en el contexto principal no ve nada de dentro de un iframe, por mucho que el panel de Elements deje entrar visualmente.

Un contexto por cada worker, sea dedicado, compartido o de servicio. No tienen document ni window; su global es distinto. Aparecen listados en el desplegable cuando están activos.

Los mundos aislados de las extensiones. Un script de contenido de una extensión se ejecuta en el mismo documento pero con un global separado: comparte el DOM y no comparte las variables. Por eso una extensión puede definir una variable con el mismo nombre que la tuya sin conflicto.

Los worklets, para audio, animaciones y pintado, con contextos muy restringidos.

El desplegable de la barra de la consola lista todos los que existen en ese momento. Seleccionar uno cambia dónde se evalúa lo que escribas, y la barra lo indica cuando no es el principal, que es la señal a la que hay que acostumbrarse a mirar.

⚠️
Cuidado

El contexto seleccionado se restablece al principal en cada navegación, así que si trabajas dentro de un iframe y la página recarga, la siguiente expresión se evalúa en otro sitio sin ningún aviso. Los null inexplicables después de una recarga son casi siempre esto.

Iframes

Cuando el iframe es del mismo origen, hay dos formas de llegar.

La primera es cambiar el contexto en el desplegable y escribir normalmente. Es lo más cómodo cuando vas a trabajar un rato dentro.

La segunda es acceder desde el contexto principal a través de la referencia del elemento.

const marco = document.querySelector('iframe');
marco.contentDocument.querySelectorAll('button');
marco.contentWindow.miVariableGlobal;

Cuando el iframe es de origen distinto, el modelo de seguridad lo impide. contentDocument devuelve null y cualquier acceso a contentWindow más allá de unas pocas propiedades lanza un error de seguridad. Esto no es una limitación de las DevTools: es la política del mismo origen y no hay forma de saltársela desde la consola.

Lo que puedes hacer con un iframe de otro origen es seleccionar su contexto en el desplegable y evaluar ahí. La consola tiene privilegios que la página no tiene, así que desde dentro del contexto del iframe puedes inspeccionar su documento con normalidad. Lo que no puedes es cruzar la frontera programáticamente en una sola expresión.

Hay una peculiaridad que conviene conocer: los iframes de otro origen se renderizan en un proceso distinto, y eso hace que ciertas operaciones —como pausar la ejecución— afecten solo a uno de los dos. Es posible tener el documento principal detenido en un breakpoint y el iframe siguiendo su curso.

Workers

Un worker aparece en el desplegable de contexto y en el árbol de Sources bajo su propia sección. Seleccionarlo permite evaluar en su ámbito, donde self es el global y no existen ni document ni window.

Tres cosas que hacer con un worker desde la consola.

Inspeccionar su estado. Cambiando al contexto y consultando las variables del ámbito global del worker.

Poner breakpoints. Los ficheros del worker aparecen en Sources y aceptan breakpoints como cualquier otro. Al detenerse, la consola cambia de contexto automáticamente si tienes activada la selección automática.

Ver sus mensajes. Un console.log desde un worker aparece en la consola principal con una marca de origen. No hay que hacer nada especial para verlos, que es una confusión frecuente.

Para depurar la comunicación, el patrón que más rinde es instrumentar los dos extremos.

// En el contexto principal, ver todo lo que cruza en ambos sentidos
const w = /* tu instancia de Worker */ null;
if (w) {
  const enviar = w.postMessage.bind(w);
  w.postMessage = (...a) => { console.debug('%c-> worker', 'color:#89b4fa', ...a); return enviar(...a); };
  w.addEventListener('message', e => console.debug('%c<- worker', 'color:#a6e3a1', e.data));
}

Verificar dónde estás

La costumbre que evita la mitad de los problemas de esta lección: cuando algo devuelva un resultado sorprendente, comprueba el contexto antes de dudar del código.

// Donde estoy evaluando ahora mismo
console.log({
  url: location.href,
  esVentanaPrincipal: typeof window !== 'undefined' && window === window.top,
  tieneDocumento: typeof document !== 'undefined',
  origen: origin
});

En el contexto principal, esVentanaPrincipal es true. En un iframe, false. En un worker, la expresión falla al no existir window, lo cual también es una respuesta.

El mismo código, el mismo nombre de variable y dos mundos que no se ven

El caso que más desconcierta de esta lección no es el iframe, que al menos se ve en el árbol, sino el de las extensiones y sus mundos aislados, porque produce una situación que parece violar las reglas del lenguaje. Un script de contenido de una extensión comparte el DOM con tu página: puede leer y modificar los mismos nodos, escuchar los mismos eventos y ver los mismos estilos. Y sin embargo tiene un objeto global completamente separado: sus variables no son tus variables, sus prototipos no son tus prototipos, y sus clases no son tus clases. La consecuencia más rara de todas es que una comprobación de tipo puede fallar sobre un objeto perfectamente válido: si una extensión te pasa un array creado en su mundo, Array.isArray funciona porque tiene un tratamiento especial, pero objeto instanceof Array devuelve false, porque el Array de su mundo no es el Array del tuyo. Lo mismo ocurre entre iframes, y es la razón por la que la comprobación robusta de tipos entre contextos usa Object.prototype.toString.call o funciones específicas y no instanceof. Para depurar, la implicación práctica es doble. Primera: cuando una página se comporta de forma inexplicable, comprobar el desplegable de contexto es tan barato como mirar el filtro de la consola y descarta una familia entera de causas; si aparecen contextos de extensión que no esperabas, ya sabes que hay código ajeno tocando tu documento. Segunda, y es el argumento definitivo del perfil limpio para desarrollo: un mundo aislado que modifica tu DOM es indistinguible de un bug de tu aplicación desde cualquier herramienta que uses, porque los cambios aparecen en el árbol, disparan tus observadores y afectan a tu layout, sin que haya nada en tu código que los explique. La única forma de descartarlo en un segundo es abrir la misma página sin extensiones.