[Destripando JS] Descifrando el Call stack, event loop, stack/heap

EnderFX

Pre-tocho:

Estoy intentando arrancar con un blog sobre desarrollo próximamente, aunque me llevará tiempo tener todos los estilos y todo listo, pero me gustaría compartir con vosotros lo que voy preparando y que me dierais vuestra opinión y crítica constructiva, hijos de puta. Espero que os resulte interesante y, de paso, me digáis qué os gustó menos o qué cambiaríais, ya sea en forma o contenido.

Si llevas un tiempo programando con JavaScript, seguramente te hayas encontrado situaciones en las que las interacciones de usuario en la web que estabas programando empezaban a ralentizarse, o quizá te hayas dado de bruces con un problema inesperado de recursividad, que deja tu web completamente congelada.

Paulo Coelho

Para poder entender mejor estos problemas, y de cara a poder evitarlos en el futuro, conviene entender, aunque sea superficialmente, cómo funciona el motor de JS que utiliza nuestro navegador o aplicación de Node (v8 en Chrome/Chromium/NodeJS, SpiderMonkey en Firefox, JavaScriptCore en Safari).

¿Cómo funciona JavaScript?

En primer lugar, deberíamos cuestionarnos lo más básico. ¿Cómo funciona JS? De forma práctica podemos decir que, en esencia, cargamos código, ya sean scripts incrustados en HTML (inline) o desde un archivo externo, lo que causa que se definan funciones, variables y se ejecute aquel código que esté fuera de toda función.

¿Qué es una variable para el motor de JavaScript? ¿Cómo se procesa una asignación? ¿Qué es una función? ¿Cómo maneja la ejecución de una función? ¿Y si ésta llama a otra? ¿Cómo resuelve el motor una referencia a una variable de un bloque o una función externa?

Vamos a examinar el siguiente bloque de código:

const title = document.title;
function logTitle () {
    const n = 10;
    console.log(title);
    return n;
}
logTitle();

¡En estas 10 líneas de código están pasando muchas cosas! En las líneas 1 y 4 estamos declarando variables, en la 3 declaramos una función; estamos invocando a dicha función, llamando a una API (console.log) y devolviendo un número desde ella.

¿Cómo funciona todo esto? Es decir, ¿qué es declarar una variable, o una función? ¿en qué consiste realmente asignarle un valor? Pero antes de llegar a eso, algo más básico si cabe, ¿cómo llegamos desde nuestro código escrito en nuestro IDE favorito y almacenado en un archivo hasta ver el resultado funcionando en nuestro navegador?

Del IDE al navegador

JavaScript es un lenguaje de programación creado para dotar de interactividad a páginas web HTML durante los primeros años de la web. Se trata de un lenguaje monohilo: una pestaña web de tu navegador tendrá un único hilo (proceso ejecutándose en tu Sistema Operativo) encargándose de ejecutar el código JavaScript de dicha pestaña.

En cualquier momento, ese hilo puede estar ocupado encargándose de ejecutar alguna función o bloque de código, y puede haber eventos pendientes de ser procesados (como una petición al servidor que ha terminado, un setTimeout que debe ejecutarse, etc.). Ésta es una característica muy importante del lenguaje, porque define cómo se estructura el código que escribimos, simplifica el desarrollo e introduce nuevos problemas también.

JavaScript carece de mecanismos de sincronización como semáforos o mutex’es.

Cada pestaña tiene un solo hilo, pero otras APIs como ServiceWorker o WebWorkers funcionarán con su/s propio/s hilo/s.

Yung Beef

Como JavaScript es un lenguaje de programación, posee características que podemos encontrar en el idioma Castellano o Inglés:

  • Léxico: definen el alfabeto del lenguaje, es decir, qué símbolos o caracteres son válidos en el lenguaje, así como las categorías léxicas que existen en él (identificadores, palabras clave, operadores…).
  • Sintaxis o gramática: los usaremos indistintamente. Son las reglas que definen qué elementos del alfabeto del lenguaje pueden aparecer junto a qué otros, y en qué orden, para formar sentencias validas dentro del lenguaje.
  • Semántica: da significado a las sentencias y a los distintos elementos del alfabeto que aparecen en el contexto de una sentencia.

Así, tomando como referencia la siguiente línea de código:

const a = 2;

Pongamos que contamos con una sintaxis muy simple, con una regla que dice «una sentencia debe estar formada por const/var/let, seguido de un identificador, el carácter = y un número entero». Para dicha sintaxis, nuestra línea de código sería válida. No lo sería si cambiáramos, por ejemplo, «=» por «+=». Las sintaxis/gramáticas, son, obviamente, complejas: en este caso no hemos definido lo que es un identificador, ni hemos tratado los espacios, ni signos de punto y coma…

En cuanto a la semántica, como sabemos un poco de JavaScript, sabemos que const declara una constante, a es el nombre/identificador de la constante, el símbolo igual realiza una asignación y 2 es el valor que tendrá la constante. Además, sabemos que al escribir punto y coma cualquier carácter que aparezca a continuación pertenecerá a la siguiente sentencia.

Teniendo claros estos conceptos, vamos a empezar a entender cómo nuestro bonito código cobra vida.

Del código a la ejecución: el tokenizador/tokenizer

El input esencial del navegador, a la hora de procesar nuestros scripts, se puede considerar esencialmente una secuencia enorme de texto: nuestro código fuente. Pero el motor de JavaScript no entiende este texto, nuestro procesador tampoco. Tal y como está, como texto, no podemos ejecutarlo.

La primera fase de procesado del código se conoce como tokenización. En esta fase, un programa (analizador léxico, tokenizador o tokenizer) va transformando el texto de entrada en tokens, lexemas o partículas válidas para el lenguaje.

El tokenizer va leyendo carácter a carácter del código de entrada hasta que se encuentra caracteres especiales (un espacio, una coma, un punto y coma, etc.), y va «cortando» en estos puntos para generar tokens. Cuidado, los puntos y coma o las comas, además de servir para delimitar, también generan tokens del lenguaje.


(Una asignación sencilla transformada a Tokens)

En la imagen anterior, se puede ver cómo se podría transformar en tokens una asignación de una variable en JS. Nótese cómo el «;» es considerado un token de tipo separador. También que se puede guardar información de la línea y carácter/columna en que aparece el código (y podríamos guardar el archivo en el que aparece). De esta forma podríamos saber, cuando ocurra un error, en qué línea de código ha ocurrido – cada token generado en esa línea lleva consigo dicha información.

Lexer o scanner son otras formas de referirse a tokenizer. En el motor v8, por ejemplo, se denomina scanner.

El proceso de tokenización no es imprescindible. Podría hacerse un parser que no necesite un lexer, pero suele ser mucho menos eficiente.

En esta fase del proceso, además, se realizan las siguientes tareas:

  • Eliminar whitespaces o espacios en blanco (espacios, tabulaciones y saltos de línea – con atención a estos últimos, ya que pueden implicar el final de una instrucción y la inserción automática de un punto y coma).
  • Eliminar comentarios. Ya que no forman parte del código, no se convierten en tokens.
  • Lanzar un error (que se mostrará como SyntaxError) en el caso de que el input (nuestro código) contenga un carácter ilegal, no aceptado por el léxico del lenguaje (por ejemplo, const \t123 = 2 produciría tal error, ya que la secuencia \t123 no genera un lexema o token válido.

Del código a la ejecución: el analizador/parser

Llegados a este punto, hemos dividido el código fuente de entrada en trozos atómicos llamados tokens, deshaciéndonos de elementos superfluos para la ejecución (espacios, comentarios) y quedándonos con aquello que representa la lógica de nuestro código.

Sin embargo, esto sigue siendo insuficiente para ejecutar el programa. Por ejemplo, hemos analizado los lexemas para validar que formen parte del léxico del lenguaje, pero no hemos analizado la sintaxis (si analizamos const a b = 2 y lo convertimos en tokens, los tokens son válidos, pero sintácticamente la expresión no lo es).

Un nuevo programa o pieza de código se encargará de convertir secuencias de tokens en una estructura más útil para su procesamiento: el analizador o parser.

El parser se encargará de analizar los tokens resultantes de la etapa anterior, pero no individualmente sino en grupos, de acuerdo con la sintaxis del lenguaje. El parser sabrá, por ejemplo, para el caso de const a b = 2, que en una declaración, después de un identificador (a) no puede aparecer otro identificador de inmediato (b). Esto produciría un error de sintaxis (por ejemplo, Unexpected identifier).

Abstract Syntax Tree (AST)

El resultado del análisis del código es una estructura de datos conocida como árbol sintáctico abstracto o abstract syntax tree, en Inglés, generalmente abreviado con sus siglas AST.

Sin meternos en mucho detalle, este árbol ordenado contiene una representación de la estructura de nuestro código, generado a partir del listado de tokens anterior. Es decir, agrupa bloques de código (varios tokens) en ramas reflejando su funcionamiento y orden. Veamos esto con un ejemplo:

function greet () {
  var a = 2;
  return "Hello";
}
greet();


(AST simplificado generado a partir del código anterior)

Se ha simplificado el resultado para su representación. Si te interesa, en ASTExplorer puedes introducir código de ejemplo y ver el AST resultante sobre la marcha.

Como se puede observar, el parser ha interpretado los tokens que le ha proporcionado el tokenizer y ha aplicado la sintaxis de JS para construir un árbol sintáctico que representa lo que hace nuestro código.

Si volvemos a ver el árbol podemos darnos cuenta de un detalle: aquí ya no hay separadores, caracteres especiales u operadores. Nos quedamos sólo con la semántica del código, nos hemos deshecho de paréntesis, puntos y coma, etc. Por eso se llama árbol de sintaxis abstracto.

Si quisiéramos ejecutar el código mentalmente bastaría por empezar desde el nodo inicial (Program) y realizar una búsqueda en profundidad de izquierda a derecha (es decir, para cada nodo, cogemos los hijos del nodo, de izquierda a derecha, y los visitamos, realizando lo mismo recursivamente).

El orden de ejecución sería:

  • Program: punto de inicio
  • FunctionDeclaration: nodo que representa la declaración de la función greet.
  • BlockStatement: nodo que representa el código en el cuerpo o bloque principal {} de la función (así como scope, pero esto se verá en una entrada - posterior).
  • VariableDeclaration: nodo que declara una variable a, y su inicializador con valor 2.
  • ReturnStatement: último nodo de la función, que retorna el literal «Hello».
  • ExpressionStatement: invocación a la función greet vía greet().

Nuestros caracteres de código ya empiezan a formar algo más consistente que está mas cerca de poder ser ejecutado. Al menos, tenemos un árbol que define semánticamente y unívocamente el funcionamiento de nuestro código. Sin embargo, nuestra CPU no entiende ese código. En realidad, este código no irá directamente al procesador de nuestro ordenador. Aún nos faltan un par de piezas por explicar.

Del código a la ejecución: el intérprete y compilador

Si uno está atento a buscar las cosquillas a la entrada, podría preguntarse «¿y por qué hablamos del código que entiende la CPU, si interpretando ese árbol ya se podría ejecutar el código?» y sería una pregunta completamente válida. De hecho, así funcionaba JS en algunos navegadores en sus etapas iniciales.

Una de las maneras de agrupar lenguajes de programación es dependiendo de si son interpretados o compilados.

Los lenguajes compilados, como pueden ser C/C++, Rust o GO, transforman el código en uno o varios archivos, generalmente produciendo un ejecutable (un .exe en Windows, por ejemplo, o un archivo binario en Linux) que contiene instrucciones en código binario para la arquitectura de Sistema Operativo y procesador para los que se compiló (por ejemplo, Windows x64 o Linux x86). Esto permite optimizar mucho el código, conseguir un rendimiento excelente, pero requiere compilar el código de antemano para cada plataforma antes de ser distribuido, y es un proceso más lento y costoso.

Los lenguajes interpretados, como JavaScript, Java, Python o PHP, son lenguajes no pensados para ser compilados (transformados a código máquina), si no interpretados a partir del código fuente, por un intérprete (como el intérprete de JavaScript o de Python) que actúa a partir del código y controla la ejecución. Existen casos intermedios y/o excepciones como puede ser Java y su máquina virtual, o JS, como veremos a continuación.

Si bien hemos dicho que JS es interpretado y que con el AST podríamos ejecutar el código, y así se hacía, las cosas han cambiado mucho. JS ya no se usa solo para implementar algunos apaños sino de forma intensiva, ya sea como orquestador principal de toda la aplicación web o de cientos de formas distintas. Interpretar el mismo código una y otra vez, cuando una función puede ejecutarse decenas, cientos o miles de veces no es algo muy eficiente. Y a nadie le gusta acabar con el hilo de JS bloqueado haciendo cálculos.

Por esta razón, la mayoría de los navegadores actuales utilizan uno o varios compiladores de código para generar un código máquina intermedio llamado bytecode, que aunque no es código binario construido para tu procesador y SO, está en un nivel intermedio y es mucho más eficiente que interpretar JavaScript al vuelo.

Sin entrar en mucho detalle, el motivo por el que los navegadores pueden utilizar varios compiladores se debe a la optimización de código. Hoy en día el motor de JS es una parte crítica de la web, y por lo tanto su ejecución debe ser lo más rápida posible, pero también lo más temprana. Así, se suele utilizar un compilador base (baseline compiler), que realiza sólo las optimizaciones más sencillas y rápidas, para que la web empiece a ejecutarlo cuanto antes. Según las funciones son invocadas, y en función de múltiples parámetros como el número de veces que se invoca, con qué tipos, qué tipo de datos devuelve, etc., un segundo compilador (optimizing compiler) entra en acción generando bytecode aún más optimizado que el anterior. De igual manera, si alguna asunción hecha por el compilador no se ve satisfecha, es posible que el código optimizado se elimine y se vuelva a la versión base o interpretada (esto se conoce como bailout).

Resumen

Ahora sí. Nos ha costado, pero ya tenemos una visión general de cómo llega a ejecutarse el código a partir de los caracteres que escribimos, guardamos en archivos y cargamos en el navegador.

Todavía no sabemos cómo funciona eso de los eventos ni cómo se ejecuta realmente el código (¿dónde se guarda el valor de una variable?), pero ya tenemos una buena base sobre la cual ir asentando el resto de conceptos que nos faltan para entenderlo.

Si quieres leer más sobre estos temas, aquí van unos enlaces:
V8 Blog: Blazingly fast parsing, part 1: optimizing the scanner
V8 Blog: Blazingly fast parsing, part 2: lazy parsing
V8 Blog: https://v8.dev/blog/ignition-interpreter
A crash course in JIT compilers
Understanding JS's ByteCode
How JS works

22
EnderFX

Reserved - Pt. 2 (stack, heap, etc.).

1
EnderFX

... Continúa la parte 2, que no está escrita aún (Stack, heap, etc.).

Entornos léxicos y contextos de ejecución :ballot_box:

Vamos a ir entendiendo todo poco a poco, y para ello comenzaremos con una función muy sencilla:

spoiler

Este código define una función, sumTwo, y a continuación la invoca con un argumento (5). ¿Qué es lo que hace el motor cuando invocamos a la función?

El motor tiene que preparar un entorno adecuado para que se ejecute la lógica de la función, específico para dicha invocación. A este entorno lo llamaremos contexto de ejecución (execution context), porque es lo que da sentido a cada ejecución formal de la función, con un scope (this) y unos argumentos concretos.

Elementos que se pueden encontrar dentro de un contexto de ejecución:

Así, cuando invocamos a la función sumTwo, un nuevo contexto de ejecución es creado:

En realidad, la declaración y asignación de las variables se hace en dos pasos distintos, como se podrá ver más adelante siguiendo los enlaces propuestos para entender los entornos léxicos. Bastaría saber que n empezaría tienendo undefined como valor, posteriormente se inicializaría a 5, y después se ejecutaría el código, asignando el valor de two en la primera instrucción.

Con estos datos, el motor ya tiene suficiente información para poder ejecutar nuestra función. Tenemos una estructura de datos donde resolver los argumentos de la función, o buscar el valor de las variables que se utilicen en la misma.

Así que esto es el dichoso hoisting… :astonished:

Si has indagado un poco acerca de JS, puede que te hayas encontrado con algo llamado hoisting. Independientemente de en qué punto de una función se encuentre una declaración (var/let/const), siempre que no sea en otro bloque de código interno (lo veremos dentro de poco), el intérprete de JS moverá todas las declaraciones (no inicializaciones) al comienzo de la función.

De este modo, puedes referenciar a una variable que se defina en una línea posterior, y en lugar de un error de sintaxis el valor será simplemente undefined. Quizá ahora entiendas el por qué: en el contexto de ejecución creado al invocar la función ya existen esas variables, inicialmente con valor undefined.

¿Y qué es this? :rage:

Viendo el listado anterior, uno se puede preguntar… ¿cuál es el valor de this? Veamos, cuando se ejecuta la función, se crea el contexto de ejecución y se determina el valor de this para esa invocación concreta:

  • En el caso de una invocación mediante call, apply o bind, el valor de this es el primer argumento que se utilizó a llamar al método elegido.
  • Si en el momento de ejecución la función está contenida dentro de un objeto, dicho objeto. ¡OJO! Si defines la función dentro de un objeto, pero luego creas una variable global que la referencia, y la invocas, en el momento de ejecución ya no estará contenida en dicho objeto, por lo que habrá perdido el scope.
  • En caso contrario:
    • this apuntará al objeto global Window (en modo no estricto de JS).
    • this tendrá como valor undefined (en modo estricto de JS).

En cuanto al contexto de ejecución padre, también lo hemos pasado por alto, pero ya estamos preparados para volver a él. Hemos visto cómo se crea un contexto de ejecución cada vez que invocamos una función pero ¿qué ocurre con el código que ejecutamos en un script sin formar parte de una función? Pues bien, existe un contexto de ejecución especial.

El contexto de ejecución global :kaaba:

El contexto de ejecución global se creará al inicio de la ejecución del código (antes de ejecutar tu código) y estará siempre disponible. En este contexto especial se definen o mapean, por ejemplo, las APIs del navegador y objetos como window o document, de forma que el código que escribes puede acceder a ello.

La cadena de scopes / scope chain :chains:

Nos hemos desviado, a propósito, del asunto entre manos. ¿Cuál es el contexto de ejecución padre de nuestra función sumTwo? Pues el contexto de ejecución global. Todas las funciones tendrán un puntero a un contexto de ejecución padre: o bien otra función, si fueron definidas dentro de ella, o bien el contexto de ejecución global.

De esta forma, cuando en una función se intenta acceder o manipular una variable, y ésta no está definida en la propia función (es decir, en su contexto de ejecución) se va accediendo al contexto de ejecución padre de la función y, si la variable está definida en dicho contexto, se actuará sobre ella. En caso contrario, se seguirá recorriendo esta cadena de contextos de ejecución hasta llegar al global, que no tiene padre. En este momento, nuestro código fallará, al hacer referencia a una variable no definida ni en la función ni en ninguna función que la contenga.

Ahora ya sabemos cómo una función en JS puede tener acceso a una variable de otra función dentro de la cual fue definida.

¿Qué es una closure? :moyai:

Una técnica muy frecuentemente utilizada en JS se denomina closure, y nos permite capturar y ocultar el valor de una variable definida en una función padre, dentro de una función hija, e interactuar con la función hija.

spoiler

Si inspeccionamos este fragmento de código, vemos que la función counter inicializa una variable con valor 0 (value) y devuelve una nueva función que devuelve e incrementa su valor cada vez que se invoca. El consumidor de la función counter no sabe de la existencia de la variable value. Además, cada ejecución de counter creará su propia variable con valores independientes.

¿Qué está pasando? Pues ya no tiene ninguna magia: cuando se invoca a la función counter se crea un contexto de ejecución para ella, que contiene el valor de su propia variable llamada value. A continuación, cada vez que se invoca a la función devuelta por counter, se crea un nuevo contexto de ejecucion, cuyo contexto de ejecución padre es el que acabamos de ver que se ha creado para la invocación de counter.

Así, cuando en la función anidada se accede y modifica value, como esta variable no existe en su contexto de ejecución, se intenta encontrar en el contexto padre (el de counter), en el que sí existe, y por lo tanto se accede al valor almacenado. Decimos que la función retornada tiene memoria, porque recuerda los resultados de la ejecución anterior. En realidad, lo único que hace es modificar el contexto de ejecución de counter, que sobrevive entre llamadas a la función anidada.

Vamos a parar el carro… :octagonal_sign:

Por ahora considero que hemos bajado a suficiente profundidad para explicar el resto de puntos que más nos interesan.

Sin embargo, algunos de los conceptos anteriores no son 100% tal cual se han explicado. Por brevedad y simplicidad para entender lo que nos interesaba, hemos descrito un contexto de ejecución como un objeto con un par de campos y un puntero.

En realidad un contexto de ejecución contiene un entorno léxico (lexical environment) y/o un entorno de variables (variable environment), que a su vez es un entorno léxico, y son estos los que poseen punteros al entorno externo, referencia a this y un registro de entorno (environment record), que es el que contiene los valores de las variables de la función.

Ah, y no sólo existirán entornos léxicos para funciones, si no que bloques de código (como un bloque denotado por {}, como puede ser un if o un bucle for) también tendrán su propio entorno léxico. Esto explica por qué no puedes utilizar una variable de la rama principal de un if en su rama else, o por qué no puedes acceder a una variable declarada en un bucle for fuera del bucle.

Si quieres leer más sobre estos temas, aquí van unos enlaces:

Alrich

Reservado

1 respuesta
EnderFX

#4 hasta aquí había reservado, cabrón, jajajajajaj.

Voy a meterle ahora el contenido del hilo, dadme un par de min, pls xD

1 respuesta
Alrich

#5 Sí necesitas el hueco mandame mp y lo pego ;)

Zoko

Lo primero, gran hilo.

Yo no sé si os pasa a vosotros pero me cuesta más leer posts técnicos en español que en inglés, tengo que hacer como un esfuerzo extra porque realmente los términos en inglés no los traduzco cuando leo sobre ellos y tal.

1 respuesta
EnderFX

#7 Un poco, la verdad, porque acabas mezclando y en la cabeza tienes interiorizados los términos en inglés, como closure. Yo leo función flecha y me cuesta medio segundo traducirlo a arrow function, que es lo que busco y leo siempre. Mi idea es ver con WP cuánto de complicado es hacer el blog multiidioma, y dejarlo todo en inglés y español.

Pero creo que, aunque hay menos lectores en español, hay gente a la que le cuesta el inglés y en español es difícil encontrar posts un poco completos sobre según qué cosas.

Luego, creo que otra habilidad que voy a intentar aprender estos días es hacer el post más ameno, párrafos más pequeños, más visual o práctico. Lo que pasa es que este tema es bastante teórico, y a veces es difícil partirlo, sobre todo cuando quieres hacer hincapié en algo.

1
Amazon

Si pones el contenido aquí google lo detectará antes y posicionarás como blog que copia contenido

1 respuesta
EnderFX

#9 Pues mira, algo que no sabía y ya he aprendido. De todas formas, por ahora voy a dejarlo aquí con esta entrada, ya que iré modificando el borrador que yo tengo, y a ver si le meto caña. Thanks!

desu

Buena introduccion basica (pero tecnica noice) a js.

Me encantan estos temas asi que me quedo por aqui para aprender algo nuevo.

Un apunte que te hago es que para explicar el contexto o env las representaciones circulares con capas se entienden mucho mas (en mi opinion). Si te interesa te busco un ejemplo de algun libro. Muy buen aporte dicho esto de nuevo, solo era un apunte sin animo de desanimar/despreciar pero si quieres hacer presentaciones o algo con este material te diria que lo mires.

1 respuesta
EnderFX

#11 ¿A qué te refieres exactamente? A cómo "envuelven" unos contextos a otros con el puntero al contexto "padre"? Si es así, mi idea era representarlo como esos execution context que he puesto, con otros dentro, que a su vez tengan otros dentro. Pero estaba esperando a explicar el call stack para hacerlo.

Algo de este estilo (pero en lugar de código, con las formas que he puesto yo):

1 1 respuesta
desu

#12 Si algo parecido.. He buscado la fuente que tenia la cabeza ya por curiosidad y hacia la representacio para las excepciones pero me referia a algo similar a lo que has dicho. Parece que me he adelantado xd Pues nada me espero a los proximos capitulos.


pag 92
https://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.102.7366&rep=rep1&type=pdf

Ranthas

Muy interesante y muy bien explicado; me jode q tenga tan pocas manitas.

Aunque uso JS lo menos posible, no está demás mejorar el conocimiento base del mismo. Me ha gustado sobre todo la explicación sobre los closures, en mi vida podría haberlo expuesto de manera tan sencilla.

1
RedSpirit

Genial post, me ha gustado mucho. Además estos son los típicos temas "tricky" que a la gente se le atragantan en Javascript.

Si alguien tiene suscripción a Frontend Masters, los cursos de Will Sentance vienen muy bien para machacar los conceptos que expone el compañero aquí.

7 días después
EnderFX

Actualizo el hilo con cambios y nuevo contenido:

  • Lo que antes era el primer post pasará a ser el 3º.
  • Añadido un primer post sobre el proceso de interpretación del código (lenguaje, lexer, parser, intérprete/compiler - sin entrar en mucho detalle).
  • El segundo post irá principalmente sobre heap y stack.

No me he querido meter en diversos temas para no hacer un post increíblemente largo, como ya es... Temas como:

  • Lazy loading, preparsing, cómo saber qué código utilizar (detectar closures, por ejemplo). Esto además tendría que ser después de los 3 posts.
  • Gramáticas libres de contexto, citar a Chomsky. Hablar de lenguajes formales.
  • Ser muy detallado o específico con temas como los tokens o el AST. No creo que aporte nada entrar en ejemplos muy complejos o detallar exactamente todos los datos que tiene el AST: que una función no sea asíncrona nos da igual para el ejemplo.

tl;dr: Ahora la página 3 es la antigua página 1. La 1 es nueva.

4 1 respuesta
Ranthas

#16 Para hablar de temas que están fuertemente relacionados con el nuevo #1, creo que da incluso para nuevo hilo; intentar explicar teoría de autómatas, gramáticas, etc en un único post va a ser complicado, y es algo básico para poder luego profundizar en conceptos más avanzados como AST y los distintos tipos de parsers (LALR, LR(0), etc)

Aunque todo eso se puede resumir con el libro del dragón

1 respuesta
EnderFX

#17 Tienes toda la razón. Cuando termine de lanzar el blog (ya tengo el hosting y el dominio, el wp configurado, estoy con el tema y viendo qué plugins y cosillas me faltan, mirando algo de SEO también) esa es mi intención.

Las 3 primeras entradas serían una explicación por encima de todo, pero leyendo artículos (sobre todo los del blog de v8, que me flipan) da para escribir un artículo sobre autómatas, gramáticas y lenguajes, y el tokenizer; otro para hablar del parser, el AST y posiblemente las optimizaciones que se hacen, etc.

2 1 respuesta
HeXaN

#18 Menos mal que al menos alguien dice que va a hacer un blog y lo hace. Lo seguiré.

4

Usuarios habituales

  • EnderFX
  • Ranthas
  • RedSpirit
  • desu
  • Amazon
  • Zoko
  • Alrich