Eloquent JavaScript Small
Eloquent JavaScript Small
3ra edición
Marijn Haverbeke
Copyright © 2018 por Marijn Haverbeke
You can buy a print version of this book, with an extra bonus chapter in-
cluded, printed by No Starch Press at http://a-fwd.com/com=marijhaver-
20&asin-com=1593279507.
i
Contents
Introducción 1
Acerca de la programación . . . . . . . . . . . . . . . . . . . . . 3
Por qué el lenguaje importa . . . . . . . . . . . . . . . . . . . . 5
¿Qué es JavaScript? . . . . . . . . . . . . . . . . . . . . . . . . . 10
Código, y qué hacer con él . . . . . . . . . . . . . . . . . . . . . 12
Descripción general de este libro . . . . . . . . . . . . . . . . . . 13
Convenciones tipográficas . . . . . . . . . . . . . . . . . . . . . 15
ii
2 Estructura de Programa 35
Expresiones y declaraciones . . . . . . . . . . . . . . . . . . . . 35
Vinculaciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
Nombres vinculantes . . . . . . . . . . . . . . . . . . . . . . . . 40
El entorno . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
Funciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
La función console.log . . . . . . . . . . . . . . . . . . . . . . . . 42
Valores de retorno . . . . . . . . . . . . . . . . . . . . . . . . . . 43
Flujo de control . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
Ejecución condicional . . . . . . . . . . . . . . . . . . . . . . . . 45
Ciclos while y do . . . . . . . . . . . . . . . . . . . . . . . . . . 48
Indentando Código . . . . . . . . . . . . . . . . . . . . . . . . . 51
Ciclos for . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
Rompiendo un ciclo . . . . . . . . . . . . . . . . . . . . . . . . . 53
Actualizando vinculaciones de manera sucinta . . . . . . . . . . 55
Despachar en un valor con switch . . . . . . . . . . . . . . . . . 56
Capitalización . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
Comentarios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
Resumen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
3 Funciones 64
Definiendo una función . . . . . . . . . . . . . . . . . . . . . . . 65
Vinculaciones y alcances . . . . . . . . . . . . . . . . . . . . . . 67
Funciones como valores . . . . . . . . . . . . . . . . . . . . . . . 70
Notación de declaración . . . . . . . . . . . . . . . . . . . . . . 71
Funciones de flecha . . . . . . . . . . . . . . . . . . . . . . . . . 72
iii
La pila de llamadas . . . . . . . . . . . . . . . . . . . . . . . . . 73
Argumentos Opcionales . . . . . . . . . . . . . . . . . . . . . . . 75
Cierre . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
Recursión . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
Funciones crecientes . . . . . . . . . . . . . . . . . . . . . . . . . 85
Funciones y efectos secundarios . . . . . . . . . . . . . . . . . . 89
Resumen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
iv
Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130
v
Getters, setters y estáticos . . . . . . . . . . . . . . . . . . . . . 182
Herencia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 184
El operador instanceof . . . . . . . . . . . . . . . . . . . . . . . 187
Resumen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 187
Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 189
vi
Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 233
10 Módulos 274
Módulos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 275
Paquetes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 276
vii
Módulos improvisados . . . . . . . . . . . . . . . . . . . . . . . 278
Evaluando datos como código . . . . . . . . . . . . . . . . . . . 280
CommonJS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 281
Módulos ECMAScript . . . . . . . . . . . . . . . . . . . . . . . 285
Construyendo y empaquetando . . . . . . . . . . . . . . . . . . 287
Diseño de módulos . . . . . . . . . . . . . . . . . . . . . . . . . 289
Resumen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 292
Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 293
viii
12 Proyecto: Un Lenguaje de Programación 332
Análisis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 333
The evaluator . . . . . . . . . . . . . . . . . . . . . . . . . . . . 340
Special forms . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 342
The environment . . . . . . . . . . . . . . . . . . . . . . . . . . 345
Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 347
Compilation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 349
Cheating . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 350
Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 351
ix
14 Manejo de Eventos 386
Manejador de eventos . . . . . . . . . . . . . . . . . . . . . . . . 386
Eventos y nodos DOM . . . . . . . . . . . . . . . . . . . . . . . 388
Objetos de evento . . . . . . . . . . . . . . . . . . . . . . . . . . 389
Propagación . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 390
Acciones por defecto . . . . . . . . . . . . . . . . . . . . . . . . 393
Eventos de teclado . . . . . . . . . . . . . . . . . . . . . . . . . 394
Eventos de puntero . . . . . . . . . . . . . . . . . . . . . . . . . 397
Eventos de desplazamiento . . . . . . . . . . . . . . . . . . . . . 403
Eventos de foco . . . . . . . . . . . . . . . . . . . . . . . . . . . 405
Evento de carga . . . . . . . . . . . . . . . . . . . . . . . . . . . 406
Eventos y el ciclo de eventos . . . . . . . . . . . . . . . . . . . . 407
Temporizadores . . . . . . . . . . . . . . . . . . . . . . . . . . . 409
Antirrebote . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 410
Resumen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 412
Ejercicios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 413
x
Tracking keys . . . . . . . . . . . . . . . . . . . . . . . . . . . . 445
Running the game . . . . . . . . . . . . . . . . . . . . . . . . . . 446
Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 450
16 Node.js 452
Antecedentes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 453
El comando Node . . . . . . . . . . . . . . . . . . . . . . . . . . 454
Módulos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 456
Instalación con NPM . . . . . . . . . . . . . . . . . . . . . . . . 458
El módulo del sistema de archivos . . . . . . . . . . . . . . . . . 461
The HTTP module . . . . . . . . . . . . . . . . . . . . . . . . . 464
Streams . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 467
A file server . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 470
Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 478
Exercises . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 479
xi
Estructuras de Datos: Objetos y Arrays . . . . . . . . . . . . . 516
Funciones de Orden Superior . . . . . . . . . . . . . . . . . . . 520
La Vida Secreta de los Objetos . . . . . . . . . . . . . . . . . . 521
Proyecto: Un Robot . . . . . . . . . . . . . . . . . . . . . . . . . 523
Bugs y Errores . . . . . . . . . . . . . . . . . . . . . . . . . . . . 524
Expresiones Regulares . . . . . . . . . . . . . . . . . . . . . . . 525
Módulos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 526
Programación Asincrónica . . . . . . . . . . . . . . . . . . . . . 529
Proyecto: Un Lenguaje de Programación . . . . . . . . . . . . . 531
El Modelo de Objeto del Documento . . . . . . . . . . . . . . . 532
Manejo de Eventos . . . . . . . . . . . . . . . . . . . . . . . . . 534
Proyecto: Un Juego de Plataforma . . . . . . . . . . . . . . . . 536
Node.js . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 537
Proyecto: Sitio web para compartir habilidades . . . . . . . . . 539
xii
“Nosotros creemos que estamos creando el sistema para
nuestros propios propósitos. Creemos que lo estamos haciendo
a nuestra propia imagen... Pero la computadora no es
realmente como nosotros. Es una proyección de una parte
muy delgada de nosotros mismos: esa porción dedicada a la
lógica, el orden, la reglas y la claridad.”
—Ellen Ullman, Close to the Machine: Technophilia and its
Discontents
Introducción
Este es un libro acerca de instruir computadoras. Hoy en dia las com-
putadoras son tan comunes como los destornilladores (aunque bastante
más complejas que estos), y hacer que hagan exactamente lo que quieres
que hagan no siempre es fácil.
Si la tarea que tienes para tu computadora es común, y bien enten-
dida, tal y como mostrarte tu correo electrónico o funcionar como una
calculadora, puedes abrir la aplicación apropiada y ponerte a trabajar
en ella. Pero para realizar tareas únicas o abiertas, es posible que no
haya una aplicación disponible.
Ahí es donde la programación podría entrar en juego. La progra-
mación es el acto de construir un programa—un conjunto de instruc-
ciones precisas que le dicen a una computadora qué hacer. Porque las
computadoras son bestias tontas y pedantes, la programación es fun-
damentalmente tediosa y frustrante.
Afortunadamente, si puedes superar eso, y tal vez incluso disfrutar el
rigor de pensar en términos que las máquinas tontas puedan manejar,
la programación puede ser muy gratificante. Te permite hacer en se-
gundos cosas que tardarían siglos a mano. Es una forma de hacer que
1
tu herramienta computadora haga cosas que antes no podía. Ademas
proporciona de un maravilloso ejercicio en pensamiento abstracto.
La mayoría de la programación se realiza con lenguajes de progra-
mación. Un lenguaje de programación es un lenguaje artificialmente
construido que se utiliza para instruir ordenadores. Es interesante que
la forma más efectiva que hemos encontrado para comunicarnos con una
computadora es bastante parecida a la forma que usamos para comuni-
carnos entre nosotros. Al igual que los lenguajes humanos, los lenguajes
de computación permiten que las palabras y frases sean combinadas de
nuevas maneras, lo que nos permite expresar siempre nuevos conceptos.
Las interfaces basadas en lenguajes, que en un momento fueron la
principal forma de interactuar con las computadoras para la mayoría
de las personas, han sido en gran parte reemplazadas con interfaces más
simples y limitadas. Pero todavía están allí, si sabes dónde mirar.
En un punto, las interfaces basadas en lenguajes, como las terminales
BASIC y DOS de los 80 y 90, eran la principal forma de interactuar
con las computadoras. Estas han sido reemplazados en gran medida por
interfaces visuales, las cuales son más fáciles de aprender pero ofrecen
menos libertad. Los lenguajes de computadora todavía están allí, si
sabes dónde mirar. Uno de esos lenguajes, JavaScript, está integrado
en cada navegador web moderno y, por lo tanto, está disponible en casi
todos los dispositivos.
Este libro intentará familiarizarte lo suficiente con este lenguaje para
poder hacer cosas útiles y divertidas con él.
2
Acerca de la programación
Además de explicar JavaScript, también introduciré los principios bási-
cos de la programación. La programación, en realidad, es difícil. Las
reglas fundamentales son típicamente simples y claras, pero los progra-
mas construidos en base a estas reglas tienden a ser lo suficientemente
complejas como para introducir sus propias reglas y complejidad. De
alguna manera, estás construyendo tu propio laberinto, y es posible que
te pierdas en él.
Habrá momentos en los que leer este libro se sentirá terriblemente
frustrante. Si eres nuevo en la programación, habrá mucho material
nuevo para digerir. Gran parte de este material sera entonces combinado
en formas que requerirán que hagas conexiones adicionales.
Depende de ti hacer el esfuerzo necesario. Cuando estés luchando
para seguir el libro, no saltes a ninguna conclusión acerca de tus propias
capacidades. Estás bien, sólo tienes que seguir intentando. Tomate un
descanso, vuelve a leer algún material, y asegúrate de leer y compren-
der los programas de ejemplo y ejercicios. Aprender es un trabajo duro,
pero todo lo que aprendes se convertirá en tuyo, y hará que el apren-
dizaje subsiguiente sea más fácil.
3
lo que hace, son datos en la memoria de la computadora, y sin embargo
controla las acciones realizadas en esta misma memoria. Las analogías
que intentan comparar programas a objetos con los que estamos famil-
iarizados tienden a fallar. Una analogía que es superficialmente ade-
cuada es el de una máquina —muchas partes separadas tienden a estar
involucradas—, y para hacer que todo funcione, tenemos que considerar
la formas en las que estas partes se interconectan y contribuyen a la
operación de un todo.
Una computadora es una máquina física que actúa como un anfitrión
para estas máquinas inmateriales. Las computadoras en si mismas solo
pueden hacer cosas estúpidamente sencillas. La razón por la que son
tan útiles es que hacen estas cosas a una velocidad increíblemente alta.
Un programa puede ingeniosamente combinar una cantidad enorme de
estas acciones simples para realizar cosas bastante complicadas.
Un programa es un edificio de pensamiento. No cuesta nada con-
struirlo, no pesa nada, y crece fácilmente bajo el teclear de nuestras
manos.
Pero sin ningún cuidado, el tamaño de un programa y su comple-
jidad crecerán sin control, confundiendo incluso a la persona que lo
creó. Mantener programas bajo control es el problema principal de la
programación. Cuando un programa funciona, es hermoso. El arte de
la programación es la habilidad de controlar la complejidad. Un gran
programa es moderado, hecho simple en su complejidad.
Algunos programadores creen que esta complejidad se maneja mejor
mediante el uso de solo un pequeño conjunto de técnicas bien entendi-
das en sus programas. Ellos han compuesto reglas estrictas (“mejores
4
prácticas”) que prescriben la forma que los programas deberían tener,
y se mantienen cuidadosamente dentro de su pequeña y segura zona.
Esto no solamente es aburrido, sino que también es ineficaz. Prob-
lemas nuevos a menudo requieren soluciones nuevas. El campo de la
programación es joven y todavía se esta desarrollando rápidamente, y
es lo suficientemente variado como para tener espacio para aproxima-
ciones salvajemente diferentes. Hay muchos errores terribles que hacer
en el diseño de programas, así que ve adelante y comételos para que los
entiendas mejor. La idea de cómo se ve un buen programa se desarrolla
con la práctica, no se aprende de una lista de reglas.
5
e imprime el resultado: 1 + 2 + ... + 10 = 55. Podría ser ejecutado
en una simple máquina hipotética. Para programar las primeras com-
putadoras, era necesario colocar grandes arreglos de interruptores en
la posición correcta o perforar agujeros en tarjetas de cartón y dárse-
los a la computadora. Probablemente puedas imaginarte lo tedioso y
propenso a errores que era este procedimiento. Incluso escribir progra-
mas simples requería de mucha inteligencia y disciplina. Los complejos
eran casi inconcebibles.
Por supuesto, ingresar manualmente estos patrones arcanos de bits
(los unos y ceros) le dieron al programador un profundo sentido de ser
un poderoso mago. Y eso tiene que valer algo en términos de satisfacción
laboral.
{{index memoria, instrucción}}
Cada línea del programa anterior contiene una sola instrucción. Po-
dría ser escrito en español así:
6
6. Sumar el valor de la ubicación de memoria 1 a la ubicación de
memoria 0.
7. Sumar el número 1 al valor de la ubicación de memoria 1.
8. Continuar con la instrucción 3.
9. Imprimir el valor de la ubicación de memoria 0.
7
Las líneas usando comparar son probablemente las más extrañas. El
programa quiere ver si cuenta es igual a 11 para decidir si puede detener
su ejecución. Debido a que nuestra máquina hipotética es bastante
primitiva, esta solo puede probar si un número es cero y hace una
decisión (o salta) basándose en eso. Por lo tanto, usa la ubicación de
memoria etiquetada como comparar para calcular el valor de cuenta -
11 y toma una decisión basada en ese valor. Las siguientes dos líneas
agregan el valor de cuenta al resultado e incrementan cuenta en 1 cada
vez que el programa haya decidido que cuenta todavía no es 11.
Aquí está el mismo programa en JavaScript:
let total = 0, cuenta = 1;
while (cuenta <= 10) {
total += cuenta;
cuenta += 1;
}
console.log(total);
// → 55
8
poder de los lenguajes de programación es que se encargan por nosotros
de los detalles sin interés.
Al final del programa, después de que el while haya terminado, la
operación console.log se usa para mostrar el resultado.
{{index “sum function”, “range function”, abstracción, function}}
Finalmente, aquí está cómo se vería el programa si tuviéramos ac-
ceso a las las convenientes operaciones rango y suma disponibles, que
respectivamente crean una colección de números dentro de un rango y
calculan la suma de una colección de números:
console.log(suma(rango(1, 10)));
// → 55
9
¿Qué es JavaScript?
JavaScript se introdujo en 1995 como una forma de agregar progra-
mas a páginas web en el navegador Netscape Navigator. El lenguaje
ha sido desde entonces adoptado por todos los otros navegadores web
principales. Ha hecho que las aplicaciones web modernas sean posibles:
aplicaciones con las que puedes interactuar directamente, sin hacer una
recarga de página para cada acción. JavaScript también es utilizado
en sitios web más tradicionales para proporcionar diversas formas de
interactividad e ingenio.
Es importante tener en cuenta que JavaScript casi no tiene nada que
ver con el lenguaje de programación llamado Java. El nombre similar
fue inspirado por consideraciones de marketing, en lugar de buen juicio.
Cuando JavaScript estaba siendo introducido, el lenguaje Java estaba
siendo fuertemente comercializado y estaba ganando popularidad. Al-
guien pensó que era una buena idea intentar cabalgar sobre este éxito.
Ahora estamos atrapados con el nombre.
Después de su adopción fuera de Netscape, un documento estándar
fue escrito para describir la forma en que debería funcionar el lenguaje
JavaScript, para que las diversas piezas de software que decían ser
compatibles con JavaScript en realidad estuvieran hablando del mismo
lenguaje. Este se llamo el Estándar ECMAScript, por Ecma Interna-
tional que hizo la estandarización. En la práctica, los términos EC-
MAScript y JavaScript se puede usar indistintamente, son dos nombres
para el mismo lenguaje.
Hay quienes dirán cosas terribles sobre JavaScript. Muchas de es-
tas cosas son verdaderas. Cuando estaba comenzando a escribir algo
10
en JavaScript por primera vez, rápidamente comencé a despreciarlo.
El lenguaje aceptaba casi cualquier cosa que escribiera, pero la inter-
pretaba de una manera que era completamente diferente de lo que quería
decir. Por supuesto, esto tenía mucho que ver con el hecho de que no
tenía idea de lo que estaba haciendo, pero hay un problema real aquí:
JavaScript es ridículamente liberal en lo que permite. La idea detrás
de este diseño era que haría a la programación en JavaScript más fácil
para los principiantes. En realidad, lo que mas hace es que encontrar
problemas en tus programas sea más difícil porque el sistema no los
señalará por ti.
Sin embargo, esta flexibilidad también tiene sus ventajas. Deja es-
pacio para muchas técnicas que son imposibles en idiomas más rígidos,
y como verás (por ejemplo en el Capítulo 10) se pueden usar para su-
perar algunas de las deficiencias de JavaScript. Después de aprender
el idioma correctamente y luego de trabajar con él por un tiempo, he
aprendido a querer a JavaScript.
Ha habido varias versiones de JavaScript. ECMAScript versión 3
fue la versión mas ampliamente compatible en el momento del ascenso
de JavaScript a su dominio, aproximadamente entre 2000 y 2010. Du-
rante este tiempo, se trabajó en marcha hacia una ambiciosa versión 4,
que planeaba una serie de radicales mejoras y extensiones al lenguaje.
Cambiar un lenguaje vivo y ampliamente utilizado de una manera tan
radical resultó ser políticamente difícil, y el trabajo en la versión 4 fue
abandonado en 2008, lo que llevó a la versión 5, mucho menos ambi-
ciosa, que se publicaría en el 2009. Luego, en 2015, una actualización
importante, incluyendo algunas de las ideas planificadas para la versión
11
4, fue realizada. Desde entonces hemos tenido actualizaciones nuevas y
pequeñas cada año.
El hecho de que el lenguaje esté evolucionando significa que los nave-
gadores deben mantenerse constantemente al día, y si estás usando
uno más antiguo, puede que este no soporte todas las mejoras. Los
diseñadores de lenguajes tienen cuidado de no realizar cualquier cam-
bio que pueda romper los programas ya existentes, de manera que los
nuevos navegadores puedan todavía ejecutar programas viejos. En este
libro, usaré la versión 2017 de JavaScript.
Los navegadores web no son las únicas plataformas en las que se usa
JavaScript. Algunas bases de datos, como MongoDB y CouchDB, usan
JavaScript como su lenguaje de scripting y consultas. Varias platafor-
mas para programación de escritorio y servidores, más notablemente
el proyecto Node.js (el tema del Capítulo 20) proporcionan un entorno
para programar en JavaScript fuera del navegador.
12
Te recomiendo que pruebes tus soluciones a los ejercicios en un intér-
prete real de JavaScript. De esta forma, obtendrás retroalimentación
inmediata acerca de que si esta funcionando lo que estás haciendo, y,
espero, serás tentado a experimentar e ir más allá de los ejercicios.
La forma más fácil de ejecutar el código de ejemplo en el libro y ex-
perimentar con él, es buscarlo en la versión en línea del libro en eloquen-
tjavascript.net. Alli puedes hacer clic en cualquier ejemplo de código
para editar y ejecutarlo y ver el resultado que produce. Para trabajar
en los ejercicios, ve a eloquentjavascript.net/code, que proporciona el
código de inicio para cada ejercicio de programación y te permite ver
las soluciones.
Si deseas ejecutar los programas definidos en este libro fuera de la
caja de arena del libro, se requiere cierto cuidado. Muchos ejemplos se
mantienen por si mismos y deberían de funcionar en cualquier entorno
de JavaScript. Pero código en capítulos más avanzados a menudo se
escribe para un entorno específico (el navegador o Node.js) y solo puede
ser ejecutado allí. Además, muchos capítulos definen programas más
grandes, y las piezas de código que aparecen en ellos dependen de otras
piezas o de archivos externos. La caja de arena en el sitio web propor-
ciona enlaces a archivos Zip que contienen todos los scripts y archivos
de datos necesarios para ejecutar el código de un capítulo determinado.
13
son acerca de los navegadores web y la forma en la que JavaScript es
usado para programarlos. Finalmente, dos capítulos están dedicados a
Node.js, otro entorno en donde programar JavaScript.
A lo largo del libro, hay cinco capítulos de proyectos, que describen
programas de ejemplo más grandes para darte una idea de la progra-
mación real. En orden de aparición, trabajaremos en la construcción
de un robot de delivery, un lenguaje de programación, un juego de
plataforma, un programa de paint y un sitio web dinámico.
La parte del lenguaje del libro comienza con cuatro capítulos para
presentar la estructura básica del lenguaje de JavaScript. Estos intro-
ducen estructuras de control (como la palabra while que ya viste en
esta introducción), funciones (escribir tus propios bloques de construc-
ción), y estructuras de datos. Después de estos, seras capaz de escribir
programas simples. Luego, los Capítulos 5 y 6 introducen técnicas para
usar funciones y objetos y asi escribir código más abstracto y de manera
que puedas mantener la complejidad bajo control.
Después de un primer capítulo de proyecto, la primera parte del li-
bro continúa con los capítulos sobre manejo y solución de errores, en
expresiones regulares (una herramienta importante para trabajar con
texto), en modularidad (otra defensa contra la complejidad), y en pro-
gramación asincrónica (que se encarga de eventos que toman tiempo).
El segundo capítulo de proyecto concluye la primera parte del libro.
La segunda parte, Capítulos 13 a 19, describe las herramientas a las
que el JavaScript en un navegador tiene acceso. Aprenderás a mostrar
cosas en la pantalla (Capítulos 14 y 17), responder a entradas de usuario
(Capitulo 15), y a comunicarte a través de la red (Capitulo 18). Hay
14
dos capítulos de proyectos en este parte.
Después de eso, el Capítulo 20 describe Node.js, y el Capitulo 21
construye un pequeño sistema web usando esta herramienta.
Convenciones tipográficas
En este libro, el texto escrito en una fuente monoespaciada representará
elementos de programas, a veces son fragmentos autosuficientes, y a
veces solo se refieren a partes de un programa cercano. Los programas
(de los que ya has visto algunos), se escriben de la siguiente manera:
function factorial(numero) {
if (numero == 0) {
return 1;
} else {
return factorial(numero - 1) * numero;
}
}
¡Buena suerte!
15
“Debajo de la superficie de la máquina, el programa se mueve.
Sin esfuerzo, se expande y se contrae. En gran armonía, los
electrones se dispersan y se reagrupan. Las figuras en el
monitor son tan solo ondas sobre el agua. La esencia se
mantiene invisible debajo de la superficie.”
—Master Yuan-Ma, The Book of Programming
Chapter 1
Valores, Tipos, y Operadores
Dentro del mundo de la computadora, solo existen datos. Puedes leer
datos, modificar datos, crear nuevos datos—pero todo lo que no sean
datos, no puede ser mencionado. Toda estos datos están almacenados
como largas secuencias de bits, y por lo tanto, todos los datos son
fundamentalmente parecidos.
Los bits son cualquier tipo de cosa que pueda tener dos valores, usual-
mente descritos como ceros y unos. Dentro de la computadora, es-
tos toman formas tales como cargas eléctricas altas o bajas, una señal
fuerte o débil, o un punto brillante u opaco en la superficie de un CD.
Cualquier pedazo de información discreta puede ser reducida a una se-
cuencia de ceros y unos y, de esa manera ser representada en bits.
Por ejemplo, podemos expresar el numero 13 en bits. Funciona de
la misma manera que un número decimal, pero en vez de 10 diferentes
dígitos, solo tienes 2, y el peso de cada uno aumenta por un factor de 2
de derecha a izquierda. Aquí tenemos los bits que conforman el número
13, con el peso de cada dígito mostrado debajo de el:
0 0 0 0 1 1 0 1
16
128 64 32 16 8 4 2 1
Valores
Imagina un mar de bits—un océano de ellos. Una computadora mod-
erna promedio tiene mas de 30 billones de bits en su almacenamiento
de datos volátiles (memoria funcional). El almacenamiento no volátil
(disco duro o equivalente) tiende a tener unas cuantas mas ordenes de
magnitud.
Para poder trabajar con tales cantidades de bits sin perdernos, debe-
mos separarlos en porciones que representen pedazos de información.
En un entorno de JavaScript, esas porciones son llamadas valores. Aunque
todos los valores están hechos de bits, estos juegan papeles diferentes.
Cada valor tiene un tipo que determina su rol. Algunos valores son
números, otros son pedazos de texto, otros son funciones, y asi sucesi-
vamente.
Para crear un valor, solo debemos de invocar su nombre. Esto es
conveniente. No tenemos que recopilar materiales de construcción para
nuestros valores, o pagar por ellos. Solo llamamos su nombre, y woosh,
ahi lo tienes. Estos no son realmente creados de la nada, por supuesto.
Cada valor tiene que ser almacenado en algún sitio, y si quieres usar una
cantidad gigante de valores al mismo tiempo, puede que te quedes sin
memoria. Afortunadamente, esto solo es un problema si los necesitas
todos al mismo tiempo. Tan pronto como dejes de utilizar un valor,
17
este se disipará, dejando atrás sus bits para que estos sean reciclados
como material de construcción para la próxima generación de valores.
Este capitulo introduce los elementos atómicos de los programas en
JavaScript, estos son, los tipos de valores simples y los operadores que
actúan en tales valores.
Números
Valores del tipo number (número) son, como es de esperar, valores
numéricos. En un programa hecho en JavaScript, se escriben de la
siguiente manera:
13
18
la actualidad, y las personas tendían a utilizar grupos de 8 o 16 bits
para representar sus números. Era común accidentalmente desbordar
esta limitación— terminando con un número que no cupiera dentro de
la cantidad dada de bits. Hoy en día, incluso computadoras que caben
dentro de tu bolsillo poseen de bastante memoria, por lo que somos
libres de usar pedazos de memoria de 64 bits, y solamente nos tenemos
que preocupar por desbordamientos de memoria cuando lidiamos con
números verdaderamente astronómicos.
A pesar de esto, no todos los números enteros por debajo de 18 mil
trillones caben en un número de JavaScript. Esos bits también almace-
nan números negativos, por lo que un bit indica el signo de un número.
Un problema mayor es que los números no enteros tienen que ser rep-
resentados también. Para hacer esto, algunos de los bits son usados
para almacenar la posición del punto decimal. El número entero mas
grande que puede ser almacenado está en el rango de los 9 trillones (15
ceros)—lo cual es todavía placenteramente inmenso.
Los números fraccionarios se escriben usando un punto:
9.81
19
Eso es 2.998 × 108 = 299,800,000.
Los cálculos con números enteros (también llamados integers) mas
pequeños a los 9 trillones anteriormente mencionados están garanti-
zados a ser siempre precisos. Desafortunadamente, los calculos con
números fraccionarios, generalmente no lo son. Así como π (pi) no
puede ser precisamente expresado por un número finito de números
decimales, muchos números pierden algo de precisión cuando solo hay
64 bits disponibles para almacenarlos. Esto es una pena, pero solo
causa problemas prácticos en situaciones especificas. Lo importante es
que debemos ser consciente de estas limitaciones y tratar a los números
fraccionarios como aproximaciones, no como valores precisos.
Aritmética
Lo que mayormente se hace con los números es aritmética. Operaciones
aritméticas tales como la adición y la multiplicación, toman dos valores
numéricos y producen un nuevo valor a raíz de ellos. Asi es como lucen
en JavaScript:
100 + 4 * 11
20
quizás hayas podido adivinar, la multiplicación sucede primero. Pero
asi como en las matemáticas, puedes cambiar este orden envolviendo la
adición en paréntesis:
(100 + 4) * 11
21
Números especiales
Existen 3 valores especiales en JavaScript que son considerados números
pero que no se comportan como números normales.
Los primeros dos son Infinity y -Infinity, los cuales representan las
infinidades positivas y negativas. Infinity - 1 aun es Infinity, y asi
sucesivamente. A pesar de esto, no confíes mucho en computaciones que
dependan de infinidades. Estas no son matemáticamente confiables, y
puede que muy rápidamente nos resulten en el próximo número especial:
NaN.
NaN significa “no es un número” (“Not A Number”), aunque sea un
valor del tipo numérico. Obtendras este resultado cuando, por ejemplo,
trates de calcular 0 / 0 (cero dividido entre cero), Infinity - Infinity,
o cualquier otra cantidad de operaciones numéricas que no produzcan
un resultado significante.
Strings
El próximo tipo de dato básico es el string. Los Strings son usados para
representar texto. Son escritos encerrando su contenido en comillas:
`Debajo en el mar`
"Descansa en el océano"
'Flota en el océano'
22
final coincidan.
Casi todo puede ser colocado entre comillas, y JavaScript construirá
un valor string a partir de ello. Pero algunos caracteres son mas difíciles.
Te puedes imaginar que colocar comillas entre comillas podría ser difícil.
Los Newlines (los caracteres que obtienes cuando presionas la tecla de
Enter) solo pueden ser incluidos cuando el string está encapsulado con
comillas invertidas (\‘).
Para hacer posible incluir tales caracteres en un string, la siguiente
notación es utilizada: cuando una barra invertida (\) es encontrada
dentro de un texto entre comillas, indica que el carácter que le sigue
tiene un significado especial. Esto se conoce como escapar el carácter.
Una comilla que es precedida por una barra invertida no representará el
final del string sino que formara parte del mismo. Cuando el carácter n
es precedido por una barra invertida, este se interpreta como un Newline
(salto de linea). De la mima forma, t después de una barra invertida,
se interpreta como un character de tabulación. Toma como referencia
el siguiente string:
"Esta es la primera linea\nY esta es la segunda"
23
ter especial. Si dos barras invertidas prosiguen una a la otra, serán
colapsadas y sólo una permanecerá en el valor resultante del string. Asi
es como el string “Un carácter de salto de linea es escrito así: "\n".”
puede ser expresado:
Un carácter de salto de linea es escrito así: \"\\n\"."
También los strings deben de ser modelados como una serie de bits
para poder existir dentro del computador. La forma en la que JavaScript
hace esto es basada en el estándar Unicode. Este estándar asigna un
número a todo carácter que alguna vez pudieras necesitar, incluyendo
caracteres en Griego, Árabe, Japones, Armenio, y asi sucesivamente.
Si tenemos un número para representar cada carácter, un string puede
ser descrito como una secuencia de números.
Y eso es lo que hace JavaScript. Pero hay una complicación: La rep-
resentación de JavaScript usa 16 bits por cada elemento string, en el
cual caben 216 números diferentes. Pero Unicode define mas caracteres
que aquellos—aproximadamente el doble, en este momento. Entonces
algunos caracteres, como muchos emojis, necesitan ocupar dos “posi-
ciones de caracteres” en los strings de JavaScript. Volveremos a este
tema en el Capitulo 5.
Los strings no pueden ser divididos, multiplicados, o substraidos,
pero el operador + puede ser utilizado en ellos. No los agrega, sino que
los concatena—pega dos strings juntos. La siguiente línea producirá el
string "concatenar":
"con" + "cat" + "e" + "nar"
24
Los valores string tienen un conjunto de funciones (métodos) asoci-
adas, que pueden ser usadas para realizar operaciones en ellos. Regre-
saremos a estas en el Capítulo 4.
Los strings escritos con comillas simples o dobles se comportan casi
de la misma manera—La unica diferencia es el tipo de comilla que nece-
sitamos para escapar dentro de ellos. Los strings de comillas inversas,
usualmente llamados plantillas literales, pueden realizar algunos trucos
más. Mas alla de permitir saltos de lineas, pueden también incrustar
otros valores.
`la mitad de 100 es ${100 / 2}`
Operadores unarios
No todo los operadores son simbolos. Algunos se escriben como pal-
abras. Un ejemplo es el operador typeof, que produce un string con el
nombre del tipo de valor que le demos.
console.log(typeof 4.5)
// → number
console.log(typeof "x")
// → string
25
Usaremos console.log en los ejemplos de código para indicar que que
queremos ver el resultado de alguna evaluación. Mas acerca de esto
esto en el proximo capitulo.
En los otros operadores que hemos visto hasta ahora, todos operaban
en dos valores, pero typeof sola opera con un valor. Los operadores
que usan dos valores son llamados operadores binarios, mientras que
aquellos operadores que usan uno son llamados operadores unarios. El
operador menos puede ser usado tanto como un operador binario o como
un operador unario.
console.log(- (10 - 2))
// → -8
Valores Booleanos
Es frecuentemente util tener un valor que distingue entre solo dos posi-
bilidades, como “si”, y “no”, o “encendido” y “apagado”. Para este
propósito, JavaScript tiene el tipo Boolean, que tiene solo dos valores:
true (verdadero) y false (falso) que se escriben de la misma forma.
Comparación
Aquí se muestra una forma de producir valores Booleanos:
console.log(3 > 2)
26
// → true
console.log(3 < 2)
// → false
Los signos > y < son tradicionalmente símbolos para “mayor que” y
“menor que”, respectivamente. Ambos son operadores binarios. Apli-
carlos resulta en un valor Boolean que indica si la condición que indican
se cumple.
Los Strings pueden ser comparados de la misma forma.
console.log("Aardvark" < "Zoroaster")
// → true
27
Solo hay un valor en JavaScript que no es igual a si mismo, y este es
NaN (“no es un número”).
console.log(NaN == NaN)
// → false
Operadores lógicos
También existen algunas operaciones que pueden ser aplicadas a valores
Booleanos. JavaScript soporta tres operadores lógicos: and, or, y not.
Estos pueden ser usados para “razonar” acerca de valores Booleanos.
El operador && representa el operador lógico and. Es un operador
binario, y su resultado es verdadero solo si ambos de los valores dados
son verdaderos.
console.log(true && false)
// → false
console.log(true && true)
// → true
28
console.log(false || true)
// → true
console.log(false || false)
// → false
29
Este es llamado el operador condicional (o algunas veces simplemente
operador ternario ya que solo existe uno de este tipo). El valor a la
izquierda del signo de interrogación “decide” cual de los otros dos valores
sera retornado. Cuando es verdadero, elige el valor de en medio, y
cuando es falso, el valor de la derecha.
Valores vacíos
Existen dos valores especiales, escritos como null y undefined, que son
usados para denotar la ausencia de un valor significativo. Son en si
mismos valores, pero no traen consigo información.
Muchas operaciones en el lenguaje que no producen un valor significa-
tivo (veremos algunas mas adelante), producen undefined simplemente
porque tienen que producir algún valor.
La diferencia en significado entre undefined y null es un accidente del
diseño de JavaScript, y realmente no importa la mayor parte del tiempo.
En los casos donde realmente tendríamos que preocuparnos por estos
valores, mayormente recomiendo que los trates como intercambiables.
30
console.log(8 * null)
// → 0
console.log("5" - 1)
// → 4
console.log("5" + 1)
// → 51
console.log("cinco" * 2)
// → NaN
console.log(false == 0)
// → true
31
difieren, JavaScript utiliza una serie de reglas complicadas y confusas
para determinar que hacer. En la mayoria de los casos, solo tratara
de convertir uno de estos valores al tipo del otro valor. Sin embargo,
cuando null o undefined ocurren en cualquiera de los lados del oper-
ador, este produce verdadero solo si ambos lados son valores o null o
undefined.
console.log(null == undefined);
// → true
console.log(null == 0);
// → false
32
adas te estorben. Pero cuando estés seguro de que el tipo va a ser el
mismo en ambos lados, no es problemático utilizar los operadores mas
cortos.
33
Cuando el valor a su izquierda es algo que se convierte a falso, devuelve
ese valor, y de lo contrario, devuelve el valor a su derecha.
Otra propiedad importante de estos dos operadores es que la parte
de su derecha solo es evaluada si es necesario. En el caso de de true ||
X, no importa que sea X—aun si es una pieza del programa que hace
algo terrible—el resultado será verdadero, y X nunca sera evaluado. Lo
mismo sucede con false && X, que es falso e ignorará X. Esto es llamado
evaluación de corto circuito.
El operador condicional funciona de manera similar. Del segundo y
tercer valor, solo el que es seleccionado es evaluado.
Resumen
Observamos cuatro tipos de valores de JavaScript en este capítulo:
números, textos (strings), Booleanos, y valores indefinidos.
Tales valores son creados escribiendo su nombre (true, null) o valor
(13, "abc"). Puedes combinar y transformar valores con operadores. Vi-
mos operadores binarios para aritmética (+, -, *, /, y %), concatenación
de strings (+), comparaciones (==, !=, ===, !==, <, >, <=, >=), y lógica
(&&, ||), así también como varios otros operadores unarios (- para ne-
gar un número, ! para negar lógicamente, y typeof para saber el valor
de un tipo) y un operador ternario (?:) para elegir uno de dos valores
basándose en un tercer valor.
Esto te dá la información suficiente para usar JavaScript como una
calculadora de bolsillo, pero no para mucho más. El próximo capitulo
empezará a juntar estas expresiones para formar programas básicos.
34
“Y mi corazón brilla de un color rojo brillante bajo mi piel
transparente y translúcida, y tienen que administrarme 10cc
de JavaScript para conseguir que regrese. (respondo bien a las
toxinas en la sangre.) Hombre, esa cosa es increible!”
—_why, Why’s (Poignant) Guide to Ruby
Chapter 2
Estructura de Programa
En este capítulo, comenzaremos a hacer cosas que realmente se pueden
llamar programación. Expandiremos nuestro dominio del lenguaje JavaScri
más allá de los sustantivos y fragmentos de oraciones que hemos visto
hasta ahora, al punto donde podemos expresar prosa significativa.
Expresiones y declaraciones
En el Capítulo 1, creamos valores y les aplicamos operadores a ellos para
obtener nuevos valores. Crear valores de esta manera es la sustancia
principal de cualquier programa en JavaScript. Pero esa sustancia tiene
que enmarcarse en una estructura más grande para poder ser útil. Así
que eso es lo que veremos a continuación.
Un fragmento de código que produce un valor se llama una expresión.
Cada valor que se escribe literalmente (como 22 o "psicoanálisis") es
una expresión. Una expresión entre paréntesis también es una expre-
sión, como lo es un operador binario aplicado a dos expresiones o un
operador unario aplicado a una sola.
35
Esto demuestra parte de la belleza de una interfaz basada en un
lenguaje. Las expresiones pueden contener otras expresiones de una
manera muy similar a como las sub-oraciones en los lenguajes hu-
manos están anidadas, una sub-oración puede contener sus propias sub-
oraciones, y así sucesivamente. Esto nos permite construir expresiones
que describen cálculos arbitrariamente complejos.
Si una expresión corresponde al fragmento de una oración, una declaració
en JavaScript corresponde a una oración completa. Un programa es una
lista de declaraciones.
El tipo más simple de declaración es una expresión con un punto y
coma después ella. Esto es un programa:
1;
!false;
36
final de una declaración. En otros casos, tiene que estar allí, o la próx-
ima línea serán tratada como parte de la misma declaración. Las reglas
para saber cuando se puede omitir con seguridad son algo complejas y
propensas a errores. Asi que en este libro, cada declaración que nece-
site un punto y coma siempre tendra uno. Te recomiendo que hagas lo
mismo, al menos hasta que hayas aprendido más sobre las sutilezas de
los puntos y comas que puedan ser omitidos.
Vinculaciones
Cómo mantiene un programa un estado interno? Cómo recuerda cosas?
Hasta ahora hemos visto cómo producir nuevos valores a partir de val-
ores anteriores, pero esto no cambia los valores anteriores, y el nuevo
valor tiene que ser usado inmediatamente o se disipará nuevamente.
Para atrapar y mantener valores, JavaScript proporciona una cosa lla-
mada vinculación, o variable:
let atrapado = 5 * 5;
37
usarse como una expresión. El valor de tal expresión es el valor que la
vinculación mantiene actualmente. Aquí hay un ejemplo:
let diez = 10;
console.log(diez * diez);
// → 100
38
vuelta $35, le das a esta vinculación un nuevo valor:
let deudaLuigi = 140;
deudaLuigi = deudaLuigi - 35;
console.log(deudaLuigi);
// → 105
Las palabras var y const también pueden ser usadas para crear vin-
culaciones, en una manera similar a let.
var nombre = "Ayda";
const saludo = "Hola ";
console.log(saludo + nombre);
// → Hola Ayda
39
a la forma precisa en que difiere de let en el próximo capítulo. Por
ahora, recuerda que generalmente hace lo mismo, pero raramente la
usaremos en este libro porque tiene algunas propiedades confusas.
La palabra const representa una constante. Define una vinculación
constante, que apunta al mismo valor por el tiempo que viva. Esto
es útil para vinculaciones que le dan un nombre a un valor para que
fácilmente puedas consultarlo más adelante.
Nombres vinculantes
Los nombres de las vinculaciones pueden ser cualquier palabra. Los
dígitos pueden ser parte de los nombres de las vinculaciones pueden—
catch22 es un nombre válido, por ejemplo—pero el nombre no debe
comenzar con un dígito. El nombre de una vinculación puede incluir
signos de dólar ($) o caracteres de subrayado (_), pero no otros signos
de puntuación o caracteres especiales.
Las palabras con un significado especial, como let, son palabras
claves, y no pueden usarse como nombres vinculantes. También hay
una cantidad de palabras que están “reservadas para su uso” en futuras
versiones de JavaScript, que tampoco pueden ser usadas como nombres
vinculantes. La lista completa de palabras clave y palabras reservadas
es bastante larga:
break case catch class const continue debugger default
delete do else enum export extends false finally for
function if implements import interface in instanceof let
new package private protected public return static super
40
switch this throw true try typeof var void while with yield
El entorno
La colección de vinculaciones y sus valores que existen en un momento
dado se llama entorno. Cuando se inicia un programa, est entorno no
está vacío. Siempre contiene vinculaciones que son parte del estándar
del lenguaje, y la mayoría de las veces, también tiene vinculaciones que
proporcionan formas de interactuar con el sistema circundante. Por
ejemplo, en el navegador, hay funciones para interactuar con el sitio
web actualmente cargado y para leer entradas del mouse y teclado.
Funciones
Muchos de los valores proporcionados por el entorno predeterminado
tienen el tipo función. Una función es una pieza de programa envuelta
en un valor. Dichos valores pueden ser aplicados para ejecutar el pro-
grama envuelto. Por ejemplo, en un entorno navegador, la vinculación
prompt sostiene una función que muestra un pequeño cuadro de diálogo
preguntando por entrada del usuario. Esta se usa así:
prompt("Introducir contraseña");
41
Ejecutar una función tambien se conoce como invocarla, llamarla, o
aplicarla. Puedes llamar a una función poniendo paréntesis después de
una expresión que produzca un valor de función. Usualmente usarás
directamente el nombre de la vinculación que contenga la función. Los
valores entre los paréntesis se dan al programa dentro de la función. En
el ejemplo, la función prompt usa el string que le damos como el texto
a mostrar en el cuadro de diálogo. Los valores dados a las funciones se
llaman argumentos. Diferentes funciones pueden necesitar un número
diferente o diferentes tipos de argumentos
La función prompt no se usa mucho en la programación web moderna,
sobre todo porque no tienes control sobre la forma en como se ve la
caja de diálogo resultante, pero puede ser útil en programas de juguete
y experimentos.
La función console.log
En los ejemplos, utilicé console.log para dar salida a los valores. La
mayoría de los sistemas de JavaScript (incluidos todos los navegadores
42
web modernos y Node.js) proporcionan una función console.log que
escribe sus argumentos en algun dispositivo de salida de texto. En los
navegadores, esta salida aterriza en la consola de JavaScript. Esta parte
de la interfaz del navegador está oculta por defecto, pero la mayoría de
los navegadores la abren cuando presionas F12 o, en Mac, Command-
Option-I. Si eso no funciona, busca en los menús un elemento llamado
“herramientas de desarrollador” o algo similar.
Aunque los nombres de las vinculaciones no puedan contener carác-
teres de puntos, console.log tiene uno. Esto es porque console.log
no es un vinculación simple. En realidad, es una expresión que ob-
tiene la propiedad log del valor mantenido por la vinculación console.
Averiguaremos qué significa esto exactamente en el Capítulo 4.
Valores de retorno
Mostrar un cuadro de diálogo o escribir texto en la pantalla es un efecto
secundario. Muchas funciones son útiles debido a los efectos secundarios
que ellas producen. Las funciones también pueden producir valores, en
cuyo caso no necesitan tener un efecto secundario para ser útil. Por
ejemplo, la función Math.max toma cualquier cantidad de argumentos
numéricos y devuelve el mayor de ellos.
console.log(Math.max(2, 4));
// → 4
Cuando una función produce un valor, se dice que retorna ese valor.
43
Todo lo que produce un valor es una expresión en JavaScript, lo que
significa que las llamadas a funciones se pueden usar dentro de expre-
siones más grandes. aquí una llamada a Math.min, que es lo opuesto a
Math.max, se usa como parte de una expresión de adición:
console.log(Math.min(2, 4) + 100);
// → 102
Flujo de control
Cuando tu programa contiene más de una declaración, las declaraciones
se ejecutan como si fueran una historia, de arriba a abajo. Este pro-
grama de ejemplo tiene dos declaraciones. La primera le pide al usuario
un número, y la segunda, que se ejecuta después de la primera, muestra
el cuadrado de ese número.
let elNumero = Number(prompt("Elige un numero"));
console.log("Tu número es la raiz cuadrada de " +
elNumero * elNumero);
44
Aquí está la representación esquemática (bastante trivial) de un flujo
de control en línea recta:
Ejecución condicional
No todos los programas son caminos rectos. Podemos, por ejemplo,
querer crear un camino de ramificación, donde el programa toma la
rama adecuada basadandose en la situación en cuestión. Esto se llama
ejecución condicional.
45
ninguna salida.
La palabra clave if ejecuta u omite una declaración dependiendo
del valor de una expresión booleana. La expresión decisiva se escribe
después de la palabra clave, entre paréntesis, seguida de la declaración
a ejecutar.
La función Number.isNaN es una función estándar de JavaScript que
retorna true solo si el argumento que se le da es NaN. Resulta que la
función Number devuelve NaN cuando le pasas un string que no representa
un número válido. Por lo tanto, la condición se traduce a “a menos que
elNumero no sea un número, haz esto”.
La declaración debajo del if está envuelta en llaves ({y }) en este
ejemplo. Estos pueden usarse para agrupar cualquier cantidad de declara-
ciones en una sola declaración, llamada un bloque. Podrías también
haberlas omitido en este caso, ya que solo tienes una sola declaración,
pero para evitar tener que pensar si se necesitan o no, la mayoría de los
programadores en JavaScript las usan en cada una de sus declaraciones
envueltas como esta. Seguiremos esta convención en la mayoria de este
libro, a excepción de la ocasional declaración de una sola linea.
if (1 + 1 == 2) console.log("Es verdad");
// → Es verdad
46
let elNumero = Number(prompt("Elige un numero"));
if (!Number.isNaN(elNumero)) {
console.log("Tu número es la raiz cuadrada de " +
elNumero * elNumero);
} else {
console.log("Ey. Por qué no me diste un número?");
}
47
Ciclos while y do
Considera un programa que muestra todos los números pares de 0 a 12.
Una forma de escribir esto es la siguiente:
console.log(0);
console.log(2);
console.log(4);
console.log(6);
console.log(8);
console.log(10);
console.log(12);
48
El flujo de control de ciclos nos permite regresar a algún punto del
programa en donde estábamos antes y repetirlo con nuestro estado del
programa actual. Si combinamos esto con una vinculación que cuenta,
podemos hacer algo como esta:
let numero = 0;
while (numero <= 12) {
console.log(numero);
numero = numero + 2;
}
// → 0
// → 2
// … etcetera
49
resultado y una para contar cuántas veces hemos multiplicado este re-
sultado por 2. El ciclo prueba si la segunda vinculación ha llegado a 10
todavía y, si no, actualiza ambas vinculaciones.
let resultado = 1;
let contador = 0;
while (contador < 10) {
resultado = resultado * 2;
contador = contador + 1;
}
console.log(resultado);
// → 1024
50
Este programa te obligará a ingresar un nombre. Preguntará de nuevo
y de nuevo hasta que obtenga algo que no sea un string vacío. Aplicar
el operador ! convertirá un valor a tipo Booleano antes de negarlo y
todos los strings, excepto "" seran convertidas a true. Esto significa
que el ciclo continúa dando vueltas hasta que proporciones un nombre
no-vacío.
Indentando Código
En los ejemplos, he estado agregando espacios adelante de declaraciones
que son parte de una declaración más grande. Estos no son necesarios—
la computadora aceptará el programa normalmente sin ellos. De hecho,
incluso las nuevas líneas en los programas son opcionales. Podrías es-
cribir un programa en una sola línea inmensa si asi quisieras.
El rol de esta indentación dentro de los bloques es hacer que la estruc-
tura del código se destaque. En código donde se abren nuevos bloques
dentro de otros bloques, puede ser difícil ver dónde termina un bloque
y donde comienza el otro. Con la indentación apropiada, la forma vi-
sual de un programa corresponde a la forma de los bloques dentro de
él. Me gusta usar dos espacios para cada bloque abierto, pero los gus-
tos varían—algunas personas usan cuatro espacios, y algunas personas
usan carácteres de tabulación. Lo cosa importante es que cada bloque
nuevo agregue la misma cantidad de espacio.
if (false != true) {
console.log("Esto tiene sentido.");
if (1 < 2) {
51
console.log("Ninguna sorpresa alli.");
}
}
Ciclos for
Muchos ciclos siguen el patrón visto en los ejemplos de while. Primero
una vinculación “contador” se crea para seguir el progreso del ciclo. En-
tonces viene un ciclo while, generalmente con una expresión de prueba
que verifica si el contador ha alcanzado su valor final. Al final del cuerpo
del ciclo, el el contador se actualiza para mantener un seguimiento del
progreso.
Debido a que este patrón es muy común, JavaScript y otros lenguajes
similares proporcionan una forma un poco más corta y más completa,
el ciclo for:
for (let numero = 0; numero <= 12; numero = numero + 2) {
console.log(numero);
}
// → 0
// → 2
// … etcetera
52
Este programa es exactamente equivalente al ejemplo anterior de im-
presión de números pares. El único cambio es que todos las declaraciónes
que están relacionadas con el “estado” del ciclo estan agrupadas después
del for.
Los paréntesis después de una palabra clave for deben contener dos
punto y comas. La parte antes del primer punto y coma inicializa el
cicloe, generalmente definiendo una vinculación. La segunda parte es la
expresión que chequea si el ciclo debe continuar. La parte final actualiza
el estado del ciclo después de cada iteración. En la mayoría de los casos,
esto es más corto y conciso que un constructo while.
Este es el código que calcula 210 , usando for en lugar de while:
let resultado = 1;
for (let contador = 0; contador < 10; contador = contador + 1) {
resultado = resultado * 2;
}
console.log(resultado);
// → 1024
Rompiendo un ciclo
Hacer que la condición del ciclo produzca false no es la única forma
en que el ciclo puede terminar. Hay una declaración especial llamada
break (“romper”) que tiene el efecto de inmediatamente saltar afuera
del ciclo circundante.
Este programa ilustra la declaración break. Encuentra el primer
53
número que es a la vez mayor o igual a 20 y divisible por 7.
for (let actual = 20; ; actual = actual + 1) {
if (actual % 7 == 0) {
console.log(actual);
break;
}
}
// → 21
54
Actualizando vinculaciones de manera
sucinta
Especialmente cuando realices un ciclo, un programa a menudo necesita
“actualizar” una vinculación para mantener un valor basadandose en el
valor anterior de esa vinculación.
contador = contador + 1;
55
Despachar en un valor con switch
No es poco común que el código se vea así:
if (x == "valor1") accion1();
else if (x == "valor2") accion2();
else if (x == "valor3") accion3();
else accionPorDefault();
56
Puedes poner cualquier número de etiquetas de case dentro del bloque
abierto por switch. El programa comenzará a ejecutarse en la etiqueta
que corresponde al valor que se le dio a switch, o en default si no se
encuentra ningún valor que coincida. Continuará ejecutándose, incluso
a través de otras etiquetas, hasta que llegue a una declaración break.
En algunos casos, como en el caso "soleado" del ejemplo, esto se puede
usar para compartir algo de código entre casos (recomienda salir para
ambos climas soleado y nublado). Pero ten cuidado—es fácil olvidarse
de break, lo que hará que el programa ejecute código que no quieres que
sea ejecutado.
Capitalización
Los nombres de vinculaciones no pueden contener espacios, sin embargo,
a menudo es útil usar múltiples palabras para describir claramente lo
que representa la vinculación. Estas son más o menos tus opciones para
escribir el nombre de una vinculación con varias palabras en ella:
pequeñatortugaverde
pequeña_tortuga_verde
PequeñaTortugaVerde
pequeñaTortugaVerde
57
programadores de JavaScript, siguen el estilo de abajo: capitalizan cada
palabra excepto la primera. No es difícil acostumbrarse a pequeñas
cosas así, y programar con estilos de nombres mixtos pueden ser algo
discordante para leer, así que seguiremos esta convención.
En algunos casos, como en la función Number, la primera letra de la
vinculación también está en mayúscula. Esto se hizo para marcar esta
función como un constructor. Lo que es un constructor quedará claro
en el Capítulo 6. Por ahora, lo importante es no ser molestado por esta
aparente falta de consistencia.
Comentarios
A menudo, el código en si mismo no transmite toda la información que
deseas que un programa transmita a los lectores humanos, o lo trans-
mite de una manera tan críptica que la gente quizás no lo entienda. En
otras ocasiones, podrías simplemente querer incluir algunos pensamien-
tos relacionados como parte de tu programa. Esto es para lo qué son
los comentarios.
Un comentario es una pieza de texto que es parte de un programa
pero que es completamente ignorado por la computadora. JavaScript
tiene dos formas de escribir comentarios. Para escribir un comentario
de una sola línea, puede usar dos caracteres de barras inclinadas (//) y
luego el texto del comentario después.
let balanceDeCuenta = calcularBalance(cuenta);
// Es un claro del bosque donde canta un río
balanceDeCuenta.ajustar();
58
// Cuelgan enloquecidamente de las hierbas harapos de plata
let reporte = new Reporte();
// Donde el sol de la orgullosa montaña luce:
añadirAReporte(balanceDeCuenta, reporte);
// Un pequeño valle espumoso de luz.
Resumen
Ahora sabes que un programa está construido a partir de declaraciones,
las cuales a veces pueden contener más declaraciones. Las declaraciones
tienden a contener expresiones, que a su vez se pueden construir a partir
59
de expresiones mas pequeñas.
Poner declaraciones una despues de otras te da un programa que es
ejecutado de arriba hacia abajo. Puedes introducir alteraciones en el
flujo de control usando declaraciones condicionales (if, else, y switch)
y ciclos (while, do, y for).
Las vinculaciones se pueden usar para archivar datos bajo un nombre,
y son utiles para el seguimiento de estado en tu programa. El entorno es
el conjunto de vinculaciones que se definen. Los sistemas de JavaScript
siempre incluyen por defecto un número de vinculaciones estándar útiles
en tu entorno.
Las funciones son valores especiales que encapsulan una parte del
programa. Puedes invocarlas escribiendo nombreDeLaFuncion(argumento1
, argumento2). Tal llamada a función es una expresión, y puede producir
un valor.
Ejercicios
Si no estas seguro de cómo probar tus soluciones para los ejercicios,
consulta la introducción.
Cada ejercicio comienza con una descripción del problema. Lee eso
y trata de resolver el ejercicio. Si tienes problemas, considera leer las
pistas en el final del libro. Las soluciones completas para los ejercicios
no estan incluidas en este libro, pero puedes encontrarlas en línea en
eloquentjavascript.net/code. Si quieres aprender algo de los ejercicios,
te recomiendo mirar a las soluciones solo despues de que hayas resuelto
el ejercicio, o al menos despues de que lo hayas intentando resolver por
60
un largo tiempo y tengas un ligero dolor de cabeza.
Ciclo de un triángulo
Escriba un ciclo que haga siete llamadas a console.log para generar el
siguiente triángulo:
#
##
###
####
#####
######
#######
FizzBuzz
Escribe un programa que use console.log para imprimir todos los números
de 1 a 100, con dos excepciones. Para números divisibles por 3, imprime
61
"Fizz" en lugar del número, y para los números divisibles por 5 (y no
3), imprime "Buzz" en su lugar.
Cuando tengas eso funcionando, modifica tu programa para imprimir
"FizzBuzz", para números que sean divisibles entre 3 y 5 (y aún imprimir
"Fizz" o "Buzz" para números divisibles por solo uno de ellos).
(Esta es en realidad una pregunta de entrevista que se ha dicho elim-
ina un porcentaje significativo de candidatos a programadores. Así que
si la puedes resolver, tu valor en el mercado laboral acaba de subir).
Tablero de ajedrez
Escribe un programa que cree un string que represente una cuadrícula
de 8 × 8, usando caracteres de nueva línea para separar las líneas. En
cada posición de la cuadrícula hay un espacio o un carácter "#". Los
caracteres deberían de formar un tablero de ajedrez.
Pasar este string a console.log debería mostrar algo como esto:
# # # #
# # # #
# # # #
# # # #
# # # #
# # # #
# # # #
# # # #
Cuando tengas un programa que genere este patrón, define una vincu-
lación tamaño = 8 y cambia el programa para que funcione con cualquier
62
tamaño, dando como salida una cuadrícula con el alto y ancho dados.
63
“La gente piensa que las ciencias de la computación son el
arte de los genios, pero la verdadera realidad es lo opuesto,
estas solo consisten en mucha gente haciendo cosas que se
construyen una sobre la otra, al igual que un muro hecho de
piedras pequeñas.”
—Donald Knuth
Chapter 3
Funciones
Las funciones son el pan y la mantequilla de la programación en JavaScript.
El concepto de envolver una pieza de programa en un valor tiene muchos
usos. Esto nos da una forma de estructurar programas más grandes, de
reducir la repetición, de asociar nombres con subprogramas y de aislar
estos subprogramas unos con otros.
La aplicación más obvia de las funciones es definir nuevo vocabulario.
Crear nuevas palabras en la prosa suele ser un mal estilo. Pero en la
programación, es indispensable.
En promedio, un tipico adulto que hable español tiene unas 20,000
palabras en su vocabulario. Pocos lenguajes de programación vienen
con 20,000 comandos ya incorporados en el. Y el vocabulario que está
disponible tiende a ser más precisamente definido, y por lo tanto menos
flexible, que en el lenguaje humano. Por lo tanto, nosotros por lo gen-
eral tenemos que introducir nuevos conceptos para evitar repetirnos
demasiado.
64
Definiendo una función
Una definición de función es una vinculación regular donde el valor de
la vinculación es una función. Por ejemplo, este código define cuadrado
para referirse a una función que produce el cuadrado de un número
dado:
const cuadrado = function(x) {
return x * x;
};
console.log(cuadrado(12));
// → 144
Una función es creada con una expresión que comienza con la pal-
abra clave function (“función”). Las funciones tienen un conjunto de
parámetros (en este caso, solo x) y un cuerpo, que contiene las declara-
ciones que deben ser ejecutadas cuando se llame a la función. El cuerpo
de la función de una función creada de esta manera siempre debe estar
envuelto en llaves, incluso cuando consista en una sola declaración.
Una función puede tener múltiples parámetros o ningún parámetro en
absoluto. En el siguiente ejemplo, hacerSonido no lista ningún nombre
de parámetro, mientras que potencia enumera dos:
const hacerSonido = function() {
console.log("Pling!");
};
65
hacerSonido();
// → Pling!
console.log(potencia(2, 10));
// → 1024
66
Vinculaciones y alcances
Cada vinculación tiene un alcace, que correspone a la parte del pro-
grama en donde la vinculación es visible. Para vinculaciones definidas
fuera de cualquier función o bloque, el alcance es todo el programa—
puedes referir a estas vinculaciones en donde sea que quieras. Estas son
llamadas globales.
Pero las vinculaciones creadas como parámetros de función o declaradas
dentro de una función solo puede ser referenciadas en esa función. Estas
se llaman locales. Cada vez que se llame a la función, se crean nuevas
instancias de estas vinculaciones. Esto proporciona cierto aislamiento
entre funciones—cada llamada de función actúa sobre su pequeño pro-
pio mundo (su entorno local), y a menudo puede ser entendida sin saber
mucho acerca de lo qué está pasando en el entorno global.
Vinculaciones declaradas con let y const son, de hecho, locales al
bloque donde esten declarados, así que si creas uno de esas dentro de
un ciclo, el código antes y después del ciclo no puede “verlas”. En
JavaScript anterior a 2015, solo las funciones creaban nuevos alcances,
por lo que las vinculaciones de estilo-antiguo, creadas con la palabra
clave var, son visibles a lo largo de toda la función en la que aparecen—
o en todo el alcance global, si no están dentro de una función.
let x = 10;
if (true) {
let y = 20;
var z = 30;
console.log(x + y + z);
// → 60
67
}
// y no es visible desde aqui
console.log(x + z);
// → 40
Cada alcance puede “mirar afuera” hacia al alcance que lo rodee, por
lo que x es visible dentro del bloque en el ejemplo. La excepción es
cuando vinculaciones múltiples tienen el mismo nombre—en ese caso,
el código solo puede ver a la vinculación más interna. Por ejemplo,
cuando el código dentro de la función dividirEnDos se refiera a numero,
estara viendo su propio numero, no el numero en el alcance global.
const dividirEnDos = function(numero) {
return numero / 2;
};
Alcance anidado
JavaScript no solo distingue entre vinculaciones globales y locales. Blo-
ques y funciones pueden ser creados dentro de otros bloques y funciones,
produciendo múltiples grados de localidad.
68
Por ejemplo, esta función—que muestra los ingredientes necesarios
para hacer un lote de humus—tiene otra función dentro de ella:
const humus = function(factor) {
const ingrediente = function(cantidad, unidad, nombre) {
let cantidadIngrediente = cantidad * factor;
if (cantidadIngrediente > 1) {
unidad += "s";
}
console.log(`${cantidadIngrediente} ${unidad} ${nombre}`);
};
ingrediente(1, "lata", "garbanzos");
ingrediente(0.25, "taza", "tahini");
ingrediente(0.25, "taza", "jugo de limón");
ingrediente(1, "clavo", "ajo");
ingrediente(2, "cucharada", "aceite de oliva");
ingrediente(0.5, "cucharadita", "comino");
};
69
alcance léxico.
70
Notación de declaración
Hay una forma ligeramente más corta de crear una vinculación de fun-
ción. Cuando la palabra clave function es usada al comienzo de una
declaración, funciona de una manera diferente.
function cuadrado(x) {
return x * x;
}
function futuro() {
return "Nunca tendran autos voladores";
}
71
sin preocuparnos por tener que definir todas las funciones antes de que
sean utilizadas.
Funciones de flecha
Existe una tercera notación para funciones, que se ve muy diferente de
las otras. En lugar de la palabra clave function, usa una flecha (=>)
compuesta de los caracteres igual y mayor que (no debe ser confundida
con el operador igual o mayor que, que se escribe >=).
const potencia = (base, exponente) => {
let resultado = 1;
for (let cuenta = 0; cuenta < exponente; cuenta++) {
resultado *= base;
}
return resultado;
};
72
const cuadrado1 = (x) => { return x * x; };
const cuadrado2 = x => x * x;
No hay una buena razón para tener ambas funciones de flecha y ex-
presiones function en el lenguaje. Aparte de un detalle menor, que
discutiremos en Capítulo 6, estas hacen lo mismo. Las funciones de
flecha se agregaron en 2015, principalmente para que fuera posible es-
cribir pequeñas expresiones de funciones de una manera menos verbosa.
Las usaremos mucho en el Capitulo 5.
La pila de llamadas
La forma en que el control fluye a través de las funciones es algo com-
plicado. Vamos a écharle un vistazo más de cerca. Aquí hay un simple
programa que hace unas cuantas llamadas de función:
function saludar(quien) {
console.log("Hola " + quien);
}
saludar("Harry");
73
console.log("Adios");
Ya que una función tiene que regresar al lugar donde fue llamada
cuando esta retorna, la computadora debe recordar el contexto de donde
sucedió la llamada. En un caso, console.log tiene que volver a la función
saludar cuando está lista. En el otro caso, vuelve al final del programa.
El lugar donde la computadora almacena este contexto es la pila de
llamadas. Cada vez que se llama a una función, el contexto actual es
74
almacenado en la parte superior de esta “pila”. Cuando una función
retorna, elimina el contexto superior de la pila y lo usa para continuar
la ejecución.
Almacenar esta pila requiere espacio en la memoria de la computa-
dora. Cuando la pila crece demasiado grande, la computadora fallará
con un mensaje como “fuera de espacio de pila” o “demasiada recursivi-
dad”. El siguiente código ilustra esto haciendo una pregunta realmente
difícil a la computadora, que causara un ir y venir infinito entre las
dos funciones. Mejor dicho, sería infinito, si la computadora tuviera
una pila infinita. Como son las cosas, nos quedaremos sin espacio, o
“explotaremos la pila”.
function gallina() {
return huevo();
}
function huevo() {
return gallina();
}
console.log(gallina() + " vino primero.");
// → ??
Argumentos Opcionales
El siguiente código está permitido y se ejecuta sin ningún problema:
function cuadrado(x) { return x * x; }
console.log(cuadrado(4, true, "erizo"));
75
// → 16
console.log(menos(10));
// → -10
console.log(menos(10, 5));
// → 5
76
Si escribes un operador = después un parámetro, seguido de una ex-
presión, el valor de esa expresión reemplazará al argumento cuando este
no sea dado.
Por ejemplo, esta versión de potencia hace que su segundo argumento
sea opcional. Si este no es proporcionado o si pasas el valor undefined,
este se establecerá en dos y la función se comportará como cuadrado.
function potencia(base, exponente = 2) {
let resultado = 1;
for (let cuenta = 0; cuenta < exponente; cuenta++) {
resultado *= base;
}
return resultado;
}
console.log(potencia(4));
// → 16
console.log(potencia(2, 6));
// → 64
77
Cierre
La capacidad de tratar a las funciones como valores, combinado con el
hecho de que las vinculaciones locales se vuelven a crear cada vez que
una sea función es llamada, trae a la luz una pregunta interesante. Qué
sucede con las vinculaciones locales cuando la llamada de función que
los creó ya no está activa?
El siguiente código muestra un ejemplo de esto. Define una función,
envolverValor, que crea una vinculación local. Luego retorna una fun-
ción que accede y devuelve esta vinculación local.
function envolverValor(n) {
let local = n;
return () => local;
}
78
buena demostración del hecho de que las vinculaciones locales se crean
de nuevo para cada llamada, y que las diferentes llamadas no pueden
pisotear las distintas vinculaciones locales entre sí.
Esta característica—poder hacer referencia a una instancia especí-
fica de una vinculación local en un alcance encerrado—se llama cierre.
Una función que que hace referencia a vinculaciones de alcances locales
alrededor de ella es llamada un cierre. Este comportamiento no solo
te libera de tener que preocuparte por la duración de las vinculaciones
pero también hace posible usar valores de funciones en algunas formas
bastante creativas.
Con un ligero cambio, podemos convertir el ejemplo anterior en una
forma de crear funciones que multipliquen por una cantidad arbitraria.
function multiplicador(factor) {
return numero => numero * factor;
}
79
se crean. Cuando son llamadas, el cuerpo de la función ve su entorno
original, no el entorno en el que se realiza la llamada.
En el ejemplo, se llama a multiplicador y esta crea un entorno en
el que su parámetro factor está ligado a 2. El valor de función que
retorna, el cual se almacena en duplicar, recuerda este entorno. Asi
que cuando es es llamada, multiplica su argumento por 2.
Recursión
Está perfectamente bien que una función se llame a sí misma, siempre
que no lo haga tanto que desborde la pila. Una función que se llama
a si misma es llamada recursiva. La recursión permite que algunas
funciones sean escritas en un estilo diferente. Mira, por ejemplo, esta
implementación alternativa de potencia:
function potencia(base, exponente) {
if (exponente == 0) {
return 1;
} else {
return base * potencia(base, exponente - 1);
}
}
console.log(potencia(2, 3));
// → 8
80
la exponenciación y posiblemente describa el concepto más claramente
que la variante con el ciclo. La función se llama a si misma muchas veces
con cada vez exponentes más pequeños para lograr la multiplicación
repetida.
Pero esta implementación tiene un problema: en las implementa-
ciones típicas de JavaScript, es aproximadamente 3 veces más lenta que
la versión que usa un ciclo. Correr a través de un ciclo simple es gen-
eralmente más barato en terminos de memoria que llamar a una función
multiples veces.
El dilema de velocidad versus elegancia es interesante. Puedes verlo
como una especie de compromiso entre accesibilidad-humana y accesibilidad
maquina. Casi cualquier programa se puede hacer más rápido hacien-
dolo más grande y complicado. El programador tiene que decidir acerca
de cual es un equilibrio apropiado.
En el caso de la función potencia, la versión poco elegante (con el
ciclo) sigue siendo bastante simple y fácil de leer. No tiene mucho
sentido reemplazarla con la versión recursiva. A menudo, sin embargo,
un programa trata con conceptos tan complejos que renunciar a un poco
de eficiencia con el fin de hacer que el programa sea más sencillo es útil.
Preocuparse por la eficiencia puede ser una distracción. Es otro factor
más que complica el diseño del programa, y cuando estás haciendo
algo que ya es difícil, añadir algo más de lo que preocuparse puede ser
paralizante.
Por lo tanto, siempre comienza escribiendo algo que sea correcto y
fácil de comprender. Si te preocupa que sea demasiado lento—lo que
generalmente no sucede, ya que la mayoría del código simplemente no
81
se ejecuta con la suficiente frecuencia como para tomar cantidades sig-
nificativas de tiempo—puedes medir luego y mejorar si es necesario.
La recursión no siempre es solo una alternativa ineficiente a los ciclos.
Algunos problemas son realmente más fáciles de resolver con recursión
que con ciclos. En la mayoría de los casos, estos son problemas que
requieren explorar o procesar varias “ramas”, cada una de las cuales
podría ramificarse de nuevo en aún más ramas.
Considera este acertijo: comenzando desde el número 1 y repeti-
damente agregando 5 o multiplicando por 3, una cantidad infinita de
números nuevos pueden ser producidos. ¿Cómo escribirías una función
que, dado un número, intente encontrar una secuencia de tales adiciones
y multiplicaciones que produzca ese número?
Por ejemplo, se puede llegar al número 13 multiplicando primero por
3 y luego agregando 5 dos veces, mientras que el número 15 no puede
ser alcanzado de ninguna manera.
Aquí hay una solución recursiva:
function encontrarSolucion(objetivo) {
function encontrar(actual, historia) {
if (actual == objetivo) {
return historia;
} else if (actual > objetivo) {
return null;
} else {
return encontrar(actual + 5, `(${historia} + 5)`) ||
encontrar(actual * 3, `(${historia} * 3)`);
}
}
82
return encontrar(1, "1");
}
console.log(encontrarSolucion(24));
// → (((1 * 3) + 5) * 3)
83
devuelve algo que no es null, esta es retornada. De lo contrario, se re-
torna la segunda llamada, independientemente de si produce un string
o el valor null.
Para comprender mejor cómo esta función produce el efecto que es-
tamos buscando, veamos todas las llamadas a encontrar que se hacen
cuando buscamos una solución para el número 13.
encontrar(1, "1")
encontrar(6, "(1 + 5)")
encontrar(11, "((1 + 5) + 5)")
encontrar(16, "(((1 + 5) + 5) + 5)")
muy grande
encontrar(33, "(((1 + 5) + 5) * 3)")
muy grande
encontrar(18, "((1 + 5) * 3)")
muy grande
encontrar(3, "(1 * 3)")
encontrar(8, "((1 * 3) + 5)")
encontrar(13, "(((1 * 3) + 5) + 5)")
¡encontrado!
84
suceda. Esta búsqueda tiene más suerte—su primera llamada recursiva,
a través de otra llamada recursiva, encuentra al número objetivo. Esa
llamada más interna retorna un string, y cada uno de los operadores
|| en las llamadas intermedias pasa ese string a lo largo, en última
instancia retornando la solución.
Funciones crecientes
Hay dos formas más o menos naturales para que las funciones sean
introducidas en los programas.
La primera es que te encuentras escribiendo código muy similar múlti-
ples veces. Preferiríamos no hacer eso. Tener más código significa más
espacio para que los errores se oculten y más material que leer para las
personas que intenten entender el programa. Entonces tomamos la fun-
cionalidad repetida, buscamos un buen nombre para ella, y la ponemos
en una función.
La segunda forma es que encuentres que necesitas alguna funcional-
idad que aún no has escrito y parece que merece su propia función.
Comenzarás por nombrar a la función y luego escribirás su cuerpo. In-
cluso podrías comenzar a escribir código que use la función antes de
que definas a la función en sí misma.
Que tan difícil te sea encontrar un buen nombre para una función es
una buena indicación de cuán claro es el concepto que está tratando de
envolver. Veamos un ejemplo.
Queremos escribir un programa que imprima dos números, los números
de vacas y pollos en una granja, con las palabras Vacas y Pollos después
85
de ellos, y ceros acolchados antes de ambos números para que siempre
tengan tres dígitos de largo.
007 Vacas
011 Pollos
86
la agricultora (junto con una considerable factura), ella nos llama y nos
dice que ella también comenzó a criar cerdos, y que si no podríamos
extender el software para imprimir cerdos también?
Claro que podemos. Pero justo cuando estamos en el proceso de
copiar y pegar esas cuatro líneas una vez más, nos detenemos y recon-
sideramos. Tiene que haber una mejor manera. Aquí hay un primer
intento:
function imprimirEtiquetaAlcochadaConCeros(numero, etiqueta) {
let stringNumero = String(numero);
while (stringNumero.length < 3) {
stringNumero = "0" + stringNumero;
}
console.log(`${stringNumero} ${etiqueta}`);
}
87
intentemos elegir un solo concepto.
function alcocharConCeros(numero, amplitud) {
let string = String(numero);
while (string.length < amplitud) {
string = "0" + string;
}
return string;
}
88
decimales, relleno con diferentes caracteres, y así sucesivamente.
Un principio útil es no agregar mucho ingenio a menos que estes
absolutamente seguro de que lo vas a necesitar. Puede ser tentador
escribir “frameworks” generalizados para cada funcionalidad que en-
cuentres. Resiste ese impulso. No realizarás ningún trabajo real de
esta manera—solo estarás escribiendo código que nunca usarás.
89
llamada a tal función puede ser sustituida por su valor de retorno sin
cambiar el significado del código. Cuando no estás seguro de que una
función pura esté funcionando correctamente, puedes probarla simple-
mente llamándola, y saber que si funciona en ese contexto, funcionará
en cualquier contexto. Las funciones no puras tienden a requerir más
configuración para poder ser probadas.
Aún así, no hay necesidad de sentirse mal cuando escribas funciones
que no son puras o de hacer una guerra santa para purgarlas de tu
código. Los efectos secundarios a menudo son útiles. No habría forma
de escribir una versión pura de console.log, por ejemplo, y console.log
es bueno de tener. Algunas operaciones también son más fáciles de
expresar de una manera eficiente cuando usamos efectos secundarios,
por lo que la velocidad de computación puede ser una razón para evitar
la pureza.
Resumen
Este capítulo te enseñó a escribir tus propias funciones. La palabra
clave function, cuando se usa como una expresión, puede crear un valor
de función. Cuando se usa como una declaración, se puede usar para
declarar una vinculación y darle una función como su valor. Las fun-
ciones de flecha son otra forma más de crear funciones.
// Define f para sostener un valor de función
const f = function(a) {
console.log(a + 2);
};
90
// Declara g para ser una función
function g(a, b) {
return a * b * 3.5;
}
Ejercicios
Mínimo
El capítulo anterior introdujo la función estándar Math.min que devuelve
su argumento más pequeño. Nosotros podemos construir algo como eso
ahora. Escribe una función min que tome dos argumentos y retorne su
91
mínimo.
Recursión
Hemos visto que % (el operador de residuo) se puede usar para probar
si un número es par o impar usando % 2 para ver si es divisible entre
dos. Aquí hay otra manera de definir si un número entero positivo es
par o impar:
• Zero es par.
• Uno es impar.
• Para cualquier otro número N, su paridad es la misma que N - 2.
Conteo de frijoles
Puedes obtener el N-ésimo carácter, o letra, de un string escribiendo "
string"[N]. El valor devuelto será un string que contiene solo un carác-
ter (por ejemplo, "f"). El primer carácter tiene posición cero, lo que
hace que el último se encuentre en la posición string.length - 1. En
otras palabras, un string de dos caracteres tiene una longitud de 2, y
sus carácteres tendrán las posiciones 0 y 1.
92
Escribe una función contarFs que tome un string como su único ar-
gumento y devuelva un número que indica cuántos caracteres “F” en
mayúsculas haya en el string.
Despues, escribe una función llamada contarCaracteres que se com-
porte como contarFs, excepto que toma un segundo argumento que
indica el carácter que debe ser contado (en lugar de contar solo carac-
teres “F” en mayúscula). Reescribe contarFs para que haga uso de esta
nueva función.
93
“En dos ocasiones me han preguntado, ‘Dinos, Sr. Babbage, si
pones montos equivocadas en la máquina, saldrán las
respuestas correctas? [...] No soy capaz de comprender
correctamente el tipo de confusión de ideas que podrían
provocar tal pregunta.”
—Charles Babbage, Passages from the Life of a Philosopher
(1864)
Chapter 4
Estructuras de Datos: Objetos y Arrays
Los números, los booleanos y los strings son los átomos que constituyen
las
estructuras de datos. Sin embargo, muchos tipos de información re-
quieren más de un átomo. Los objetos nos permiten agrupar valores—
incluidos otros objetos— para construir estructuras más complejas.
Los programas que hemos construido hasta ahora han estado limita-
dos por el hecho de que estaban operando solo en tipos de datos simples.
Este capítulo introducira estructuras de datos básicas. Al final de el,
sabrás lo suficiente como para comenzar a escribir programas útiles.
El capítulo trabajara a través de un ejemplo de programación más
o menos realista, presentando nuevos conceptos según se apliquen al
problema en cuestión. El código de ejemplo a menudo se basara en
funciones y vinculaciones que fueron introducidas anteriormente en el
texto.
La caja de arena en línea para el libro (eloquentjavascript.net/code]
proporciona una forma de ejecutar código en el contexto de un capítulo
en específico. Si decides trabajar con los ejemplos en otro entorno,
94
asegúrate de primero descargar el código completo de este capítulo
desde la página de la caja de arena.
El Hombre Ardilla
De vez en cuando, generalmente entre las ocho y las diez de la noche,
Jacques se encuentra a si mismo transformándose en un pequeño roedor
peludo con una cola espesa.
Por un lado, Jacques está muy contento de no tener la licantropía
clásica. Convertirse en una ardilla causa menos problemas que conver-
tirse en un lobo. En lugar de tener que preocuparse por accidentalmente
comerse al vecino (eso sería incómodo), le preocupa ser comido por el
gato del vecino. Después de dos ocasiones en las que se despertó en
una rama precariamente delgada de la copa de un roble, desnudo y
desorientado, Jacques se ha dedicado a bloquear las puertas y ventanas
de su habitación por la noche y pone algunas nueces en el piso para
mantenerse ocupado.
Eso se ocupa de los problemas del gato y el árbol. Pero Jacques
preferiría deshacerse de su condición por completo. Las ocurrencias
irregulares de la transformación lo hacen sospechar que estas podrían
ser provocadas por algo en especifico. Por un tiempo, creyó que solo
sucedia en los días en los que el había estado cerca de árboles de roble.
Pero evitar los robles no detuvo el problema.
Cambiando a un enfoque más científico, Jacques ha comenzado a
mantener un registro diario de todo lo que hace en un día determi-
nado y si su forma cambio. Con esta información el espera reducir las
95
condiciones que desencadenan las transformaciones.
Lo primero que el necesita es una estructura de datos para almacenar
esta información.
Conjuntos de datos
Para trabajar con una porción de datos digitales, primero debemos en-
contrar una manera de representarlo en la memoria de nuestra máquina.
Digamos, por ejemplo, que queremos representar una colección de los
números 2, 3, 5, 7 y 11.
Podríamos ponernos creativos con los strings—después de todo, los
strings pueden tener cualquier longitud, por lo que podemos poner una
gran cantidad de datos en ellos—y usar "2 3 5 7 11" como nuestra rep-
resentación. Pero esto es incómodo. Tendrías que extraer los dígitos de
alguna manera y convertirlos a números para acceder a ellos.
Afortunadamente, JavaScript proporciona un tipo de datos específi-
camente para almacenar secuencias de valores. Es llamado array y está
escrito como una lista de valores entre corchetes, separados por comas.
let listaDeNumeros = [2, 3, 5, 7, 11];
console.log(listaDeNumeros[2]);
// → 5
console.log(listaDeNumeros[0]);
// → 2
console.log(listaDeNumeros[2 - 1]);
// → 3
96
La notación para llegar a los elementos dentro de un array también
utiliza corchetes. Un par de corchetes inmediatamente después de una
expresión, con otra expresión dentro de ellos, buscará al elemento en la
expresión de la izquierda que corresponde al índice dado por la expresión
entre corchetes.
El primer índice de un array es cero, no uno. Entonces el primer
elemento es alcanzado con listaDeNumeros[0]. El conteo basado en cero
tiene una larga tradición en el mundo de la tecnología, y en ciertas
maneras tiene mucho sentido, pero toma algo de tiempo acostumbrarse.
Piensa en el índice como la cantidad de elementos a saltar, contando
desde el comienzo del array.
Propiedades
Hasta ahora hemos visto algunas expresiones sospechosas como miString
.length (para obtener la longitud de un string) y Math.max (la función
máxima) en capítulos anteriores. Estas son expresiones que acceden a la
propiedad de algún valor. En el primer caso, accedemos a la propiedad
length de el valor en miString. En el segundo, accedemos a la propiedad
llamada max en el objeto Math (que es una colección de constantes y
funciones relacionadas con las matemáticas).
Casi todos los valores de JavaScript tienen propiedades. Las excep-
ciones son null y undefined. Si intentas acceder a una propiedad en
alguno de estos no-valores, obtienes un error.
null.length;
// → TypeError: null has no properties
97
Las dos formas principales de acceder a las propiedades en JavaScript
son con un punto y con corchetes. Tanto valor.x como valor[x] acceden
una propiedad en valor—pero no necesariamente la misma propiedad.
La diferencia está en cómo se interpreta x. Cuando se usa un punto, la
palabra después del punto es el nombre literal de la propiedad. Cuando
usas corchetes, la expresión entre corchetes es evaluada para obtener el
nombre de la propiedad. Mientras valor.x obtiene la propiedad de valor
llamada “x”, valor[x] intenta evaluar la expresión x y usa el resultado,
convertido en un string, como el nombre de la propiedad.
Entonces, si sabes que la propiedad que te interesa se llama color,
dices valor.color. Si quieres extraer la propiedad nombrado por el
valor mantenido en la vinculación i, dices valor[i]. Los nombres de las
propiedades son strings. Pueden ser cualquier string, pero la notación
de puntos solo funciona con nombres que se vean como nombres de
vinculaciones válidos. Entonces, si quieres acceder a una propiedad
llamada 2 o Juan Perez, debes usar corchetes: valor[2] o valor["Juan
Perez"].
Los elementos en un array son almacenados como propiedades del
array, usando números como nombres de propiedad. Ya que no puedes
usar la notación de puntos con números, y que generalmente quieres
utilizar una vinculación que contenga el índice de cualquier manera,
debes de usar la notación de corchetes para llegar a ellos.
La propiedad length de un array nos dice cuántos elementos este
tiene. Este nombre de propiedad es un nombre de vinculación válido, y
sabemos su nombre en avance, así que para encontrar la longitud de un
98
array, normalmente escribes array.length ya que es más fácil de escribir
que array["length"].
Métodos
Ambos objetos de string y array contienen, además de la propiedad
length, una serie de propiedades que tienen valores de función.
99
let secuencia = [1, 2, 3];
secuencia.push(4);
secuencia.push(5);
console.log(secuencia);
// → [1, 2, 3, 4, 5]
console.log(secuencia.pop());
// → 5
console.log(secuencia);
// → [1, 2, 3, 4]
Objetos
De vuelta al Hombre-Ardilla. Un conjunto de entradas diarias puede
ser representado como un array. Pero estas entradas no consisten en
solo un número o un string—cada entrada necesita almacenar una lista
de actividades y un valor booleano que indica si Jacques se convirtió
en una ardilla o no. Idealmente, nos gustaría agrupar estos en un solo
100
valor y luego agrupar estos valores en un array de registro de entradas.
Los valores del tipo objeto son colecciones arbitrarias de propiedades.
Una forma de crear un objeto es mediante el uso de llaves como una
expresión.
let dia1 = {
ardilla: false,
eventos: ["trabajo", "toque un arbol", "pizza", "salir a correr
"]
};
console.log(dia1.ardilla);
// → false
console.log(dia1.lobo);
// → undefined
dia1.lobo = false;
console.log(dia1.lobo);
// → false
Dentro de las llaves, hay una lista de propiedades separadas por co-
mas. Cada propiedad tiene un nombre seguido de dos puntos y un
valor. Cuando un objeto está escrito en varias líneas, indentar como en
el ejemplo ayuda con la legibilidad. Las propiedades cuyos nombres no
sean nombres válidos de vinculaciones o números válidos deben estar
entre comillas.
let descripciones = {
trabajo: "Fui a trabajar",
"toque un arbol": "Toque un arbol"
};
101
Esto significa que las llaves tienen dos significados en JavaScript. Al
comienzo de una declaración, comienzan un bloque de declaraciones.
En cualquier otra posición, describen un objeto. Afortunadamente, es
raramente útil comenzar una declaración con un objeto en llaves, por
lo que la ambigüedad entre estas dos acciones no es un gran problema.
Leer una propiedad que no existe te dará el valor undefined.
Es posible asignarle un valor a una expresión de propiedad con un
operador =. Esto reemplazará el valor de la propiedad si ya tenia uno
o crea una nueva propiedad en el objeto si no fuera así.
Para volver brevemente a nuestro modelo de vinculaciones como
tentáculos—Las vinculaciones de propiedad son similares. Ellas agar-
ran valores, pero otras vinculaciones y propiedades pueden estar agar-
rando esos mismos valores. Puedes pensar en los objetos como pulpos
con cualquier cantidad de tentáculos, cada uno de los cuales tiene un
nombre tatuado en él.
El operador delete (“eliminar”) corta un tentáculo de dicho pulpo. Es
un operador unario que, cuando se aplica a la propiedad de un objeto,
eliminará la propiedad nombrada de dicho objeto. Esto no es algo que
hagas todo el tiempo, pero es posible.
let unObjeto = {izquierda: 1, derecha: 2};
console.log(unObjeto.izquierda);
// → 1
delete unObjeto.izquierda;
console.log(unObjeto.izquierda);
// → undefined
console.log("izquierda" in unObjeto);
102
// → false
console.log("derecha" in unObjeto);
// → true
103
object". Podrias imaginarlos como pulpos largos y planos con todos
sus tentáculos en una fila ordenada, etiquetados con números.
Representaremos el diario de Jacques como un array de objetos.
let diario = [
{eventos: ["trabajo", "toque un arbol", "pizza",
"sali a correr", "television"],
ardilla: false},
{eventos: ["trabajo", "helado", "coliflor",
"lasaña", "toque un arbol", "me cepille los dientes
"],
ardilla: false},
{eventos: ["fin de semana", "monte la bicicleta", "descanso", "
nueces",
"cerveza"],
ardilla: true},
/* y asi sucesivamente... */
];
Mutabilidad
Llegaremos a la programación real pronto. Pero primero, hay una pieza
más de teoría por entender.
Vimos que los valores de objeto pueden ser modificados. Los tipos
de valores discutidos en capítulos anteriores, como números, strings y
booleanos, son todos inmutables—es imposible cambiar los valores de
aquellos tipos. Puedes combinarlos y obtener nuevos valores a partir de
104
ellos, pero cuando tomas un valor de string específico, ese valor siempre
será el mismo. El texto dentro de él no puede ser cambiado. Si tienes
un string que contiene "gato", no es posible que otro código cambie un
carácter en tu string para que deletree "rato".
Los objetos funcionan de una manera diferente. Tu puedes cambiar
sus propiedades, haciendo que un único valor de objeto tenga contenido
diferente en diferentes momentos.
Cuando tenemos dos números, 120 y 120, podemos considerarlos el
mismo número precisamente, ya sea que hagan referencia o no a los
mismos bits físicos. Con los objetos, hay una diferencia entre tener
dos referencias a el mismo objeto y tener dos objetos diferentes que
contengan las mismas propiedades. Considera el siguiente código:
let objeto1 = {valor: 10};
let objeto2 = objeto1;
let objeto3 = {valor: 10};
console.log(objeto1 == objeto2);
// → true
console.log(objeto1 == objeto3);
// → false
objeto1.valor = 15;
console.log(objeto2.valor);
// → 15
console.log(objeto3.valor);
// → 10
105
Las vinculaciones objeto1 y objeto2 agarran el mismo objeto, que es
la razon por la cual cambiar objeto1 también cambia el valor de objeto2.
Se dice que tienen la misma identidad. La vinculación objeto3 apunta a
un objeto diferente, que inicialmente contiene las mismas propiedades
que objeto1 pero vive una vida separada.
Las vinculaciones también pueden ser cambiables o constantes, pero
esto es independiente de la forma en la que se comportan sus valores.
Aunque los valores numéricos no cambian, puedes usar una vinculación
let para hacer un seguimiento de un número que cambia al cambiar
el valor al que apunta la vinculación. Del mismo modo, aunque una
vinculación const a un objeto no pueda ser cambiada en si misma y
continuará apuntando al mismo objeto, los contenidos de ese objeto
pueden cambiar.
const puntuacion = {visitantes: 0, locales: 0};
// Esto esta bien
puntuacion.visitantes = 1;
// Esto no esta permitido
puntuacion = {visitantes: 1, locales: 1};
106
El diario del licántropo
Asi que Jacques inicia su intérprete de JavaScript y establece el entorno
que necesita para mantener su diario.
let diario = [];
107
añadirEntrada(["fin de semana", "monte la bicicleta", "descanso",
"nueces",
"cerveza"], true);
108
No squirrel, no pizza 76 No squirrel, pizza 9
109
Entonces para la tabla de pizza, la parte arriba de la línea de división
(el dividendo) sería 1×76−4×9 = 40, y √ la parte inferior (el divisor)
sería la raíz cuadrada de 5×85×10×80, o 340000. Esto da φ ≈ 0.069,
que es muy pequeño. Comer pizza no parece tener influencia en las
transformaciones.
Calculando correlación
Podemos representar una tabla de dos-por-dos en JavaScript con un
array de cuatro elementos ([76, 9, 4, 1]). También podríamos usar
otras representaciones, como un array que contiene dos arrays de dos
elementos ([[76, 9], [4, 1]]) o un objeto con nombres de propiedad
como "11" y "01", pero el array plano es simple y hace que las expre-
siones que acceden a la tabla agradablemente cortas. Interpretaremos
los índices del array como número binarios de dos-bits , donde el dígito
más a la izquierda (más significativo) se refiere a la variable ardilla y
el digito mas a la derecha (menos significativo) se refiere a la variable
de evento. Por ejemplo, el número binario 10 se refiere al caso en que
Jacques se convirtió en una ardilla, pero el evento (por ejemplo, “pizza”)
no ocurrió. Esto ocurrió cuatro veces. Y dado que el 10 binario es 2 en
notación decimal, almacenaremos este número en el índice 2 del array.
Esta es la función que calcula el coeficiente φ de tal array:
function phi(tabla) {
return (tabla[3] * tabla[0] - tabla[2] * tabla[1]) /
Math.sqrt((tabla[2] + tabla[3]) *
(tabla[0] + tabla[1]) *
110
(tabla[1] + tabla[3]) *
(tabla[0] + tabla[2]));
}
console.log(phi([76, 9, 4, 1]));
// → 0.068599434
111
}
return tabla;
}
console.log(tablaPara("pizza", JOURNAL));
// → [76, 9, 4, 1]
Ciclos de array
En la función tablaPara, hay un ciclo como este:
for (let i = 0; i < DIARIO.length; i++) {
let entrada = DIARIO[i];
// Hacer con algo con la entrada
112
}
El análisis final
Necesitamos calcular una correlación para cada tipo de evento que
ocurra en el conjunto de datos. Para hacer eso, primero tenemos que
encontrar cada tipo de evento.
function eventosDiario(diario) {
let eventos = [];
113
for (let entrada of diario) {
for (let evento of entrada.eventos) {
if (!eventos.includes(evento)) {
eventos.push(evento);
}
}
}
return eventos;
}
console.log(eventosDiario(DIARIO));
// → ["zanahoria", "ejercicio", "fin de semana", "pan", …]
114
La mayoría de las correlaciones parecen estar cercanas a cero. Come
zanahorias, pan o pudín aparentemente no desencadena la licantropía
de ardilla. Parece ocurrir un poco más a menudo los fines de semana.
Filtremos los resultados para solo mostrar correlaciones mayores que
0.1 o menores que -0.1.
for (let evento of eventosDiario(DIARIO)) {
let correlacion = phi(tablaPara(evento, DIARIO));
if (correlacion > 0.1 || correlacion < -0.1) {
console.log(evento + ":", correlacion);
}
}
// → fin de semana: 0.1371988681
// → me cepille los dientes: -0.3805211953
// → dulces: 0.1296407447
// → trabajo: -0.1371988681
// → spaghetti: 0.2425356250
// → leer: 0.1106828054
// → nueces: 0.5902679812
A-ha! Hay dos factores con una correlación que es claramente más
fuerte que las otras. Comer nueces tiene un fuerte efecto positivo en
la posibilidad de convertirse en una ardilla, mientras que cepillarse los
dientes tiene un significativo efecto negativo.
Interesante. Intentemos algo.
for (let entrada of DIARIO) {
if (entrada.eventos.includes("nueces") &&
!entrada.eventos.includes("me cepille los dientes")) {
115
entrada.eventos.push("dientes con nueces");
}
}
console.log(phi(tablaPara("dientes con nueces", DIARIO)));
// → 1
Arrayología avanzada
Antes de terminar el capítulo, quiero presentarte algunos conceptos
extras relacionados a los objetos. Comenzaré introduciendo algunos en
métodos de arrays útiles generalmente.
116
Vimos push y pop, que agregan y removen elementos en el final de un
array, anteriormente en este capítulo. Los métodos correspondientes
para agregar y remover cosas en el comienzo de un array se llaman
unshift y shift.
117
console.log([1, 2, 3, 2, 1].indexOf(2));
// → 1
console.log([1, 2, 3, 2, 1].lastIndexOf(2));
// → 3
118
return array.slice(0, indice)
.concat(array.slice(indice + 1));
}
console.log(remover(["a", "b", "c", "d", "e"], 2));
// → ["a", "b", "d", "e"]
119
console.log("panaderia".slice(0, 3));
// → pan
console.log("panaderia".indexOf("a"));
// → 1
120
metodo split (“dividir”), y unirlo nuevamente con join (“unir”).
let oracion = "Los pajaros secretarios se especializan en
pisotear";
let palabras = oracion.split(" ");
console.log(palabras);
// → ["Los", "pajaros", "secretarios", "se", "especializan", "en
", "pisotear"]
console.log(palabras.join(". "));
// → Los. pajaros. secretarios. se. especializan. en. pisotear
121
Parámetros restantes
Puede ser útil para una función aceptar cualquier cantidad de argumen-
tos. Por ejemplo, Math.max calcula el máximo de todos los argumentos
que le son dados.
Para escribir tal función, pones tres puntos antes del ultimo parámetro
de la función, asi:
function maximo(...numeros) {
let resultado = -Infinity;
for (let numero of numeros) {
if (numero > resultado) resultado = numero;
}
return resultado;
}
console.log(maximo(4, 1, 9, -2));
// → 9
122
let numeros = [5, 1, 7];
console.log(max(...numeros));
// → 7
El objeto Math
Como hemos visto, Math es una bolsa de sorpresas de utilidades rela-
cionadas a los numeros, como Math.max (máximo), Math.min (mínimo) y
Math.sqrt (raíz cuadrada).
El objeto Math es usado como un contenedor que agrupa un grupo de
funcionalidades relacionadas. Solo hay un objeto Math, y casi nunca es
útil como un valor. Más bien, proporciona un espacio de nombre para
123
que todos estas funciones y valores no tengan que ser vinculaciones
globales.
Tener demasiadas vinculaciones globales “contamina” el espacio de
nombres. Cuanto más nombres hayan sido tomados, es más probable
que accidentalmente sobrescribas el valor de algunas vinculaciones ex-
istentes. Por ejemplo, no es es poco probable que quieras nombrar algo
max en alguno de tus programas. Dado que la función max ya incorpo-
rada en JavaScript está escondida dentro del Objeto Math, no tenemos
que preocuparnos por sobrescribirla.
Muchos lenguajes te detendrán, o al menos te advertirán, cuando
estes por definir una vinculación con un nombre que ya este tomado.
JavaScript hace esto para vinculaciones que hayas declarado con let
oconst pero-perversamente-no para vinculaciones estándar, ni para vin-
culaciones declaradas con var o function.
De vuelta al objeto Math. Si necesitas hacer trigonometría, Math te
puede ayudar. Contiene cos (coseno), sin (seno) y tan (tangente), así
como sus funciones inversas, acos, asin, y atan, respectivamente. El
número π (pi)—o al menos la aproximación más cercano que cabe en
un número de JavaScript—está disponible como Math.PI. Hay una vieja
tradición en la programación de escribir los nombres de los valores con-
stantes en mayúsculas.
function puntoAleatorioEnCirculo(radio) {
let angulo = Math.random() * 2 * Math.PI;
return {x: radio * Math.cos(angulo),
y: radio * Math.sin(angulo)};
}
console.log(puntoAleatorioEnCirculo(2));
124
// → {x: 0.3667, y: 1.966}
Si los senos y los cosenos son algo con lo que no estas familiarizado,
no te preocupes. Cuando se usen en este libro, en el Capítulo 14, te los
explicaré.
El ejemplo anterior usó Math.random. Esta es una función que retorna
un nuevo número pseudoaleatorio entre cero (inclusivo) y uno (exclu-
sivo) cada vez que la llamas.
console.log(Math.random());
// → 0.36993729369714856
console.log(Math.random());
// → 0.727367032552138
console.log(Math.random());
// → 0.40180766698904335
125
más cercano) con el resultado de Math.random.
console.log(Math.floor(Math.random() * 10));
// → 2
Desestructurar
Volvamos a la función phi por un momento:
function phi(tabla) {
return (tabla[3] * tabla[0] - tabla[2] * tabla[1]) /
Math.sqrt((tabla[2] + tabla[3]) *
(tabla[0] + tabla[1]) *
(tabla[1] + tabla[3]) *
(tabla[0] + tabla[2]));
}
126
Una de las razones por las que esta función es incómoda de leer es que
tenemos una vinculación apuntando a nuestro array, pero preferiríamos
tener vinculaciones para los elementos del array, es decir, let n00 =
tabla[0] y así sucesivamente. Afortunadamente, hay una forma concisa
de hacer esto en JavaScript.
function phi([n00, n01, n10, n11]) {
return (n11 * n00 - n10 * n01) /
Math.sqrt((n10 + n11) * (n00 + n01) *
(n01 + n11) * (n00 + n10));
}
127
JSON
Ya que las propiedades solo agarran su valor, en lugar de contenerlo,
los objetos y arrays se almacenan en la memoria de la computadora
como secuencias de bits que contienen las direcciónes—el lugar en la
memoria—de sus contenidos. Asi que un array con otro array dentro
de el consiste en (al menos) una región de memoria para el array interno,
y otra para el array externo, que contiene (entre otras cosas) un número
binario que representa la posición del array interno.
Si deseas guardar datos en un archivo para más tarde, o para en-
viarlo a otra computadora a través de la red, tienes que convertir de
alguna manera estos enredos de direcciones de memoria a una descrip-
ción que se puede almacenar o enviar. Supongo, que podrías enviar
toda la memoria de tu computadora junto con la dirección del valor
que te interesa, pero ese no parece el mejor enfoque.
Lo que podemos hacer es serializar los datos. Eso significa que son
convertidos a una descripción plana. Un formato de serialización popu-
lar llamado JSON (pronunciado “Jason”), que significa JavaScript Ob-
ject Notation (“Notación de Objetos JavaScript”). Es ampliamente
utilizado como un formato de almacenamiento y comunicación de datos
en la Web, incluso en otros lenguajes diferentes a JavaScript.
JSON es similar a la forma en que JavaScript escribe arrays y objetos,
con algunas restricciones. Todos los nombres de propiedad deben estar
rodeados por comillas dobles, y solo se permiten expresiones de datos
simples—sin llamadas a función, vinculaciones o cualquier otra cosa que
involucre computaciones reales. Los comentarios no están permitidos
en JSON.
128
Una entrada de diario podria verse así cuando se representa como
datos JSON:
{
"ardilla": false,
"eventos": ["trabajo", "toque un arbol", "pizza", "sali a
correr"]
}
Resumen
Los objetos y arrays (que son un tipo específico de objeto) proporcionan
formas de agrupar varios valores en un solo valor. Conceptualmente,
esto nos permite poner un montón de cosas relacionadas en un bolso y
129
correr alredor con el bolso, en lugar de envolver nuestros brazos alrede-
dor de todas las cosas individuales, tratando de aferrarnos a ellas por
separado.
La mayoría de los valores en JavaScript tienen propiedades, las ex-
cepciones son null y undefined. Se accede a las propiedades usando
valor.propiedad o valor["propiedad"]. Los objetos tienden a usar nom-
bres para sus propiedades y almacenar más o menos un conjunto fijo de
ellos. Los arrays, por el otro lado, generalmente contienen cantidades
variables de valores conceptualmente idénticos y usa números (comen-
zando desde 0) como los nombres de sus propiedades.
Hay algunas propiedades con nombre en los arrays, como length
y un numero de metodos. Los métodos son funciones que viven en
propiedades y (por lo general) actuan sobre el valor del que son una
propiedad.
Puedes iterar sobre los arrays utilizando un tipo especial de ciclo
for—for (let elemento of array).
Ejercicios
La suma de un rango
La introducción de este libro aludía a lo siguiente como una buena forma
de calcular la suma de un rango de números:
console.log(suma(rango(1, 10)));
Escribe una función rango que tome dos argumentos, inicio y final,
130
y retorne un array que contenga todos los números desde inicio hasta
(e incluyendo) final.
Luego, escribe una función suma que tome un array de números y
retorne la suma de estos números. Ejecuta el programa de ejemplo y
ve si realmente retorna 55.
Como una misión extra, modifica tu función rango para tomar un
tercer argumento opcional que indique el valor de “paso” utilizado para
cuando construyas el array. Si no se da ningún paso, los elementos suben
en incrementos de uno, correspondiedo al comportamiento anterior. La
llamada a la función rango(1, 10, 2) deberia retornar [1, 3, 5, 7, 9].
Asegúrate de que también funcione con valores de pasos negativos para
que rango(5, 2, -1) produzca [5, 4, 3, 2].
Revirtiendo un array
Los arrays tienen un método reverse que cambia al array invirtiendo
el orden en que aparecen sus elementos. Para este ejercicio, escribe
dos funciones, revertirArray y revertirArrayEnSuLugar. El primero,
revertirArray, toma un array como argumento y produce un nuevo ar-
ray que tiene los mismos elementos pero en el orden inverso. El segundo,
revertirArrayEnSuLugar, hace lo que hace el métodoreverse: modifica el
array dado como argumento invirtiendo sus elementos. Ninguno de los
dos puede usar el método reverse estándar.
Pensando en las notas acerca de los efectos secundarios y las funciones
puras en el capítulo anterior, qué variante esperas que sea útil en más
situaciones? Cuál corre más rápido?
131
Una lista
Los objetos, como conjuntos genéricos de valores, se pueden usar para
construir todo tipo de estructuras de datos. Una estructura de datos
común es la lista (no confundir con un array). Una lista es un conjunto
anidado de objetos, con el primer objeto conteniendo una referencia al
segundo, el segundo al tercero, y así sucesivamente.
let lista = {
valor: 1,
resto: {
valor: 2,
resto: {
valor: 3,
resto: null
}
}
};
132
parten la estructura que conforma sus últimos tres elementos. La lista
original también sigue siendo una lista válida de tres elementos.
Escribe una función arrayALista que construya una estructura de
lista como el que se muestra arriba cuando se le da [1, 2, 3] como
argumento. También escribe una función listaAArray que produzca un
array de una lista. Luego agrega una función de utilidad preceder,
que tome un elemento y una lista y creé una nueva lista que agrega el
elemento al frente de la lista de entrada, y posicion, que toma una lista
y un número y retorne el elemento en la posición dada en la lista (con
cero refiriéndose al primer elemento) o undefined cuando no exista tal
elemento.
Si aún no lo has hecho, también escribe una versión recursiva de
posicion.
Comparación profunda
El operador == compara objetos por identidad. Pero a veces preferirias
comparar los valores de sus propiedades reales.
Escribe una función igualdadProfunda que toma dos valores y re-
torne true solo si tienen el mismo valor o son objetos con las mismas
propiedades, donde los valores de las propiedades sean iguales cuando
comparadas con una llamada recursiva a igualdadProfunda.
Para saber si los valores deben ser comparados directamente (usa
el operador == para eso) o si deben tener sus propiedades comparadas,
puedes usar el operador typeof. Si produce "object" para ambos valores,
deberías hacer una comparación profunda. Pero tienes que tomar una
excepción tonta en cuenta: debido a un accidente histórico, typeof null
133
también produce "object".
La función Object.keys será útil para cuando necesites revisar las
propiedades de los objetos para compararlos.
134
“Hay dos formas de construir un diseño de software: Una
forma es hacerlo tan simple de manera que no hayan
deficiencias obvias, y la otra es hacerlo tan complicado de
manera que obviamente no hayan deficiencias.”
—C.A.R. Hoare, 1980 ACM Turing Award Lecture
Chapter 5
Funciones de Orden Superior
Un programa grande es un programa costoso, y no solo por el tiempo
que se necesita para construirlo. El tamaño casi siempre involucra com-
plejidad, y la complejidad confunde a los programadores. A su vez, los
programadores confundidos, introducen errores en los programas. Un
programa grande entonces proporciona de mucho espacio para que estos
bugs se oculten, haciéndolos difíciles de encontrar.
Volvamos rapidamente a los dos últimos programas de ejemplo en la
introducción. El primero es auto-contenido y solo tiene seis líneas de
largo:
let total = 0, cuenta = 1;
while (cuenta <= 10) {
total += cuenta;
cuenta += 1;
}
console.log(total);
135
longitud:
console.log(suma(rango(1, 10)));
Abstracción
En el contexto de la programación, estos tipos de vocabularios suelen
ser llamados abstracciones. Las abstracciones esconden detalles y nos
dan la capacidad de hablar acerca de los problemas a un nivel superior
(o más abstracto).
Como una analogía, compara estas dos recetas de sopa de guisantes:
136
cipiente. Agregue agua hasta que los guisantes esten bien
cubiertos. Deje los guisantes en agua durante al menos 12
horas. Saque los guisantes del agua y pongalos en una
cacerola para cocinar. Agregue 4 tazas de agua por per-
sona. Cubra la sartén y mantenga los guisantes hirviendo
a fuego lento durante dos horas. Tome media cebolla por
persona. Cortela en piezas con un cuchillo. Agréguela a los
guisantes. Tome un tallo de apio por persona. Cortelo en
pedazos con un cuchillo. Agréguelo a los guisantes. Tome
una zanahoria por persona. Cortela en pedazos. Con un
cuchillo! Agregarla a los guisantes. Cocine por 10 minutos
más.
Y la segunda receta:
137
pasos precisos que debe realizar la computadora, uno por uno, ciego a
los conceptos de orden superior que estos expresan.
En la programación, es una habilidad útil, darse cuenta cuando estás
trabajando en un nivel de abstracción demasiado bajo.
Abstrayendo la repetición
Las funciones simples, como las hemos visto hasta ahora, son una buena
forma de construir abstracciones. Pero a veces se quedan cortas.
Es común que un programa haga algo una determinada cantidad de
veces. Puedes escribir un ciclo for para eso, de esta manera:
for (let i = 0; i < 10; i++) {
console.log(i);
}
138
ciones solo son valores, podemos pasar nuestra acción como un valor de
función.
function repetir(n, accion) {
for (let i = 0; i < n; i++) {
accion(i);
}
}
repetir(3, console.log);
// → 0
// → 1
// → 2
139
de cierre y paréntesis de cierre. En casos como este ejemplo, donde el
cuerpo es una expresión pequeña y única, podrias tambien omitir las
llaves y escribir el ciclo en una sola línea.
140
let resultado = funcion(...argumentos);
console.log("llamada con", argumentos, ", retorno", resultado
);
return resultado;
};
}
ruidosa(Math.min)(3, 2, 1);
// → llamando con [3, 2, 1]
// → llamada con [3, 2, 1] , retorno 1
repetir(3, n => {
aMenosQue(n % 2 == 1, () => {
console.log(n, "es par");
});
});
// → 0 es par
// → 2 es par
141
// → A
// → B
142
en la caja de arena para este capítulo (eloquentjavascript.net/code#5)
como la vinculación SCRIPTS. La vinculación contiene un array de obje-
tos, cada uno de los cuales describe un codigo.
{
name: "Coptic",
ranges: [[994, 1008], [11392, 11508], [11513, 11520]],
direction: "ltr",
year: -200,
living: false,
link: "https://en.wikipedia.org/wiki/Coptic_alphabet"
}
Tal objeto te dice el nombre del codigo, los rangos de Unicode asig-
nados a él, la dirección en la que está escrito, la tiempo de origen
(aproximado), si todavía está en uso, y un enlace a más información.
La dirección en la que esta escrito puede ser "ltr" (left-to-right) para
izquierda a derecha, "rtl" (right-to-left) para derecha a izquierda (la
forma en que se escriben los textos en árabe y en hebreo), o "ttb" (top-
to-bottom) para de arriba a abajo (como con la escritura de Mongolia).
La propiedad ranges contiene un array de rangos de caracteres Uni-
code, cada uno de los cuales es un array de dos elementos que contiene
límites inferior y superior. Se asignan los códigos de caracteres dentro
de estos rangos al codigo. El limite más bajo es inclusivo (el código
994 es un carácter Copto) y el límite superior es no-inclusivo (el código
1008 no lo es).
143
Filtrando arrays
Para encontrar los codigos en el conjunto de datos que todavía están
en uso, la siguiente función podría ser útil. Filtra hacia afuera los
elementos en un array que no pasen una prueba:
function filtrar(array, prueba) {
let pasaron = [];
for (let elemento of array) {
if (prueba(elemento)) {
pasaron.push(elemento);
}
}
return pasaron;
}
144
en su lugar:
console.log(SCRIPTS.filter(codigo => codigo.direction == "ttb"));
// → [{name: "Mongolian", …}, …]
145
;
// → ["Adlam", "Arabic", "Imperial Aramaic", …]
146
console.log(reduce([1, 2, 3, 4], (a, b) => a + b, 0));
// → 10
Para usar reduce (dos veces) para encontrar el codigo con la mayor
cantidad de caracteres, podemos escribir algo como esto:
function cuentaDeCaracteres(codigo) {
return codigo.ranges.reduce((cuenta, [desde, hasta]) => {
return cuenta + (hasta - desde);
}, 0);
}
console.log(SCRIPTS.reduce((a, b) => {
return cuentaDeCaracteres(a) < cuentaDeCaracteres(b) ? b : a;
}));
// → {name: "Han", …}
147
La función cuentaDeCaracteres reduce los rangos asignados a un codigo
sumando sus tamaños. Ten en cuenta el uso de la desestructuración en
el parámetro lista de la función reductora. La segunda llamada a reduce
luego usa esto para encontrar el codigo más grande al comparar repeti-
damente dos scripts y retornando el más grande.
El codigo Han tiene más de 89,000 caracteres asignados en el Están-
dar Unicode, por lo que es, por mucho, el mayor sistema de escritura
en el conjunto de datos. Han es un codigo (a veces) usado para texto
chino, japonés y coreano. Esos idiomas comparten muchos caracteres,
aunque tienden a escribirlos de manera diferente. El consorcio Unicode
(con sede en EE.UU.) decidió tratarlos como un único sistema de es-
critura para ahorrar códigos de caracteres. Esto se llama unificación
Han y aún enoja bastante a algunas personas.
Composabilidad
Considera cómo habríamos escrito el ejemplo anterior (encontrar el
código más grande) sin funciones de orden superior. El código no es
mucho peor.
let mayor = null;
for (let codigo of SCRIPTS) {
if (mayor == null ||
cuentaDeCaracteres(mayor) < cuentaDeCaracteres(codigo)) {
mayor = codigo;
}
}
console.log(mayor);
148
// → {name: "Han", …}
console.log(Math.round(promedio(
SCRIPTS.filter(codigo => codigo.living).map(codigo => codigo.
year))));
// → 1185
console.log(Math.round(promedio(
SCRIPTS.filter(codigo => !codigo.living).map(codigo => codigo.
year))));
// → 209
149
tomamos los años de aquellos, los promediamos, y redondeamos el re-
sultado.
Definitivamente también podrías haber escribir este codigo como un
gran ciclo.
let total = 0, cuenta = 0;
for (let codigo of SCRIPTS) {
if (codigo.living) {
total += codigo.year;
cuenta += 1;
}
}
console.log(Math.round(total / cuenta));
// → 1185
150
Strings y códigos de caracteres
Un uso del conjunto de datos sería averiguar qué código esta usando
una pieza de texto. Veamos un programa que hace esto.
Recuerda que cada codigo tiene un array de rangos para los códigos
de caracteres asociados a el. Entonces, dado un código de carácter,
podríamos usar una función como esta para encontrar el codigo corre-
spondiente (si lo hay):
function codigoCaracter(codigo_caracter) {
for (let codigo of SCRIPTS) {
if (codigo.ranges.some(([desde, hasta]) => {
return codigo_caracter >= desde && codigo_caracter < hasta;
})) {
return codigo;
}
}
return null;
}
console.log(codigoCaracter(121));
// → {name: "Latin", …}
151
ificados como una secuencia de números de 16 bits. Estos se llaman
unidades de código. Inicialmente se suponía que un código de carácter
Unicode encajara dentro de esa unidad (lo que da un poco más de 65,000
caracteres). Cuando quedó claro que esto no seria suficiente, muchas
personas se resistieron a la necesidad de usar más memoria por carác-
ter. Para apaciguar estas preocupaciones, UTF-16, el formato utilizado
por los strings de JavaScript, fue inventado. Este describe la mayoría
de los caracteres mas comunes usando una sola unidad de código de 16
bits, pero usa un par de dos de esas unidades para otros caracteres.
Al dia de hoy UTF-16 generalmente se considera como una mala
idea. Parece casi intencionalmente diseñado para invitar a errores. Es
fácil escribir programas que pretenden que las unidades de código y
caracteres son la misma cosa. Y si tu lenguaje no usa caracteres de dos
unidades, esto parecerá funcionar simplemente bien. Pero tan pronto
como alguien intente usar dicho programa con algunos menos comunes
caracteres chinos, este se rompe. Afortunadamente, con la llegada del
emoji, todo el mundo ha empezado a usar caracteres de dos unidades, y
la carga de lidiar con tales problemas esta bastante mejor distribuida.
Desafortunadamente, las operaciones obvias con strings de JavaScript,
como obtener su longitud a través de la propiedad length y acceder a
su contenido usando corchetes, trata solo con unidades de código.
// Dos caracteres emoji, caballo y zapato
let caballoZapato = "🐴👟";
console.log(caballoZapato.length);
// → 4
console.log(caballoZapato[0]);
152
// → ((Medio-carácter inválido))
console.log(caballoZapato.charCodeAt(0));
// → 55357 (Código del medio-carácter)
console.log(caballoZapato.codePointAt(0));
// → 128052 (Código real para emoji de caballo)
153
Si tienes un caracter (que será un string de unidades de uno o dos
códigos), puedes usar codePointAt(0) para obtener su código.
Reconociendo texto
Tenemos una función codigoCaracter y una forma de correctamente
hacer un ciclo a traves de caracteres. El siguiente paso sería contar
los caracteres que pertenecen a cada codigo. La siguiente abstracción
de conteo será útil para eso:
function contarPor(elementos, nombreGrupo) {
let cuentas = [];
for (let elemento of elementos) {
let nombre = nombreGrupo(elemento);
let conocido = cuentas.findIndex(c => c.nombre == nombre);
if (conocido == -1) {
cuentas.push({nombre, cuenta: 1});
} else {
cuentas[conocido].cuenta++;
}
}
return cuentas;
}
154
podamos hacer un ciclo for/of) y una función que calcula un nombre
de grupo para un elemento dado. Retorna un array de objetos, cada
uno de los cuales nombra un grupo y te dice la cantidad de elementos
que se encontraron en ese grupo.
Utiliza otro método de array—findIndex (“encontrar index”). Este
método es algo así como indexOf, pero en lugar de buscar un valor
específico, este encuentra el primer valor para el cual la función dada
retorna verdadero. Como indexOf, retorna -1 cuando no se encuentra
dicho elemento.
Usando contarPor, podemos escribir la función que nos dice qué codi-
gos se usan en una pieza de texto.
function codigosTexto(texto) {
let codigos = contarPor(texto, caracter => {
let codigo = codigoCaracter(caracter.codePointAt(0));
return codigo ? codigo.name : "ninguno";
}).filter(({name}) => name != "ninguno");
console.log(codigosTexto('英国的狗说"woof", 俄罗斯的狗说"тяв"')
);
// → 61% Han, 22% Latin, 17% Cyrillic
155
La función primero cuenta los caracteres por nombre, usando codigoCarac
para asignarles un nombre, y recurre al string "ninguno" para caracteres
que no son parte de ningún codigo. La llamada filter deja afuera las
entrada para "ninguno" del array resultante, ya que no estamos intere-
sados en esos caracteres.
Para poder calcular porcentajes, primero necesitamos la cantidad to-
tal de caracteres que pertenecen a un codigo, lo que podemos calcular
con reduce. Si no se encuentran tales caracteres, la función retorna un
string específico. De lo contrario, transforma las entradas de conteo en
strings legibles con map y luego las combina con join.
Resumen
Ser capaz de pasar valores de función a otras funciones es un aspecto
profundamente útil de JavaScript. Nos permite escribir funciones que
modelen calculos con “brechas” en ellas. El código que llama a estas
funciones pueden llenar estas brechas al proporcionar valores de función.
Los arrays proporcionan varios métodos útiles de orden superior.
Puedes usar forEach para recorrer los elementos en un array. El método
filter retorna un nuevo array que contiene solo los elementos que pasan
una función de predicado. Transformar un array al poner cada elemento
a través de una función se hace con map. Puedes usar reduce para com-
binar todos los elementos en una array a un solo valor. El método some
prueba si algun elemento coincide con una función de predicado deter-
minada. Y findIndex encuentra la posición del primer elemento que
156
coincide con un predicado.
Ejercicios
Aplanamiento
Use el método reduce en combinación con el método concat para “apla-
nar” un array de arrays en un único array que tenga todos los elementos
de los arrays originales.
Tu propio ciclo
Escriba una función de orden superior llamada ciclo que proporcione
algo así como una declaración de ciclo for. Esta toma un valor, una
función de prueba, una función de actualización y un cuerpo de función.
En cada iteración, primero ejecuta la función de prueba en el valor
actual del ciclo y se detiene si esta retorna falso. Luego llama al cuerpo
de función, dándole el valor actual. Y finalmente, llama a la función de
actualización para crear un nuevo valor y comienza desde el principio.
Cuando definas la función, puedes usar un ciclo regular para hacer
los ciclos reales.
Cada
De forma análoga al método some, los arrays también tienen un método
every (“cada”). Este retorna true cuando la función dada devuelve
verdadero para cada elemento en el array. En cierto modo, some es una
157
versión del operador || que actúa en arrays, y every es como el operador
&&.
Implementa every como una función que tome un array y una función
predicado como parámetros. Escribe dos versiones, una usando un ciclo
y una usando el método some.
158
“Un tipo de datos abstracto se realiza al escribir un tipo
especial de programa [...] que define el tipo en base a las
operaciones que puedan ser realizadas en él.”
—Barbara Liskov, Programming with Abstract Data Types
Chapter 6
La Vida Secreta de los Objetos
El Capítulo 4 introdujo los objetos en JavaScript. En la cultura de
la programación, tenemos una cosa llamada programación orientada a
objetos, la cual es un conjunto de técnicas que usan objetos (y conceptos
relacionados) como el principio central de la organización del programa.
Aunque nadie realmente está de acuerdo con su definición exacta,
la programación orientada a objetos ha contribuido al diseño de mu-
chos lenguajes de programación, incluyendo JavaScript. Este capí-
tulo describirá la forma en la que estas ideas pueden ser aplicadas en
JavaScript.
Encapsulación
La idea central en la programación orientada a objetos es dividir a los
programas en piezas más pequeñas y hacer que cada pieza sea respon-
sable de gestionar su propio estado.
De esta forma, los conocimientos acerca de como funciona una parte
del programa pueden mantenerse locales a esa pieza. Alguien traba-
159
jando en otra parte del programa no tiene que recordar o ni siquiera
tener una idea de ese conocimiento. Cada vez que los detalles locales
cambien, solo el código directamente a su alrededor debe ser actual-
izado.
Las diferentes piezas de un programa como tal, interactúan entre sí
a través de interfaces, las cuales son conjuntos limitados de funciones y
vinculaciones que proporcionan funcionalidades útiles en un nivel más
abstracto, ocultando asi su implementación interna.
Tales piezas del programa se modelan usando objetos. Sus interfaces
consisten en un conjunto específico de métodos y propiedades. Las
propiedades que son parte de la interfaz se llaman publicas. Las otras,
las cuales no deberian ser tocadas por el código externo , se les llama
privadas.
Muchos lenguajes proporcionan una forma de distinguir entre propiedade
publicas y privadas, y ademas evitarán que el código externo pueda ac-
ceder a las privadas por completo. JavaScript, una vez más tomando el
enfoque minimalista, no hace esto. Todavía no, al menos—hay trabajo
en camino para agregar esto al lenguaje.
Aunque el lenguaje no tenga esta distinción incorporada, los progra-
madores de JavaScript estan usando esta idea con éxito .Típicamente, la
interfaz disponible se describe en la documentación o en los comentar-
ios. También es común poner un carácter de guión bajo (_) al comienzo
de los nombres de las propiedades para indicar que estas propiedades
son privadas.
Separar la interfaz de la implementación es una gran idea. Esto
usualmente es llamado encapsulación.
160
Métodos
Los métodos no son más que propiedades que tienen valores de función.
Este es un método simple:
let conejo = {};
conejo.hablar = function(linea) {
console.log(`El conejo dice '${linea}'`);
};
conejo.hablar("Estoy vivo.");
// → El conejo dice 'Estoy vivo.'
161
conejoHambriento.hablar("Podria comerme una zanahoria ahora mismo
.");
// → El conejo hambriento dice 'Podria comerme una zanahoria
ahora mismo.'
Como cada función tiene su propia vinculación this, cuyo valor de-
pende de la forma en como esta se llama, no puedes hacer referencia
al this del alcance envolvente en una función regular definida con la
palabra clave function.
Las funciones de flecha son diferentes—no crean su propia vinculación
this, pero pueden ver la vinculaciónthis del alcance a su alrededor. Por
lo tanto, puedes hacer algo como el siguiente código, que hace referencia
a this desde adentro de una función local:
function normalizar() {
console.log(this.coordinadas.map(n => n / this.length));
}
normalizar.call({coordinadas: [0, 2, 3], length: 5});
162
// → [0, 0.4, 0.6]
Prototipos
Observa atentamente.
let vacio = {};
console.log(vacio.toString);
// → function toString()…{}
console.log(vacio.toString());
// → [object Object]
163
console.log(Object.getPrototypeOf({}) ==
Object.prototype);
// → true
console.log(Object.getPrototypeOf(Object.prototype));
// → null
164
como toString.
Puede usar Object.create para crear un objeto con un prototipo es-
pecifico.
let conejoPrototipo = {
hablar(linea) {
console.log(`El conejo ${this.tipo} dice '${linea}'`);
}
};
let conejoAsesino = Object.create(conejoPrototipo);
conejoAsesino.tipo = "asesino";
conejoAsesino.hablar("SKREEEE!");
// → El conejo asesino dice 'SKREEEE!'
Clases
El sistema de prototipos en JavaScript se puede interpretar como un en-
foque informal de un concepto orientado a objetos llamado clasees. Una
165
clase define la forma de un tipo de objeto—qué métodos y propiedades
tiene este. Tal objeto es llamado una instancia de la clase.
Los prototipos son útiles para definir propiedades en las cuales todas
las instancias de una clase compartan el mismo valor, como métodos.
Las propiedades que difieren por instancia, como la propiedad tipo
en nuestros conejos, necesitan almacenarse directamente en los objetos
mismos.
Entonces, para crear una instancia de una clase dada, debes crear
un objeto que derive del prototipo adecuado, pero también debes ase-
gurarte de que, en sí mismo, este objeto tenga las propiedades que las
instancias de esta clase se supone que tengan. Esto es lo que una función
constructora hace.
function crearConejo(tipo) {
let conejo = Object.create(conejoPrototipo);
conejo.tipo = tipo;
return conejo;
}
166
function Conejo(tipo) {
this.tipo = tipo;
}
Conejo.prototype.hablar = function(linea) {
console.log(`El conejo ${this.tipo} dice '${linea}'`);
};
167
console.log(Object.getPrototypeOf(conejoRaro) ==
Conejo.prototype);
// → true
Notación de clase
Entonces, las clasees en JavaScript son funciones constructoras con una
propiedad prototipo. Así es como funcionan, y hasta 2015, esa era
la manera en como tenías que escribirlas. Estos días, tenemos una
notación menos incómoda.
class Conejo {
constructor(tipo) {
this.tipo = tipo;
}
hablar(linea) {
console.log(`El conejo ${this.tipo} dice '${linea}'`);
}
}
168
tro de las llaves de la declaración. El metodo llamado constructor es
tratado de una manera especial. Este proporciona la función construc-
tora real, que estará vinculada al nombre Conejo. Los otros metodos
estaran empacados en el prototipo de ese constructor. Por lo tanto, la
declaración de clase anterior es equivalente a la definición de construc-
tor en la sección anterior. Solo que se ve mejor.
Actualmente las declaraciones de clase solo permiten que los meto-
dos—propiedades que contengan funciones—puedan ser agregados al
prototipo. Esto puede ser algo inconveniente para cuando quieras guardar
un valor no-funcional allí. La próxima versión del lenguaje probable-
mente mejore esto. Por ahora, tú puedes crear tales propiedades al
manipular directamente el prototipo después de haber definido la clase.
Al igual que function, class se puede usar tanto en posiciones de
declaración como de expresión. Cuando se usa como una expresión, no
define una vinculación, pero solo produce el constructor como un valor.
Tienes permitido omitir el nombre de clase en una expresión de clase.
let objeto = new class { obtenerPalabra() { return "hola"; } };
console.log(objeto.obtenerPalabra());
// → hola
169
Si ya había una propiedad con el mismo nombre en el prototipo, esta
propiedad ya no afectará al objeto, ya que ahora está oculta detrás de
la propiedad del propio objeto.
Rabbit.prototype.dientes = "pequeños";
console.log(conejoAsesino.dientes);
// → pequeños
conejoAsesino.dientes = "largos, filosos, y sangrientos";
console.log(conejoAsesino.dientes);
// → largos, filosos, y sangrientos
console.log(conejoNegro.dientes);
// → pequeños
console.log(Rabbit.prototype.dientes);
// → pequeños
prototype
Object
killerRabbit
create: <function>
teeth: "long, sharp, ..."
prototype
type: "killer"
...
teeth: "small"
speak: <function>
toString: <function>
...
170
Sobreescribir propiedades que existen en un prototipo puede ser algo
útil que hacer. Como muestra el ejemplo de los dientes de conejo,
esto se puede usar para expresar propiedades excepcionales en instan-
cias de una clase más genérica de objetos, dejando que los objetos no-
excepcionales tomen un valor estándar desde su prototipo.
También puedes sobreescribir para darle a los prototipos estándar de
función y array un método diferente toString al del objeto prototipo
básico.
console.log(Array.prototype.toString ==
Object.prototype.toString);
// → false
console.log([1, 2].toString());
// → 1,2
171
Mapas
Vimos a la palabra map usada en el capítulo anterior para una operación
que transforma una estructura de datos al aplicar una función en sus
elementos.
Un mapa (sustantivo) es una estructura de datos que asocia valores
(las llaves) con otros valores. Por ejemplo, es posible que desees mapear
nombres a edades. Es posible usar objetos para esto.
let edades = {
Boris: 39,
Liang: 22,
Júlia: 62
};
Aquí, los nombres de las propiedades del objeto son los nombres de las
personas, y los valores de las propiedades sus edades. Pero ciertamente
no incluimos a nadie llamado toString en nuestro mapa. Sin embargo,
debido a que los objetos simples se derivan de Object.prototype, parece
que la propiedad está ahí.
172
Como tal, usar objetos simples como mapas es peligroso. Hay varias
formas posibles de evitar este problema. Primero, es posible crear ob-
jetos sin ningun prototipo. Si pasas null a Object.create, el objeto re-
sultante no se derivará de Object.prototype y podra ser usado de forma
segura como un mapa.
console.log("toString" in Object.create(null));
// → false
173
Los métodos set (“establecer”),get (“obtener”), y has (“tiene”) son
parte de la interfaz del objeto Map. Escribir una estructura de datos que
pueda actualizarse rápidamente y buscar en un gran conjunto de valores
no es fácil, pero no tenemos que preocuparnos acerca de eso. Alguien
más lo hizo por nosotros, y podemos utilizar esta simple interfaz para
usar su trabajo.
Si tienes un objeto simple que necesitas tratar como un mapa por al-
guna razón, es útil saber que Object.keys solo retorna las llaves propias
del objeto, no las que estan en el prototipo. Como alternativa al oper-
ador in, puedes usar el métodohasOwnProperty (“tienePropiaPropiedad”),
el cual ignora el prototipo del objeto.
console.log({x: 1}.hasOwnProperty("x"));
// → true
console.log({x: 1}.hasOwnProperty("toString"));
// → false
Polimorfismo
Cuando llamas a la función String (que convierte un valor a un string)
en un objeto, llamará al método toString en ese objeto para tratar de
crear un string significativo a partir de el. Mencioné que algunos de
los prototipos estándar definen su propia versión de toString para que
puedan crear un string que contenga información más útil que "[object
Object]". También puedes hacer eso tú mismo.
174
Conejo.prototype.toString = function() {
return `un conejo ${this.tipo}`;
};
console.log(String(conejoNegro));
// → un conejo negro
Símbolos
Es posible que múltiples interfaces usen el mismo nombre de propiedad
para diferentes cosas. Por ejemplo, podría definir una interfaz en la que
175
se suponga que el método toString convierte el objeto a una pieza de
hilo. No sería posible para un objeto ajustarse a esa interfaz y al uso
estándar de toString.
Esa sería una mala idea, y este problema no es muy común. La
mayoria de los programadores de JavaScript simplemente no piensan
en eso. Pero los diseñadores del lenguaje, cuyo trabajo es pensar acerca
de estas cosas, nos han proporcionado una solución de todos modos.
Cuando afirmé que los nombres de propiedad son strings, eso no fue
del todo preciso. Usualmente lo son, pero también pueden ser símbolos.
Los símbolos son valores creados con la función Symbol. A diferencia de
los strings, los símbolos recién creados son únicos—no puedes crear el
mismo símbolo dos veces.
let simbolo = Symbol("nombre");
console.log(simbolo == Symbol("nombre"));
// → false
Conejo.prototype[simbolo] = 55;
console.log(conejoNegro[simbolo]);
// → 55
176
const simboloToString = Symbol("toString");
Array.prototype[simboloToString] = function() {
return `${this.length} cm de hilo azul`;
};
console.log([1, 2].toString());
// → 1,2
console.log([1, 2][simboloToString]());
// → 2 cm de hilo azul
La interfaz de iterador
Se espera que el objeto dado a un ciclo for/of sea iterable. Esto significa
que tenga un método llamado con el símbolo Symbol.iterator (un valor
177
de símbolo definido por el idioma, almacenado como una propiedad de
la función Symbol).
Cuando sea llamado, ese método debe retornar un objeto que propor-
cione una segunda interfaz, iteradora. Esta es la cosa real que realiza
la iteración. Tiene un método next (“siguiente”) que retorna el sigu-
iente resultado. Ese resultado debería ser un objeto con una propiedad
value (“valor”), que proporciona el siguiente valor, si hay uno, y una
propiedad done (“listo”) que debería ser cierta cuando no haya más
resultados y falso de lo contrario.
Ten en cuenta que los nombres de las propiedades next, value y done
son simples strings, no símbolos. Solo Symbol.iterator, que probable-
mente sea agregado a un monton de objetos diferentes, es un símbolo
real.
Podemos usar directamente esta interfaz nosotros mismos.
let iteradorOK = "OK"[Symbol.iterator]();
console.log(iteradorOK.next());
// → {value: "O", done: false}
console.log(iteradorOK.next());
// → {value: "K", done: false}
console.log(iteradorOK.next());
// → {value: undefined, done: true}
178
this.ancho = ancho;
this.altura = altura;
this.contenido = [];
obtener(x, y) {
return this.contenido[y * this.ancho + x];
}
establecer(x, y, valor) {
this.contenido[y * this.ancho + x] = valor;
}
}
179
la posición tanto de los elementos como de los elementos en sí mismos,
así que haremos que nuestro iterador produzca objetos con propiedades
x, y, y value (“valor”).
class IteradorMatriz {
constructor(matriz) {
this.x = 0;
this.y = 0;
this.matriz = matriz;
}
next() {
if (this.y == this.matriz.altura) return {done: true};
180
así, primero crea el objeto que contiene el valor actual y luego actualiza
su posición, moviéndose a la siguiente fila si es necesario.
Configuremos la clase Matriz para que sea iterable. A lo largo de este
libro, Ocasionalmente usaré la manipulación del prototipo después de
los hechos para agregar métodos a clases, para que las piezas individ-
uales de código permanezcan pequeñas y autónomas. En un programa
regular, donde no hay necesidad de dividir el código en pedazos pe-
queños, declararias estos métodos directamente en la clase.
Matriz.prototype[Symbol.iterator] = function() {
return new IteradorMatriz(this);
};
181
Getters, setters y estáticos
A menudo, las interfaces consisten principalmente de métodos, pero
también está bien incluir propiedades que contengan valores que no
sean de función. Por ejemplo, los objetos Map tienen una propiedad
size (“tamaño”) que te dice cuántas claves hay almacenanadas en ellos.
Ni siquiera es necesario que dicho objeto calcule y almacene tales
propiedades directamente en la instancia. Incluso las propiedades que
pueden ser accedidas directamente pueden ocultar una llamada a un
método. Tales métodos se llaman getters, y se definen escribiendo get
(“obtener”) delante del nombre del método en una expresión de objeto
o declaración de clase.
let tamañoCambiante = {
get tamaño() {
return Math.floor(Math.random() * 100);
}
};
console.log(tamañoCambiante.tamaño);
// → 73
console.log(tamañoCambiante.tamaño);
// → 49
182
class Temperatura {
constructor(celsius) {
this.celsius = celsius;
}
get fahrenheit() {
return this.celsius * 1.8 + 32;
}
set fahrenheit(valor) {
this.celsius = (valor - 32) / 1.8;
}
static desdeFahrenheit(valor) {
return new Temperatura((valor - 32) / 1.8);
}
}
183
tienen acceso a una instancia de clase, pero pueden, por ejemplo, ser
utilizados para proporcionar formas adicionales de crear instancias.
Dentro de una declaración de clase, métodos que tienen static (“estatico”
escrito antes su nombre son almacenados en el constructor. Entonces, la
clase Temperatura te permite escribir Temperature.desdeFahrenheit(100)
para crear una temperatura usando grados Fahrenheit.
Herencia
Algunas matrices son conocidas por ser simétricas. Si duplicas una
matriz simétrico alrededor de su diagonal de arriba-izquierda a derecha-
abajo, esta se mantiene igual. En otras palabras, el valor almacenado
en x,y es siempre el mismo al de y,x.
Imagina que necesitamos una estructura de datos como Matriz pero
que haga cumplir el hecho de que la matriz es y siga siendo simétrica.
Podríamos escribirla desde cero, pero eso implicaría repetir algo de
código muy similar al que ya hemos escrito.
El sistema de prototipos en JavaScript hace posible crear una nueva
clase, parecida a la clase anterior, pero con nuevas definiciones para
algunas de sus propiedades. El prototipo de la nueva clase deriva del
antiguo prototipo, pero agrega una nueva definición para, por ejemplo,
el método set.
En términos de programación orientada a objetos, esto se llama
herencia. La nueva clase hereda propiedades y comportamientos de
la vieja clase.
class MatrizSimetrica extends Matriz {
184
constructor(tamaño, elemento = (x, y) => undefined) {
super(tamaño, tamaño, (x, y) => {
if (x < y) return elemento(y, x);
else return elemento(x, y);
});
}
set(x, y, valor) {
super.set(x, y, valor);
if (x != y) {
super.set(y, x, valor);
}
}
}
El uso de la palabra extends indica que esta clase no debe estar basada
directamente en el prototipo de Objeto predeterminado, pero de alguna
otra clase. Esta se llama la superclase. La clase derivada es la subclase.
Para inicializar una instancia de MatrizSimetrica, el constructor llama
a su constructor de superclase a través de la palabra clave super. Esto es
necesario porque si este nuevo objeto se comporta (más o menos) como
una Matriz, va a necesitar las propiedades de instancia que tienen las
matrices. En orden para asegurar que la matriz sea simétrica, el con-
structor ajusta el método contenido para intercambiar las coordenadas
185
de los valores por debajo del diagonal.
El método set nuevamente usa super, pero esta vez no para llamar
al constructor, pero para llamar a un método específico del conjunto
de metodos de la superclase. Estamos redefiniendo set pero queremos
usar el comportamiento original. Ya que this.set se refiere al nuevo
métodoset, llamarlo no funcionaria. Dentro de los métodos de clase,
super proporciona una forma de llamar a los métodos tal y como se
definieron en la superclase.
La herencia nos permite construir tipos de datos ligeramente difer-
entes a partir de tipos de datos existentes con relativamente poco tra-
bajo. Es una parte fundamental de la tradición orientada a objetos,
junto con la encapsulación y el polimorfismo. Pero mientras que los
últimos dos son considerados como ideas maravillosas en la actualidad,
la herencia es más controversial.
Mientras que la encapsulación y el polimorfismo se pueden usar para
separar piezas de código entre sí, reduciendo el enredo del programa
en general, la herencia fundamentalmente vincula las clases, creando
mas enredo. Al heredar de una clase, generalmente tienes que saber
más sobre cómo funciona que cuando simplemente la usas. La heren-
cia puede ser una herramienta útil, y la uso de vez en cuando en mis
propios programas, pero no debería ser la primera herramienta que
busques, y probablemente no deberías estar buscando oportunidades
para construir jerarquías (árboles genealógicos de clases) de clases en
una manera activa.
186
El operador instanceof
Ocasionalmente es útil saber si un objeto fue derivado de una clase es-
pecífica. Para esto, JavaScript proporciona un operador binario llamado
instanceof (“instancia de”).
console.log(
new MatrizSimetrica(2) instanceof MatrizSimetrica);
// → true
console.log(new MatrizSimetrica(2) instanceof Matriz);
// → true
console.log(new Matriz(2, 2) instanceof MatrizSimetrica);
// → false
console.log([1] instanceof Array);
// → true
Resumen
Entonces los objetos hacen más que solo tener sus propias propiedades.
Ellos tienen prototipos, que son otros objetos. Estos actuarán como
si tuvieran propiedades que no tienen mientras su prototipo tenga esa
propiedad. Los objetos simples tienen Object.prototype como su pro-
187
totipo.
Los constructores, que son funciones cuyos nombres generalmente
comienzan con una mayúscula, se pueden usar con el operador new para
crear nuevos objetos. El prototipo del nuevo objeto será el objeto en-
contrado en la propiedad prototype del constructor. Puedes hacer un
buen uso de esto al poner las propiedades que todos los valores de un
tipo dado comparten en su prototipo. Hay una notación de class que
proporciona una manera clara de definir un constructor y su prototipo.
Puedes definir getters y setters para secretamente llamar a métodos
cada vez que se acceda a la propiedad de un objeto. Los métodos
estáticos son métodos almacenados en el constructor de clase, en lugar
de su prototipo.
El operador instanceof puede, dado un objeto y un constructor, decir
si ese objeto es una instancia de ese constructor.
Una cosa útil que hacer con los objetos es especificar una interfaz para
ellos y decirle a todos que se supone que deben hablar con ese objeto
solo a través de esa interfaz. El resto de los detalles que componen tu
objeto ahora estan encapsulados, escondidos detrás de la interfaz.
Más de un tipo puede implementar la misma interfaz. El código es-
crito para utilizar una interfaz automáticamente sabe cómo trabajar con
cualquier cantidad de objetos diferentes que proporcionen la interfaz.
Esto se llama polimorfismo.
Al implementar múltiples clases que difieran solo en algunos detalles,
puede ser útil escribir las nuevas clases como subclases de una clase
existente, heredando parte de su comportamiento.
188
Ejercicios
Un tipo vector
Escribe una clase Vec que represente un vector en un espacio de dos di-
mensiones. Toma los parámetros (numericos) x y y, que debería guardar
como propiedades del mismo nombre.
Dale al prototipo de Vector dos métodos, mas y menos, los cuales toman
otro vector como parámetro y retornan un nuevo vector que tiene la
suma o diferencia de los valores x y y de los dos vectores (this y el
parámetro).
Agrega una propiedad getter llamada longitud al prototipo que cal-
cule la longitud del vector—es decir, la distancia del punto (x, y) desde
el origen (0, 0).
Conjuntos
El entorno de JavaScript estándar proporciona otra estructura de datos
llamada Set (“Conjunto”). Al igual que una instancia de Map, un con-
junto contiene una colección de valores. Pero a diferencia de Map, este no
asocia valores con otros—este solo rastrea qué valores son parte del con-
junto. Un valor solo puede ser parte de un conjunto una vez—agregarlo
de nuevo no tiene ningún efecto.
Escribe una clase llamada Conjunto. Como Set, debe tener los méto-
dos add (“añadir”), delete (“eliminar”), y has (“tiene”). Su constructor
crea un conjunto vacío, añadir agrega un valor al conjunto (pero solo
si no es ya un miembro), eliminar elimina su argumento del conjunto
189
(si era un miembro) y tiene retorna un valor booleano que indica si su
argumento es un miembro del conjunto.
Usa el operador ===, o algo equivalente como indexOf, para determinar
si dos valores son iguales.
Proporcionale a la clase un método estático desde que tome un objeto
iterable como argumento y cree un grupo que contenga todos los valores
producidos al iterar sobre el.
Conjuntos Iterables
Haz iterable la clase Conjunto del ejercicio anterior. Puedes remitirte a
la sección acerca de la interfaz del iterador anteriormente en el capítulo
si ya no recuerdas muy bien la forma exacta de la interfaz.
Si usaste un array para representar a los miembros del conjunto, no
solo retornes el iterador creado llamando al método Symbol.iterator en
el array. Eso funcionaría, pero frustra el propósito de este ejercicio.
Está bien si tu iterador se comporta de manera extraña cuando el
conjunto es modificado durante la iteración.
190
¿Puedes pensar en una forma de llamar hasOwnProperty en un objeto
que tiene una propia propiedad con ese nombre?
191
“[...] la pregunta de si las Maquinas Pueden Pensar [...] es
tan relevante como la pregunta de si los Submarinos Pueden
Nadar.”
—Edsger Dijkstra, The Threats to Computing Science
Chapter 7
Proyecto: Un Robot
En los capítulos de “proyectos”, dejaré de golpearte con teoría nueva
por un breve momento y en su lugar vamos a trabajar juntos en un
programa. La teoría es necesaria para aprender a programar, pero leer
y entender programas reales es igual de importante.
Nuestro proyecto en este capítulo es construir un autómata, un pe-
queño programa que realiza una tarea en un mundo virtual. Nuestro
autómata será un robot de entregas por correo que recoge y deja pa-
quetes.
VillaPradera
El pueblo de VillaPradera no es muy grande. Este consiste de 11 lugares
con 14 caminos entre ellos. Puede ser describido con este array de
caminos:
const caminos = [
"Casa de Alicia-Casa de Bob", "Casa de Alicia-Cabaña",
192
"Casa de Alicia-Oficina de Correos", "Casa de Bob-Ayuntamiento
",
"Casa de Daria-Casa de Ernie", "Casa de Daria-
Ayuntamiento",
"Casa de Ernie-Casa de Grete", "Casa de Grete-Granja",
"Casa de Grete-Tienda", "Mercado-Granja",
"Mercado-Oficina de Correos", "Mercado-Tienda",
"Mercado-Ayuntamiento", "Tienda-Ayuntamiento"
];
193
Este grafo será el mundo por el que nuestro robot se movera.
El array de strings no es muy fácil de trabajar. En lo que estamos
interesados es en los destinos a los que podemos llegar desde un lugar
determinado. Vamos a convertir la lista de caminos en una estructura
de datos que, para cada lugar, nos diga a donde se pueda llegar desde
allí.
function construirGrafo(bordes) {
let grafo = Object.create(null);
function añadirBorde(desde, hasta) {
if (grafo[desde] == null) {
grafo[desde] = [hasta];
} else {
grafo[desde].push(hasta);
}
}
for (let [desde, hasta] of bordes.map(c => c.split("-"))) {
añadirBorde(desde, hasta);
añadirBorde(hasta, desde);
}
return grafo;
}
194
la forma "Comienzo-Final", a arrays de dos elementos que contienen el
inicio y el final como strings separados.
La tarea
Nuestro robot se moverá por el pueblo. Hay paquetes en varios lugares,
cada uno dirigido a otro lugar. El robot tomara paquetes cuando los
encuentre y los entregara cuando llegue a sus destinos.
El autómata debe decidir, en cada punto, a dónde ir después. Ha
finalizado su tarea cuando se han entregado todos los paquetes.
Para poder simular este proceso, debemos definir un mundo virtual
que pueda describirlo. Este modelo nos dice dónde está el robot y dónde
estan los paquetes. Cuando el robot ha decidido moverse a alguna parte,
necesitamos actualizar el modelo para reflejar la nueva situación.
Si estás pensando en términos de programación orientada a objetos,
tu primer impulso podría ser comenzar a definir objetos para los di-
versos elementos en el mundo. Una clase para el robot, una para un
paquete, tal vez una para los lugares. Estas podrían tener propiedades
que describen su estado actual, como la pila de paquetes en un lugar,
que podríamos cambiar al actualizar el mundo.
Esto está mal.
Al menos, usualmente lo esta. El hecho de que algo suena como
un objeto no significa automáticamente que debe ser un objeto en tu
programa. Escribir por reflejo las clases para cada concepto en tu apli-
cación tiende a dejarte con una colección de objetos interconectados
donde cada uno tiene su propio estado interno y cambiante. Tales pro-
195
gramas a menudo son difíciles de entender y, por lo tanto, fáciles de
romper.
En lugar de eso, condensemos el estado del pueblo hasta el mínimo
conjunto de valores que lo definan. Está la ubicación actual del robot
y la colección de paquetes no entregados, cada uno de los cuales tiene
una ubicación actual y una dirección de destino. Eso es todo.
Y mientras estamos en ello, hagámoslo de manera que no cambiemos
este estado cuándo se mueva el robot, sino calcular un nuevo estado
para la situación después del movimiento.
class EstadoPueblo {
constructor(lugar, paquetes) {
this.lugar = lugar;
this.paquetes = paquetes;
}
mover(destino) {
if (!grafoCamino[this.lugar].includes(destino)) {
return this;
} else {
let paquetes = this.paquetes.map(p => {
if (p.lugar != this.lugar) return p;
return {lugar: destino, direccion: p.direccion};
}).filter(p => p.lugar != p.direccion);
return new EstadoPueblo(destino, paquetes);
}
}
}
196
En el método mover es donde ocurre la acción. Este primero verifica
si hay un camino que va del lugar actual al destino, y si no, retorna el
estado anterior, ya que este no es un movimiento válido.
Luego crea un nuevo estado con el destino como el nuevo lugar del
robot. Pero también necesita crear un nuevo conjunto de paquetes—
los paquetes que el robot esta llevando (que están en el lugar actual
del robot) necesitan de moverse tambien al nuevo lugar. Y paquetes
que están dirigidos al nuevo lugar donde deben de ser entregados—es
decir, deben de eliminarse del conjunto de paquetes no entregados. La
llamada a map se encarga de mover los paquetes, y la llamada a filter
hace la entrega.
Los objetos de paquete no se modifican cuando se mueven, sino que
se vuelven a crear. El método movee nos da un nuevo estado de aldea,
pero deja el viejo completamente intacto
let primero = new EstadoPueblo(
"Oficina de Correos",
[{lugar: "Oficina de Correos", direccion: "Casa de Alicia"}]
);
let siguiente = primero.mover("Casa de Alicia");
console.log(siguiente.lugar);
// → Casa de Alicia
console.log(siguiente.parcels);
// → []
console.log(primero.lugar);
// → Oficina de Correos
197
Mover hace que se entregue el paquete, y esto se refleja en el próximo
estado. Pero el estado inicial todavía describe la situación donde el
robot está en la oficina de correos y el paquete aun no ha sido entregado.
Datos persistentes
Las estructuras de datos que no cambian se llaman inmutables o per-
sistentes. Se comportan de manera muy similar a los strings y números
en que son quienes son, y se quedan así, en lugar de contener diferentes
cosas en diferentes momentos.
En JavaScript, casi todo puede ser cambiado, así que trabajar con
valores que se supone que sean persistentes requieren cierta restricción.
Hay una función llamada Object.freeze (“Objeto.congelar”) que cambia
un objeto de manera que escribir en sus propiedades sea ignorado. Po-
drías usar eso para asegurarte de que tus objetos no cambien, si quieres
ser cuidadoso. La congelación requiere que la computadora haga un
trabajo extra e ignorar actualizaciones es probable que confunda a al-
guien tanto como para que hagan lo incorrecto. Por lo general, prefiero
simplemente decirle a la gente que un determinado objeto no debe ser
molestado, y espero que lo recuerden.
let objeto = Object.freeze({valor: 5});
objeto.valor = 10;
console.log(objeto.valor);
// → 5
198
lenguaje obviamente está esperando que lo haga?
Porque me ayuda a entender mis programas. Esto es acerca de mane-
jar la complejidad nuevamente. Cuando los objetos en mi sistema son
cosas fijas y estables, puedo considerar las operaciones en ellos de forma
aislada—moverse a la casa de Alicia desde un estado de inicio siempre
produce el mismo nuevo estado. Cuando los objetos cambian con el
tiempo, eso agrega una dimensión completamente nueva de compleji-
dad a este tipo de razonamiento.
Para un sistema pequeño como el que estamos construyendo en este
capítulo, podriamos manejar ese poco de complejidad adicional. Pero
el límite más importante sobre qué tipo de sistemas podemos construir
es cuánto podemos entender. Cualquier cosa que haga que tu código
sea más fácil de entender hace que sea posible construir un sistema más
ambicioso.
Lamentablemente, aunque entender un sistema basado en estructuras
de datos persistentes es más fácil, diseñar uno, especialmente cuando
tu lenguaje de programación no ayuda, puede ser un poco más difícil.
Buscaremos oportunidades para usar estructuras de datos persistentes
en este libro, pero también utilizaremos las modificables.
Simulación
Un robot de entregas mira al mundo y decide en qué dirección que quiere
moverse. Como tal, podríamos decir que un robot es una función que
toma un objeto EstadoPueblo y retorna el nombre de un lugar cercano.
Ya que queremos que los robots sean capaces de recordar cosas, para
199
que puedan hacer y ejecutar planes, también les pasamos su memoria
y les permitimos retornar una nueva memoria. Por lo tanto, lo que
retorna un robot es un objeto que contiene tanto la dirección en la que
quiere moverse como un valor de memoria que se le sera regresado la
próxima vez que se llame.
function correrRobot(estado, robot, memoria) {
for (let turno = 0;; turno++) {
if (estado.paquetes.length == 0) {
console.log(`Listo en ${turno} turnos`);
break;
}
let accion = robot(estado, memoria);
estado = estado.mover(accion.direccion);
memoria = accion.memoria;
console.log(`Moverse a ${accion.direccion}`);
}
}
200
Aqui esta como se podria ver eso:
function eleccionAleatoria(array) {
let eleccion = Math.floor(Math.random() * array.length);
return array[eleccion];
}
function robotAleatorio(estado) {
return {direccion: eleccionAleatoria(grafoCamino[estado.lugar])
};
}
201
let direccion = eleccionAleatoria(Object.keys(grafoCamino));
let lugar;
do {
lugar = eleccionAleatoria(Object.keys(grafoCamino));
} while (lugar == direccion);
paquetes.push({lugar, direccion});
}
return new EstadoPueblo("Oficina de Correos", paquetes);
};
202
La ruta del camión de correos
Deberíamos poder hacer algo mucho mejor que el robot aleatorio. Una
mejora fácil sería tomar una pista de la forma en que como funciona
la entrega de correos en el mundo real. Si encontramos una ruta que
pasa por todos los lugares en el pueblo, el robot podría ejecutar esa
ruta dos veces, y en ese punto esta garantizado que ha entregado todos
los paquetes. Aquí hay una de esas rutas (comenzando desde la Oficina
de Correos).
const rutaCorreo = [
"Casa de Alicia", "Cabaña", "Casa de Alicia", "Casa de Bob",
"Ayuntamiento", "Casa de Daria", "Casa de Ernie",
"GCasa de Grete", "Tienda", "Casa de Grete", "Granja",
"Mercado", "Oficina de Correos"
];
203
Este robot ya es mucho más rápido. Tomará un máximo de 26 turnos
(dos veces la ruta de 13 pasos), pero generalmente seran menos.
Búsqueda de rutas
Aún así, realmente no llamaría seguir ciegamente una ruta fija compor-
tamiento inteligente. El robot podría funcionar más eficientemente si
ajustara su comportamiento al trabajo real que necesita hacerse.
Para hacer eso, tiene que ser capaz de avanzar deliberadamente hacia
un determinado paquete, o hacia la ubicación donde se debe entregar
un paquete. Al hacer eso, incluso cuando el objetivo esté a más de un
movimiento de distancia, requiere algún tipo de función de búsqueda
de ruta.
El problema de encontrar una ruta a través de un grafo es un típico
problema de búsqueda. Podemos decir si una solución dada (una ruta) es
una solución válida, pero no podemos calcular directamente la solución
de la misma manera que podríamos para 2 + 2. En cambio, tenemos
que seguir creando soluciones potenciales hasta que encontremos una
que funcione.
El número de rutas posibles a través de un grafo es infinito. Pero
cuando buscamos una ruta de A a B, solo estamos interesados en aque-
llas que comienzan en A. Tampoco nos importan las rutas que visitan
el mismo lugar dos veces, definitivamente esa no es la ruta más efi-
ciente en cualquier sitio. Entonces eso reduce la cantidad de rutas que
el buscador de rutas tiene que considerar.
De hecho, estamos más interesados en la ruta mas corta. Entonces
204
queremos asegurarnos de mirar las rutas cortas antes de mirar las más
largas. Un buen enfoque sería “crecer” las rutas desde el punto de
partida, explorando cada lugar accesible que aún no ha sido visitado,
hasta que una ruta alcanze la meta. De esa forma, solo exploraremos
las rutas que son potencialmente interesantes, y encontremos la ruta
más corta (o una de las rutas más cortas, si hay más de una) a la meta.
Aquí hay una función que hace esto:
function encontrarRuta(grafo, desde, hasta) {
let trabajo = [{donde: desde, ruta: []}];
for (let i = 0; i < trabajo.length; i++) {
let {donde, ruta} = trabajo[i];
for (let lugar of grafo[donde]) {
if (lugar == hasta) return ruta.concat(lugar);
if (!trabajo.some(w => w.donde == lugar)) {
trabajo.push({donde: lugar, ruta: ruta.concat(lugar)});
}
}
}
}
205
array de lugares que deberían explorarse a continuación, junto con la
ruta que nos llevó ahí. Esta comienza solo con la posición de inicio y
una ruta vacía.
La búsqueda luego opera tomando el siguiente elemento en la lista y
explorando eso, lo que significa que todos los caminos que van desde ese
lugar son mirados. Si uno de ellos es el objetivo, una ruta final puede
ser retornada. De lo contrario, si no hemos visto este lugar antes, un
nuevo elemento se agrega a la lista. Si lo hemos visto antes, ya que
estamos buscando primero rutas cortas, hemos encontrado una ruta
más larga a ese lugar o una precisamente tan larga como la existente,
y no necesitamos explorarla.
Puedes imaginar esto visualmente como una red de rutas conocidas
que se arrastran desde el lugar de inicio, creciendo uniformemente hacia
todos los lados (pero nunca enredándose de vuelta a si misma). Tan
pronto como el primer hilo llegue a la ubicación objetivo, ese hilo se
remonta al comienzo, dándonos asi nuestra ruta.
Nuestro código no maneja la situación donde no hay más elementos
de trabajo en la lista de trabajo, porque sabemos que nuestro gráfico
está conectado, lo que significa que se puede llegar a todos los lugares
desde todos los otros lugares. Siempre podremos encontrar una ruta
entre dos puntos, y la búsqueda no puede fallar.
function robotOrientadoAMetas({lugar, paquetes}, ruta) {
if (ruta.length == 0) {
let paquete = paquetes[0];
if (paquete.lugar != lugar) {
ruta = encontrarRuta(grafoCamino, lugar, paquete.lugar);
} else {
206
ruta = encontrarRuta(grafoCamino, lugar, paquete.direccion)
;
}
}
return {direccion: ruta[0], memoria: ruta.slice(1)};
}
Ejercicios
Midiendo un robot
Es difícil comparar objetivamente robots simplemente dejándolos re-
solver algunos escenarios. Tal vez un robot acaba de conseguir tareas
más fáciles, o el tipo de tareas en las que es bueno, mientras que el otro
no.
Escribe una función compararRobots que toma dos robots (y su memo-
207
ria de inicio). Debe generar 100 tareas y dejar que cada uno de los robots
resuelvan cada una de estas tareas. Cuando terminen, debería generar
el promedio de pasos que cada robot tomó por tarea.
En favor de lo que es justo, asegúrate de la misma tarea a ambos
robots, en lugar de generar diferentes tareas por robot.
Conjunto persistente
La mayoría de las estructuras de datos proporcionadas en un entorno de
JavaScript estándar no son muy adecuadas para usos persistentes. Los
arrays tienen los métodos slice y concat, que nos permiten fácilmente
crear nuevos arrays sin dañar al anterior. Pero Set, por ejemplo, no
tiene métodos para crear un nuevo conjunto con un elemento agregado
o eliminado.
Escribe una nueva clase ConjuntoP, similar a la clase Conjunto del
Capitulo 6, que almacena un conjunto de valores. Como Grupo, tiene
métodos añadir, eliminar, y tiene.
Su método añadir, sin embargo, debería retornar una nueva instancia
de ConjuntoP con el miembro dado agregado, y dejar la instancia anterior
208
sin cambios. Del mismo modo, eliminar crea una nueva instancia sin
un miembro dado.
La clase debería funcionar para valores de cualquier tipo, no solo
strings. Esta no tiene que ser eficiente cuando se usa con grandes can-
tidades de valores.
El constructor no deberia ser parte de la interfaz de la clase (aunque
definitivamente querrás usarlo internamente). En cambio, allí hay una
instancia vacía, ConjuntoP.vacio, que se puede usar como un valor de
inicio.
Por qué solo necesitas un valor ConjuntoP.vacio, en lugar de tener
una función que crea un nuevo mapa vacío cada vez?
209
“Arreglar errores es dos veces mas difícil que escribir el código
en primer lugar. Por lo tanto, si escribes código de la manera
más inteligente posible, eres, por definición, no lo
suficientemente inteligente como para depurarlo.”
—Brian Kernighan and P.J. Plauger, The Elements of
Programming Style
Chapter 8
Bugs y Errores
Los defectos en los programas de computadora usualmente se llaman
bugs (o “insectos”). Este nombre hace que los programadores se sientan
bien al imaginarlos como pequeñas cosas que solo sucede se arrastran
hacia nuestro trabajo. En la realidad, por supuesto, nosotros mismos
los ponemos allí.
Si un programa es un pensamiento cristalizado, puedes categorizar
en grandes rasgos a los bugs en aquellos causados al confundir los
pensamientos, y los causados por cometer errores al convertir un pen-
samiento en código. El primer tipo es generalmente más difícil de di-
agnosticar y corregir que el último.
Lenguaje
Muchos errores podrían ser señalados automáticamente por la com-
putadora, si esta supiera lo suficiente sobre lo que estamos tratando de
hacer. Pero aquí la soltura de JavaScript es un obstáculo. Su concepto
de vinculaciones y propiedades es lo suficientemente vago que rara vez
210
atrapará errores ortograficos antes de ejecutar el programa. E incluso
entonces, te permite hacer algunas cosas claramente sin sentido, como
calcular true * "mono".
Hay algunas cosas de las que JavaScript se queja. Escribir un pro-
grama que no siga la gramática del lenguaje inmediatamente hara que
la computadora se queje. Otras cosas, como llamar a algo que no sea
una función o buscar una propiedad en un valor indefinido, causará un
error que sera reportado cuando el programa intente realizar la acción.
Pero a menudo, tu cálculo sin sentido simplemente producirá NaN (no
es un número) o un valor indefinido. Y el programa continuara feliz-
mente, convencido de que está haciendo algo significativo. El error solo
se manifestara más tarde, después de que el valor falso haya viajado a
traves de varias funciones. Puede no desencadenar un error en abso-
luto, pero en silencio causara que la salida del programa sea incorrecta.
Encontrar la fuente de tales problemas puede ser algo difícil.
El proceso de encontrar errores—bugs—en los programas se llama
depuración.
Modo estricto
JavaScript se puede hacer un poco más estricto al habilitar el modo
estricto. Esto se hace al poner el string "use strict" (“usar estricto”)
en la parte superior de un archivo o cuerpo de función. Aquí hay un
ejemplo:
function puedesDetectarElProblema() {
"use strict";
211
for (contador = 0; contador < 10; contador++) {
console.log("Feliz feliz");
}
}
puedesDetectarElProblema();
// → ReferenceError: contador is not defined
212
function Persona(nombre) { this.nombre = nombre; }
let ferdinand = Persona("Ferdinand"); // oops
console.log(nombre);
// → Ferdinand
213
Tipos
Algunos lenguajes quieren saber los tipos de todas tus vinculaciones y
expresiones incluso antes de ejecutar un programa. Estos te dirán de
una vez cuando uses un tipo de una manera inconsistente. JavaScript
solo considera a los tipos cuando ejecuta el programa, e incluso a
menudo intentara convertir implícitamente los valores al tipo que es-
pera, por lo que no es de mucha ayuda
Aún así, los tipos proporcionan un marco útil para hablar acerca de
los programas. Muchos errores provienen de estar confundido acerca del
tipo de valor que entra o sale de una función. Si tienes esa información
anotada, es menos probable que te confundas.
Podrías agregar un comentario como arriba de la función robotOrientadoA
del último capítulo, para describir su tipo.
// (EstadoMundo, Array) → {direccion: string, memoria: Array}
function robotOrientadoAMetas(estado, memoria) {
// ...
}
214
a eleccionAleatoria un tipo como ([T])→ T (función de un array de Ts
a a T).
Cuando se conocen los tipos de un programa, es posible que la com-
putadora haga un chequeo por ti, señalando los errores antes de que el
programa sea ejecutado. Hay varios dialectos de JavaScript que agregan
tipos al lenguaje y y los verifica. El más popular se llama TypeScript. Si
estás interesado en agregarle más rigor a tus programas, te recomiendo
que lo pruebes.
En este libro, continuaremos usando código en JavaScript crudo, peli-
groso y sin tipos.
Probando
Si el lenguaje no va a hacer mucho para ayudarnos a encontrar errores,
tendremos que encontrarlos de la manera difícil: ejecutando el programa
y viendo si hace lo correcto.
Hacer esto a mano, una y otra vez, es una muy mala idea. No solo
es es molesto, también tiende a ser ineficaz, ya que lleva demasiado
tiempo probar exhaustivamente todo cada vez que haces un cambio en
tu programa.
Las computadoras son buenas para las tareas repetitivas, y las prue-
bas son las tareas repetitivas ideales. Las pruebas automatizadas es el
proceso de escribir un programa que prueba otro programa. Escribir
pruebas consiste en algo más de trabajo que probar manualmente, pero
una vez que lo haz hecho, ganas un tipo de superpoder: solo te tomara
unos segundos verificar que tu programa todavía se comporta correcta-
215
mente en todas las situaciones para las que escribiste tu prueba. Cuando
rompas algo, lo notarás inmediatamente, en lugar aleatoriomente encon-
trarte con el problema en algún momento posterior.
Las pruebas usualmente toman la forma de pequeños programas eti-
quetados que verifican algún aspecto de tu código. Por ejemplo, un con-
junto de pruebas para el método (estándar, probablemente ya probado
por otra persona) toUpperCase podría verse así:
function probar(etiqueta, cuerpo) {
if (!cuerpo()) console.log(`Fallo: ${etiqueta}`);
}
216
pruebas.
Algunos programas son más fáciles de probar que otros programas.
Por lo general, con cuantos más objetos externos interactúe el código,
más difícil es establecer el contexto en el cual probarlo. El estilo de
programación mostrado en el capítulo anterior, que usa valores persis-
tentes auto-contenidos en lugar de cambiar objetos, tiende a ser fácil
de probar.
Depuración
Una vez que notes que hay algo mal con tu programa porque se com-
porta mal o produce errores, el siguiente paso es descubir cual es el
problema.
A veces es obvio. El mensaje de error apuntará a una línea específica
de tu programa, y si miras la descripción del error y esa línea de código,
a menudo puedes ver el problema.
Pero no siempre. A veces, la línea que provocó el problema es simple-
mente el primer lugar en donde un valor extraño producido en otro lugar
es usado de una manera inválida. Si has estado resolviendo los ejercicios
en capítulos anteriores, probablemente ya habrás experimentado tales
situaciones.
El siguiente programa de ejemplo intenta convertir un número entero
a un string en una base dada (decimal, binario, etc.) al repetidamente
seleccionar el último dígito y luego dividiendo el número para deshacerse
de este dígito. Pero la extraña salida que produce sugiere que tiene un
error.
217
function numeroAString(n, base = 10) {
let resultado = "", signo = "";
if (n < 0) {
signo = "-";
n = -n;
}
do {
resultado = String(n % base) + resultado;
n /= base;
} while (n > 0);
return signo + resultado;
}
console.log(numeroAString(13, 10));
// → 1.5e-3231.3e-3221.3e-3211.3e-3201.3e-3191.3e…-3181.3
218
13
1.3
0.13
0.013…
1.5e-323
219
Propagación de errores
Desafortunadamente, no todos los problemas pueden ser prevenidos por
el programador. Si tu programa se comunica con el mundo exterior de
alguna manera, es posible obtener una entrada malformada, sobrecar-
garse con el trabajo, o la red falle en la ejecución.
Si solo estás programando para ti mismo, puedes permitirte ignorar
tales problemas hasta que estos ocurran. Pero si construyes algo que
va a ser utilizado por cualquier otra persona, generalmente quieres que
el programa haga algo mejor que solo estrellarse. A veces lo correcto
es tomar la mala entrada en zancada y continuar corriendo. En otros
casos, es mejor informar al usuario lo que salió mal y luego darse por
vencido. Pero en cualquier situación, el programa tiene que hacer algo
activamente en respuesta al problema.
Supongamos que tienes una función pedirEntero que le pide al usuario
un número entero y lo retorna. Qué deberías retornar si la entrada por
parte del usuario es “naranja”?
Una opción es hacer que retorne un valor especial. Opciones comunes
para tales valores son null, undefined, o -1.
function pedirEntero(pregunta) {
let resultado = Number(prompt(pregunta));
if (Number.isNaN(resultado)) return null;
else return resultado;
}
220
Ahora cualquier código que llame a pedirEntero debe verificar si un
número real fue leído y, si eso falla, de alguna manera debe recuperarse—
tal vez preguntando nuevamente o usando un valor predeterminado. O
podría de nuevo retornar un valor especial a su llamada para indicar
que no pudo hacer lo que se pidió.
En muchas situaciones, principalmente cuando los errores son co-
munes y la persona que llama debe tenerlos explícitamente en cuenta,
retornar un valor especial es una buena forma de indicar un error. Sin
embargo, esto tiene sus desventajas. Primero, qué pasa si la función
puede retornar cada tipo de valor posible? En tal función, tendrás que
hacer algo como envolver el resultado en un objeto para poder distinguir
el éxito del fracaso.
function ultimoElemento(array) {
if (array.length == 0) {
return {fallo: true};
} else {
return {elemento: array[array.length - 1]};
}
}
221
verificarlo, y así sucesivamente.
Excepciones
Cuando una función no puede continuar normalmente, lo que nos gus-
taría hacer es simplemente detener lo que estamos haciendo e inmedi-
atamente saltar a un lugar que sepa cómo manejar el problema. Esto
es lo que el manejo de excepciones hace.
Las excepciones son un mecanismo que hace posible que el código
que se encuentre con un problema produzca (o lance) una excepción.
Una excepción puede ser cualquier valor. Producir una se asemeja a un
retorno súper-cargado de una función: salta no solo de la función actual
sino también fuera de sus llamadores, todo el camino hasta la primera
llamada que comenzó la ejecución actual. Esto se llama desenrollando
la pila. Puede que recuerdes que la pila de llamadas de función fue
mencionada en el Capítulo 3. Una excepción se aleja de esta pila,
descartando todos los contextos de llamadas que encuentra.
Si las excepciones siempre se acercaran al final de la pila, estas no
serían de mucha utilidad. Simplemente proporcionarían una nueva
forma de explotar tu programa. Su poder reside en el hecho de que
puedes establecer “obstáculos” a lo largo de la pila para capturar la ex-
cepción, cuando esta
esta se dirige hacia abajo. Una vez que hayas capturado una excepción,
puedes hacer algo con ella para abordar el problema y luego continuar
ejecutando el programa.
Aquí hay un ejemplo:
222
function pedirDireccion(pregunta) {
let resultado = prompt(pregunta);
if (resultado.toLowerCase() == "izquierda") return "I";
if (resultado.toLowerCase() == "derecha") return "D";
throw new Error("Dirección invalida: " + resultado);
}
function mirar() {
if (pedirDireccion("Hacia que dirección quieres ir?") == "I") {
return "una casa";
} else {
return "dos osos furiosos";
}
}
try {
console.log("Tu ves", mirar());
} catch (error) {
console.log("Algo incorrecto sucedio: " + error);
}
223
toda la declaración try/catch.
En este caso, usamos el constructor Error para crear nuestro valor
de excepción. Este es un constructor (estándar) de JavaScript que crea
un objeto con una propiedad message (“mensaje”). En la mayoría de
los entornos de JavaScript, las instancias de este constructor también
recopilan información sobre la pila de llamadas que existía cuando se
creó la excepción, algo llamado seguimiento de la pila. Esta información
se almacena en la propiedad stack (“pila”) y puede ser útil al intentar
depurar un problema: esta nos dice la función donde ocurrió el problema
y qué funciones realizaron la llamada fallida.
Ten en cuenta que la función mirar ignora por completo la posibilidad
de que pedirDireccion podría salir mal. Esta es la gran ventaja de las
excepciones: el código de manejo de errores es necesario solamente en
el punto donde el error ocurre y en el punto donde se maneja. Las
funciones en el medio puede olvidarse de todo.
Bueno, casi...
224
Aquí hay un código bancario realmente malo.
const cuentas = {
a: 100,
b: 0,
c: 20
};
function obtenerCuenta() {
let nombreCuenta = prompt("Ingrese el nombre de la cuenta");
if (!cuentas.hasOwnProperty(nombreCuenta)) {
throw new Error(`La cuenta "${nombreCuenta}" no existe`);
}
return nombreCuenta;
}
225
desaparezca.
Ese código podría haber sido escrito de una manera un poco más in-
teligente, por ejemplo al llamar obtenerCuenta antes de que se comience
a mover el dinero. Pero a menudo problemas como este ocurren de
maneras más sutiles. Incluso funciones que no parece que lanzarán
una excepción podría hacerlo en circunstancias excepcionales o cuando
contienen un error de programador.
Una forma de abordar esto es usar menos efectos secundarios. De
nuevo, un estilo de programación que calcula nuevos valores en lugar
de cambiar los datos existentes ayuda. Si un fragmento de código deja
de ejecutarse en el medio de crear un nuevo valor, nadie ve el valor a
medio terminar, y no hay ningún problema.
Pero eso no siempre es práctico. Entonces, hay otra característica que
las declaraciones try tienen. Estas pueden ser seguidas por un bloque
finally (“finalmente”) en lugar de o además de un bloque catch. Un
bloque finally dice “no importa lo que pase, ejecuta este código después
de intentar ejecutar el código en el bloque try.”
function transferir(desde, cantidad) {
if (cuentas[desde] < cantidad) return;
let progreso = 0;
try {
cuentas[desde] -= cantidad;
progreso = 1;
cuentas[obtenerCuenta()] += cantidad;
progreso = 2;
} finally {
if (progreso == 1) {
226
cuentas[desde] += cantidad;
}
}
}
Captura selectiva
Cuando una excepción llega hasta el final de la pila sin ser capturada,
esta es manejada por el entorno. Lo que esto significa difiere entre
los entornos. En los navegadores, una descripción del error general-
mente sera escrita en la consola de JavaScript (accesible a través de las
herramientas de desarrollador del navegador). Node.js, el entorno de
JavaScript sin navegador que discutiremos en el Capítulo 20, es más
227
cuidadoso con la corrupción de datos. Aborta todo el proceso cuando
ocurre una excepción no manejada.
Para los errores de programador, solo dejar pasar al error es a menudo
lo mejor que puedes hacer. Una excepción no manejada es una forma
razonable de señalizar un programa roto, y la consola de JavaScript, en
los navegadores moderno, te proporcionan cierta información acerca de
qué llamdas de función estaban en la pila cuando ocurrió el problema.
Para problemas que se espera que sucedan durante el uso rutinario,
estrellarse con una excepción no manejada es una estrategia terrible.
Usos inválidos del lenguaje, como hacer referencia a vinculaciones
inexistentes, buscar una propiedad en null, o llamar a algo que no sea
una función, también dará como resultado que se levanten excepciones.
Tales excepciones también pueden ser atrapadas.
Cuando se ingresa en un cuerpo catch, todo lo que sabemos es que
algo en nuestro cuerpo try provocó una excepción. Pero no sabemos
que, o cual excepción este causó.
JavaScript (en una omisión bastante evidente) no proporciona so-
porte directo para la captura selectiva de excepciones: o las atrapas
todas o no atrapas nada. Esto hace que sea tentador asumir que la
excepción que obtienes es en la que estabas pensando cuando escribiste
el bloque catch.
Pero puede que no. Alguna otra suposición podría ser violada, o es
posible que hayas introducido un error que está causando una excepción.
Aquí está un ejemplo que intenta seguir llamando pedirDireccion hasta
que obtenga una respuesta válida:
for (;;) {
228
try {
let direccion = peirDirrecion("Donde?"); // ← error
tipografico!
console.log("Tu elegiste ", direccion);
break;
} catch (e) {
console.log ("No es una dirección válida. Inténtalo de nuevo
");
}
}
229
es en la que estamos interesados y relanzar de otra manera. Pero como
hacemos para reconocer una excepción?
Podríamos comparar su propiedad message con el mensaje de error
que sucede estamos esperando. Pero esa es una forma inestable de
escribir código—estariamos utilizando información destinada al con-
sumo humano (el mensaje) para tomar una decisión programática. Tan
pronto como alguien cambie (o traduzca) el mensaje, el código dejaria
de funcionar.
En vez de esto, definamos un nuevo tipo de error y usemos instanceof
para identificarlo.
class ErrorDeEntrada extends Error {}
function pedirDireccion(pregunta) {
let resultado = prompt(pregunta);
if (resultado.toLowerCase() == "izquierda") return "I";
if (resultado.toLowerCase() == "derecha") return "D";
throw new ErrorDeEntrada("Direccion invalida: " + resultado);
}
230
for (;;) {
try {
let direccion = pedirDireccion("Donde?");
console.log("Tu eliges ", direccion);
break;
} catch (e) {
if (e instanceof ErrorDeEntrada) {
console.log ("No es una dirección válida. Inténtalo de
nuevo");
} else {
throw e;
}
}
}
Esto capturará solo las instancias de error y dejará que las excep-
ciones no relacionadas pasen a través. Si reintroduce el error tipográfico,
el error de la vinculación indefinida será reportado correctamente.
Afirmaciones
Las afirmaciones son comprobaciones dentro de un programa que verifi-
can que algo este en la forma en la que se supone que debe estar. Se usan
no para manejar situaciones que puedan aparecer en el funcionamiento
normal, pero para encontrar errores hechos por el programador.
Si, por ejemplo, primerElemento se describe como una función que
nunca se debería invocar en arrays vacíos, podríamos escribirla así:
231
function primerElemento(array) {
if (array.length == 0) {
throw new Error("primerElemento llamado con []");
}
return array[0];
}
Resumen
Los errores y las malas entradas son hechos de la vida. Una parte impor-
tante de la programación es encontrar, diagnosticar y corregir errores.
Los problemas pueden será más fáciles de notar si tienes un conjunto
de pruebas automatizadas o si agregas afirmaciones a tus programas.
Por lo general, los problemas causados por factores fuera del con-
trol del programa deberían ser manejados con gracia. A veces, cuando
el problema pueda ser manejado localmente, los valores de devolución
232
especiales son una buena forma de rastrearlos. De lo contrario, las
excepciones pueden ser preferibles.
Al lanzar una excepción, se desenrolla la pila de llamadas hasta el
próximo bloque try/catch o hasta el final de la pila. Se le dará el valor
de excepción al bloque catch que lo atrape, que debería verificar que
en realidad es el tipo esperado de excepción y luego hacer algo con eso.
Para ayudar a controlar el impredecible flujo de control causado por las
excepciones, los bloques finally se pueden usar para asegurarte de que
una parte del código siempre se ejecute cuando un bloque termina.
Ejercicios
Reintentar
Digamos que tienes una función multiplicacionPrimitiva que, en el 20
por ciento de los casos, multiplica dos números, y en el otro 80 por
ciento,
genera una excepción del tipo FalloUnidadMultiplicadora. Escribe una
función que envuelva esta torpe función y solo siga intentando hasta
que una llamada tenga éxito, después de lo cual retorna el resultado.
Asegúrete de solo manejar las excepciones que estás tratando de
manejar.
La caja bloqueada
Considera el siguiente objeto (bastante artificial):
const caja = {
233
bloqueada: true,
desbloquear() { this.bloqueada = false; },
bloquear() { this.bloqueada = true; },
_contenido: [],
get contenido() {
if (this.bloqueada) throw new Error("Bloqueada!");
return this._contenido;
}
};
Es solo una caja con una cerradura. Hay un array en la caja, pero
solo puedes accederlo cuando la caja esté desbloqueada. Acceder direc-
tamente a la propiedad privada _contenido está prohibido.
Escribe una función llamada conCajaDesbloqueada que toma un valor
de función como su argumento, desbloquea la caja, ejecuta la función y
luego se asegura de que la caja se bloquee nuevamente antes de retornar,
independientemente de si la función argumento retorno normalmente o
lanzo una excepción.
234
“Algunas personas, cuando confrontadas con un problema,
piensan ‘Ya sé, usaré expresiones regulares.’ Ahora tienen dos
problemas.”
—Jamie Zawinski
Chapter 9
Expresiones Regulares
Las herramientas y técnicas de la programación sobreviven y se propa-
gan de una forma caótica y evolutiva. No siempre son los bonitas o las
brillantes las que ganan, sino más bien las que funcionan lo suficiente-
mente bien dentro del nicho correcto o que sucede se integran con otra
pieza exitosa de tecnología.
En este capítulo, discutiré una de esas herramientas, expresiones reg-
ulares. Las expresiones regulares son una forma de describir patrones en
datos de tipo string. Estas forman un lenguaje pequeño e independiente
que es parte de JavaScript y de muchos otros lenguajes y sistemas.
Las expresiones regulares son terriblemente incómodas y extremada-
mente útiles. Su sintaxis es críptica, y la interfaz de programación que
JavaScript proporciona para ellas es torpe. Pero son una poderosa her-
ramienta para inspeccionar y procesar cadenas. Entender apropiada-
mente a las expresiones regulares te hará un programador más efectivo.
235
Creando una expresión regular
Una expresión regular es un tipo de objeto. Puede ser construido con el
constructor RegExp o escrito como un valor literal al envolver un patrón
en caracteres de barras diagonales (/).
let re1 = new RegExp("abc");
let re2 = /abc/;
236
Probando por coincidencias
Los objetos de expresión regular tienen varios métodos. El más simple es
test (“probar”). Si le pasas un string, retornar un Booleano diciéndote
si el string contiene una coincidencia del patrón en la expresión.
console.log(/abc/.test("abcde"));
// → true
console.log(/abc/.test("abxde"));
// → false
Conjuntos de caracteres
Averiguar si un string contiene abc bien podría hacerse con una llamada
a indexOf. Las expresiones regulares nos permiten expresar patrones
más complicados.
Digamos que queremos encontrar cualquier número. En una expre-
sión regular, poner un conjunto de caracteres entre corchetes hace que
esa parte de la expresión coincida con cualquiera de los caracteres entre
237
los corchetes.
Ambas expresiones coincidiran con todas los strings que contengan
un dígito:
console.log(/[0123456789]/.test("en 1992"));
// → true
console.log(/[0-9]/.test("en 1992"));
// → true
Dentro de los corchetes, un guion (-) entre dos caracteres puede ser
utilizado para indicar un rango de caracteres, donde el orden es deter-
minado por el número Unicode del carácter. Los caracteres 0 a 9 estan
uno al lado del otro en este orden (códigos 48 a 57), por lo que [0-9]
los cubre a todos y coincide con cualquier dígito.
Un numero de caracteres comunes tienen sus propios atajos incorpo-
rados. Los dígitos son uno de ellos: \d significa lo mismo que [0-9].
\d Cualquier caracter dígito
\w Un caracter alfanumérico
\s Cualquier carácter de espacio en blanco (espacio, tabulación, nueva
\D Un caracter que no es un dígito
\W Un caracter no alfanumérico
\S Un caracter que no es un espacio en blanco
. Cualquier caracter a excepción de una nueva línea
Por lo que podrías coincidir con un formato de fecha y hora como
30-01-2003 15:20 con la siguiente expresión:
let fechaHora = /\d\d-\d\d-\d\d\d\d \d\d:\d\d/;
console.log(fechaHora.test("30-01-2003 15:20"));
238
// → true
console.log(fechaHora.test("30-jan-2003 15:20"));
// → false
239
Repitiendo partes de un patrón
Ya sabemos cómo hacer coincidir un solo dígito. Qué pasa si quere-
mos hacer coincidir un número completo—una secuencia de uno o más
dígitos?
Cuando pones un signo más (+) después de algo en una expresión
regular, este indica que el elemento puede repetirse más de una vez.
Por lo tanto, /\d+/ coincide con uno o más caracteres de dígitos.
console.log(/'\d+'/.test("'123'"));
// → true
console.log(/'\d+'/.test("''"));
// → false
console.log(/'\d*'/.test("'123'"));
// → true
console.log(/'\d*'/.test("''"));
// → true
240
console.log(reusar.test("rehusar"));
// → true
console.log(reusar.test("reusar"));
// → true
Agrupando subexpresiones
Para usar un operador como * o + en más de un elemento a la vez,
tienes que usar paréntesis. Una parte de una expresión regular que se
241
encierre entre paréntesis cuenta como un elemento único en cuanto a
los operadores que la siguen están preocupados.
let caricaturaLlorando = /boo+(hoo+)+/i;
console.log(caricaturaLlorando.test("Boohoooohoohooo"));
// → true
Coincidencias y grupos
El método test es la forma más simple de hacer coincidir una expre-
sión. Solo te dice si coincide y nada más. Las expresiones regulares
también tienen un método exec (“ejecutar”) que retorna null si no se
encontró una coincidencia y retorna un objeto con información sobre la
coincidencia de lo contrario.
let coincidencia = /\d+/.exec("uno dos 100");
console.log(coincidencia);
// → ["100"]
console.log(coincidencia.index);
242
// → 8
243
en el array de salida sera undefined. Del mismo modo, cuando un grupo
coincida multiples veces, solo la ultima coincidencia termina en el array.
console.log(/mal(isimo)?/.exec("mal"));
// → ["mal", undefined]
console.log(/(\d)+/.exec("123"));
// → ["123", "3"]
244
console.log(new Date(2009, 11, 9));
// → Wed Dec 09 2009 00:00:00 GMT+0100 (CET)
console.log(new Date(2009, 11, 9, 12, 59, 59, 999));
// → Wed Dec 09 2009 12:59:59 GMT+0100 (CET)
245
Los objetos de fecha proporcionan métodos como getFullYear (“obten-
erAñoCompleto”), getMonth (“obtenerMes”), getDate (“obtenerFecha”),
getHours (“obtenerHoras”), getMinutes (“obtenerMinutos”), y getSeconds
(“obtenerSegundos”) para extraer sus componentes. Además de getFullYea
, también existe getYear (“obtenerAño”), que te da como resultado un
valor de año de dos dígitos bastante inútil (como 93 o 14).
Al poner paréntesis alrededor de las partes de la expresión en las que
estamos interesados, ahora podemos crear un objeto de fecha a partir
de un string.
function obtenerFecha(string) {
let [_, dia, mes, año] =
/(\d{1,2})-(\d{1,2})-(\d{4})/.exec(string);
return new Date(año, mes - 1, dia);
}
console.log(obtenerFecha("30-1-2003"));
// → Thu Jan 30 2003 00:00:00 GMT+0100 (CET)
246
carácter.
Si queremos hacer cumplir que la coincidencia deba abarcar el string
completamente, puedes agregar los marcadores ^ y $. El signo de inter-
calación ("^") coincide con el inicio del string de entrada, mientras que
el signo de dólar coincide con el final. Entonces, /^\d+$/ coincide con
un string compuesto por uno o más dígitos, /^!/ coincide con cualquier
string que comience con un signo de exclamación, y /x^/ no coincide
con ningun string (no puede haber una x antes del inicio del string).
Si, por el otro lado, solo queremos asegurarnos de que la fecha comience
y termina en un límite de palabras, podemos usar el marcador \b. Un
límite de palabra puede ser el inicio o el final del string o cualquier
punto en el string que tenga un carácter de palabra (como en \w) en un
lado y un carácter de no-palabra en el otro.
console.log(/cat/.test("concatenar"));
// → true
console.log(/\bcat\b/.test("concatenar"));
// → false
Patrones de elección
Digamos que queremos saber si una parte del texto contiene no solo un
número pero un número seguido de una de las palabras cerdo, vaca, o
247
pollo, o cualquiera de sus formas plurales.
Podríamos escribir tres expresiones regulares y probarlas a su vez,
pero hay una manera más agradable. El carácter de tubería (|) denota
una elección entre el patrón a su izquierda y el patrón a su derecha.
Entonces puedo decir esto:
let conteoAnimales = /\b\d+ (cerdo|vaca|pollo)s?\b/;
console.log(conteoAnimales.test("15 cerdo"));
// → true
console.log(conteoAnimales.test("15 cerdopollos"));
// → false
Los paréntesis se pueden usar para limitar la parte del patrón a la que
aplica el operador de tuberia, y puedes poner varios de estos operadores
unos a los lados de los otros para expresar una elección entre más de
dos alternativas.
248
la expresión de ganado en el ejemplo anterior:
Group #1
"pig"
"chicken"
249
aquí, pero vemos “cerdo”, entonces tomamos esa rama.
• En la posición 9, después de la rama de tres vías, un camino
se salta la caja s y va directamente al límite de la palabra final,
mientras que la otra ruta coincide con una s. Aquí hay un carácter
s, no una palabra límite, por lo que pasamos por la caja s.
• Estamos en la posición 10 (al final del string) y solo podemos
hacer coincidir una palabra límite. El final de un string cuenta
como un límite de palabra, así que pasamos por la última caja y
hemos emparejado con éxito este string.
Retrocediendo
La expresión regular /\b([01]+b|[\da-f]+h|\d+)\b/ coincide con un número
binario seguido de una b, un número hexadecimal (es decir, en base 16,
con las letras a a f representando los dígitos 10 a 15) seguido de una h,
o un número decimal regular sin caracter de sufijo. Este es el diagrama
correspondiente:
250
group #1
One of:
“0”
“b”
“1”
One of:
word boundary word boundary
digit
“h”
“a” - “f”
digit
251
cia completa. Esto significa que si múltiples ramas podrían coincidir
con un string, solo la primera (ordenado por donde las ramas aparecen
en la expresión regular) es usada.
El retroceso también ocurre para repetición de operadores como + y
*. Si hace coincidir /^.*x/ contra "abcxe", la parte .* intentará primero
consumir todo el string. El motor entonces se dará cuenta de que nece-
sita una x para que coincida con el patrón. Como no hay x al pasar el
final del string, el operador de estrella intenta hacer coincidir un carac-
ter menos. Pero el emparejador tampoco encuentra una x después de
abcx, por lo que retrocede nuevamente, haciendo coincidir el operador
de estrella con abc. Ahora encuentra una x donde lo necesita e informa
de una coincidencia exitosa de las posiciones 0 a 4.
Es posible escribir expresiones regulares que harán un monton de
retrocesos. Este problema ocurre cuando un patrón puede coincidir con
una pieza de entrada en muchas maneras diferentes. Por ejemplo, si
nos confundimos mientras escribimos una expresión regular de números
binarios, podríamos accidentalmente escribir algo como /([01]+)+b/.
Group #1
One of:
"0"
"b"
"1"
252
Si intentas hacer coincidir eso con algunas largas series de ceros y
unos sin un caracter b al final, el emparejador primero pasara por el
ciclo interior hasta que se quede sin dígitos. Entonces nota que no hay
b, asi que retrocede una posición, atraviesa el ciclo externo una vez, y se
da por vencido otra vez, tratando de retroceder fuera del ciclo interno
una vez más. Continuará probando todas las rutas posibles a través de
estos dos bucles. Esto significa que la cantidad de trabajo se duplica
con cada caracter. Incluso para unas pocas docenas de caracters, la
coincidencia resultante tomará prácticamente para siempre.
El método replace
Los valores de string tienen un método replace (“reemplazar”) que se
puede usar para reemplazar parte del string con otro string.
console.log("papa".replace("p", "m"));
// → mapa
253
// → Barabadar
254
argumento para replace. Para cada reemplazo, la función será llamada
con los grupos coincidentes (así como con la coincidencia completa)
como argumentos, y su valor de retorno se insertará en el nuevo string.
Aquí hay un pequeño ejemplo:
let s = "la cia y el fbi";
console.log(s.replace(/\b(fbi|cia)\b/g,
str => str.toUpperCase()));
// → la CIA y el FBI
255
El grupo (\d+) termina como el argumento cantidad para la función,
y el grupo (\w+) se vincula a unidad. La función convierte cantidad a un
número—lo que siempre funciona, ya que coincidio con \d+—y realiza
algunos ajustes en caso de que solo quede uno o cero.
Codicia
Es posible usar replace para escribir una función que elimine todo los
comentarios de un fragmento de código JavaScript. Aquí hay un primer
intento:
function removerComentarios(codigo) {
return codigo.replace(/\/\/.*|\/\*[^]*\*\//g, "");
}
console.log(removerComentarios("1 + /* 2 */3"));
// → 1 + 3
console.log(removerComentarios("x = 10;// ten!"));
// → x = 10;
console.log(removerComentarios("1 /* a */+/* b */ 1"));
// → 1 1
256
bloque pueden continuar en una nueva línea, y el carácter del período
no coincide con caracteres de nuevas lineas.
Pero la salida de la última línea parece haber salido mal. Por qué?
La parte [^]* de la expresión, como describí en la sección retroceder,
primero coincidirá tanto como sea posible. Si eso causa un falo en
la siguiente parte del patrón, el emparejador retrocede un caracter e
intenta nuevamente desde allí. En el ejemplo, el emparejador primero
intenta emparejar el resto del string y luego se mueve hacia atrás desde
allí. Este encontrará una ocurrencia de */ después de retroceder cuatro
caracteres y emparejar eso. Esto no es lo que queríamos, la intención
era hacer coincidir un solo comentario, no ir hasta el final del código y
encontrar el final del último comentario de bloque.
Debido a este comportamiento, decimos que los operadores de repeti-
ción (+, *, ? y {}) son _ codiciosos, lo que significa que coinciden con
tanto como pueden y retroceden desde allí. Si colocas un signo de in-
terrogación después de ellos (+?, *?, ??, {}?), se vuelven no-codiciosos
y comienzan a hacer coincidir lo menos posible, haciendo coincidir más
solo cuando el patrón restante no se ajuste a la coincidencia más pe-
queña.
Y eso es exactamente lo que queremos en este caso. Al hacer que la
estrella coincida con el tramo más pequeño de caracteres que nos lleve
a un */, consumimos un comentario de bloque y nada más.
function removerComentarios(codigo) {
return codigo.replace(/\/\/.*|\/\*[^]*?\*\//g, "");
}
console.log(removerComentarios("1 /* a */+/* b */ 1"));
257
// → 1 + 1
Al crear los marcadores de límite \b, tenemos que usar dos barras
invertidas porque las estamos escribiendo en un string normal, no en
258
una expresión regular contenida en barras. El segundo argumento para
el constructor RegExp contiene las opciones para la expresión regular—en
este caso, "gi" para global e insensible a mayúsculas y minúsculas.
Pero, y si el nombre es "dea+hl[]rd" porque nuestro usuario es un
nerd adolescente? Eso daría como resultado una expresión regular sin
sentido que en realidad no coincidirá con el nombre del usuario.
Para solucionar esto, podemos agregar barras diagonales inversas
antes de cualquier caracter que tenga un significado especial.
let nombre = "dea+hl[]rd";
let texto = "Este sujeto dea+hl[]rd es super fastidioso.";
let escapados = nombre.replace(/[\\[.+*?(){|^$]/g, "\\$&");
let regexp = new RegExp("\\b" + escapados + "\\b", "gi");
console.log(texto.replace(regexp, "_$&_"));
// → Este sujeto _dea+hl[]rd_ es super fastidioso.
El método search
El método indexOf en strings no puede invocarse con una expresión
regular. Pero hay otro método, search (“buscar”), que espera una ex-
presión regular. Al igual que indexOf, retorna el primer índice en que
se encontró la expresión, o -1 cuando no se encontró.
console.log(" palabra".search(/\S/));
// → 2
console.log(" ".search(/\S/));
// → -1
259
Desafortunadamente, no hay forma de indicar que la coincidencia
debería comenzar a partir de un desplazamiento dado (como podemos
con el segundo argumento para indexOf), que a menudo sería útil.
La propiedad lastIndex
De manera similar el método exec no proporciona una manera conve-
niente de comenzar buscando desde una posición dada en el string. Pero
proporciona una manera inconveniente.
Los objetos de expresión regular tienen propiedades. Una de esas
propiedades es source (“fuente”), que contiene el string de donde se
creó la expresión. Otra propiedad es lastIndex (“ultimoIndice”), que
controla, en algunas circunstancias limitadas, donde comenzará la sigu-
iente coincidencia.
Esas circunstancias son que la expresión regular debe tener la opción
global (g) o adhesiva (y) habilitada, y la coincidencia debe suceder a
través del método exec. De nuevo, una solución menos confusa hubiese
sido permitir que un argumento adicional fuera pasado a exec, pero la
confusión es una característica esencial de la interfaz de las expresiones
regulares de JavaScript.
let patron = /y/g;
patron.lastIndex = 3;
let coincidencia = patron.exec("xyzzy");
console.log(coincidencia.index);
// → 4
260
console.log(patron.lastIndex);
// → 5
261
console.log(digito.exec("aqui esta: 1"));
// → ["1"]
console.log(digito.exec("y ahora: 1"));
// → null
Por lo tanto, ten cuidado con las expresiones regulares globales. Los
casos donde son necesarias—llamadas a replace y lugares donde de-
seas explícitamente usar lastIndex—son generalmente los únicos lugares
donde querras usarlas.
262
let numero = /\b\d+\b/g;
let coincidencia;
while (coincidencia = numero.exec(entrada)) {
console.log("Se encontro", coincidencia[0], "en", coincidencia.
index);
}
// → Se encontro 3 en 14
// Se encontro 42 en 33
// Se encontro 88 en 38
Esto hace uso del hecho de que el valor de una expresión de asig-
nación (=) es el valor asignado. Entonces al usar coincidencia = numero
.exec(entrada) como la condición en la declaración while, realizamos
la coincidencia al inicio de cada iteración, guardamos su resultado en
una vinculación, y terminamos de repetir cuando no se encuentran más
coincidencias.
263
malevolencia=9.7
[larry]
nombrecompleto=Larry Doe
tipo=bravucon del preescolar
sitioweb=http://www.geocities.com/CapeCanaveral/11451
[davaeorn]
nombrecompleto=Davaeorn
tipo=hechizero malvado
directoriosalida=/home/marijn/enemies/davaeorn
264
objetos para secciones, con esos subobjetos conteniendo la configuración
de la sección.
Dado que el formato debe procesarse línea por línea, dividir el archivo
en líneas separadas es un buen comienzo. Usamos string.split("\n")
para hacer esto en el Capítulo 4. Algunos sistemas operativos, sin
embargo, usan no solo un carácter de nueva línea para separar lineas
sino un carácter de retorno de carro seguido de una nueva línea ("\r\
n"). Dado que el método split también permite una expresión regular
como su argumento, podemos usar una expresión regular como /\r?\
n/ para dividir el string de una manera que permita tanto "\n" como
"\r\n" entre líneas.
function analizarINI(string) {
// Comenzar con un objeto para mantener los campos de nivel
superior
let resultado = {};
let seccion = resultado;
string.split(/\r?\n/).forEach(linea => {
let coincidencia;
if (coincidencia = linea.match(/^(\w+)=(.*)$/)) {
seccion[coincidencia[1]] = coincidencia[2];
} else if (coincidencia = linea.match(/^\[(.*)\]$/)) {
seccion = resultado[coincidencia[1]] = {};
} else if (!/^\s*(;.*)?$/.test(linea)) {
throw new Error("Linea '" + linea + "' no es valida.");
}
});
return resultado;
}
265
console.log(analizarINI(`
nombre=Vasilis
[direccion]
ciudad=Tessaloniki`));
// → {nombre: "Vasilis", direccion: {ciudad: "Tessaloniki"}}
El código pasa por las líneas del archivo y crea un objeto. Las
propiedades en la parte superior se almacenan directamente en ese ob-
jeto, mientras que las propiedades que se encuentran en las secciones
se almacenan en un objeto de sección separado. La vinculación sección
apunta al objeto para la sección actual.
Hay dos tipos de de líneas significativas—encabezados de seccion o
lineas de propiedades. Cuando una línea es una propiedad regular, esta
se almacena en la sección actual. Cuando se trata de un encabezado de
sección, se crea un nuevo objeto de sección, y seccion se configura para
apuntar a él.
Nota el uso recurrente de ^ y $ para asegurarse de que la expresión
coincida con toda la línea, no solo con parte de ella. Dejando afuera
estos resultados en código que funciona principalmente, pero que se
comporta de forma extraña para algunas entradas, lo que puede ser un
error difícil de rastrear.
El patrón if (coincidencia = string.match (...)) es similar al truco
de usar una asignación como condición para while. A menudo no estas
seguro de que tu llamada a match tendrá éxito, para que puedas acceder
al objeto resultante solo dentro de una declaración if que pruebe esto.
Para no romper la agradable cadena de las formas else if, asignamos el
266
resultado de la coincidencia a una vinculación e inmediatamente usamos
esa asignación como la prueba para la declaración if.
Si una línea no es un encabezado de sección o una propiedad, la fun-
ción verifica si es un comentario o una línea vacía usando la expresión
/^\s*(;.*)?$/. Ves cómo funciona? La parte entre los paréntesis co-
incidirá con los comentarios, y el ? asegura que también coincida con
líneas que contengan solo espacios en blanco. Cuando una línea no
coincida con cualquiera de las formas esperadas, la función arroja una
excepción.
Caracteres internacionales
Debido a la simplista implementación inicial de JavaScript y al hecho de
que este enfoque simplista fue luego establecido en piedra como compor-
tamiento estándar, las expresiones regulares de JavaScript son bastante
tontas acerca de los caracteres que no aparecen en el idioma inglés.
Por ejemplo, en cuanto a las expresiones regulares de JavaScript, una
“palabra caracter” es solo uno de los 26 caracteres en el alfabeto latino
(mayúsculas o minúsculas), dígitos decimales, y, por alguna razón, el
carácter de guion bajo. Cosas como é o ß, que definitivamente son
caracteres de palabras, no coincidirán con \w (y si coincidiran con \W
mayúscula, la categoría no-palabra).
Por un extraño accidente histórico, \s (espacio en blanco) no tiene
este problema y coincide con todos los caracteres que el estándar Uni-
code considera espacios en blanco, incluyendo cosas como el (espacio de
no separación) y el Separador de vocales Mongol.
267
Otro problema es que, de forma predeterminada, las expresiones reg-
ulares funcionan en unidades del código, como se discute en el Capítulo
5, no en caracteres reales. Esto significa que los caracteres que estan
compustos de dos unidades de código se comportan de manera extraña.
console.log(/🍎{3}/.test("🍎🍎🍎"));
// → false
console.log(/<.>/.test("<🌹>"));
// → false
console.log(/<.>/u.test("<🌹>"));
// → true
268
console.log(/\p{Script=Greek}/u.test("α"));
// → true
console.log(/\p{Script=Arabic}/u.test("α"));
// → false
console.log(/\p{Alphabetic}/u.test("α"));
// → true
console.log(/\p{Alphabetic}/u.test("!"));
// → false
Resumen
Las expresiones regulares son objetos que representan patrones en strings.
Ellas usan su propio lenguaje para expresar estos patrones.
269
/abc/ Una secuencia de caracteres
/[abc]/ Cualquier caracter de un conjunto de caracteres
/[^abc]/ Cualquier carácter que no este en un conjunto de caracteres
/[0-9]/ Cualquier caracter en un rango de caracteres
/x+/ Una o más ocurrencias del patrón x
/x+?/ Una o más ocurrencias, no codiciosas
/x*/ Cero o más ocurrencias
/x?/ Cero o una ocurrencia
/x{2,4}/ De dos a cuatro ocurrencias
/(abc)/ Un grupo
/a|b|c/ Cualquiera de varios patrones
/\d/ Cualquier caracter de digito
/\w/ Un caracter alfanumérico (“carácter de palabra”)
/\s/ Cualquier caracter de espacio en blanco
/./ Cualquier caracter excepto líneas nuevas
/\b/ Un límite de palabra
/^/ Inicio de entrada
/$/ Fin de la entrada
Una expresión regular tiene un método test para probar si una de-
terminada string coincide cn ella. También tiene un método exec que,
cuando una coincidencia es encontrada, retorna un array que contiene
todos los grupos que coincidieron. Tal array tiene una propiedad index
que indica en dónde comenzó la coincidencia.
Los strings tienen un método match para coincidirlas con una expre-
sión regular y un método search para buscar por una, retornando solo la
posición inicial de la coincidencia. Su método replace puede reemplazar
coincidencias de un patrón con un string o función de reemplazo.
270
Las expresiones regulares pueden tener opciones, que se escriben de-
spués de la barra que cierra la expresión. La opción i hace que la coin-
cidencia no distinga entre mayúsculas y minúsculas. La opción g hace
que la expresión sea global, que, entre otras cosas, hace que el método
replace reemplace todas las instancias en lugar de solo la primera. La
opción y la hace adhesivo, lo que significa que hará que no busque con
anticipación y omita la parte del string cuando busque una coincidencia.
La opción u activa el modo Unicode, lo que soluciona varios problemas
alrededor del manejo de caracteres que toman dos unidades de código.
Las expresiones regulares son herramientas afiladas con un manejo
incómodo. Ellas simplifican algunas tareas enormemente, pero pueden
volverse inmanejables rápidamente cuando se aplican a problemas com-
plejos. Parte de saber cómo usarlas es resistiendo el impulso de tratar
de calzar cosas que no pueden ser expresadas limpiamente en ellas.
Ejercicios
Es casi inevitable que, durante el curso de trabajar en estos ejercicios, te
sentiras confundido y frustrado por el comportamiento inexplicable de
alguna regular expresión. A veces ayuda ingresar tu expresión en una
herramienta en línea como debuggex.com para ver si su visualización
corresponde a lo que pretendías y a experimentar con la forma en que
responde a varios strings de entrada.
271
Golf Regexp
Golf de Codigo es un término usado para el juego de intentar expresar
un programa particular con el menor número de caracteres posible.
Similarmente, Golf de Regexp es la práctica de escribir una expresión
regular tan pequeña como sea posible para que coincida con un patrón
dado, y sólo con ese patrón.
Para cada uno de los siguientes elementos, escribe una expresión reg-
ular para probar si alguna de las substrings dadas ocurre en un string.
La expresión regular debe coincidir solo con strings que contengan una
de las substrings descritas. No te preocupes por los límites de palabras
a menos que sean explícitamente mencionados. Cuando tu expresión
funcione, ve si puedes hacerla más pequeña.
1. car y cat
2. pop y prop
3. ferret, ferry, y ferrari
4. Cualquier palabra que termine ious
5. Un carácter de espacio en blanco seguido de un punto, coma, dos
puntos o punto y coma
6. Una palabra con mas de seis letras
7. Una palabra sin la letra e (o E)
Consulta la tabla en el resumen del capítulo para ayudarte. Pruebe
cada solución con algunos strings de prueba.
272
Estilo entre comillas
Imagina que has escrito una historia y has utilizado comillass simples en
todas partes para marcar piezas de diálogo. Ahora quieres reemplazar
todas las comillas de diálogo con comillas dobles, pero manteniendo las
comillas simples usadas en contracciones como aren’t.
Piensa en un patrón que distinga de estos dos tipos de uso de citas y
crea una llamada al método replace que haga el reemplazo apropiado.
273
“Escriba código que sea fácil de borrar, no fácil de extender.”
—Tef, Programming is Terrible
Chapter 10
Módulos
El programa ideal tiene una estructura cristalina. La forma en que
funciona es fácil de explicar, y cada parte juega un papel bien definido.
Un típico programa real crece orgánicamente. Nuevas piezas de fun-
cionalidad se agregan a medida que surgen nuevas necesidades. Estructurar
y preservar la estructura—es trabajo adicional, trabajo que solo valdra
la pena en el futuro, la siguiente vez que alguien trabaje en el programa.
Así que es tentador descuidarlo, y permitir que las partes del programa
se vuelvan profundamente enredadas.
Esto causa dos problemas prácticos. En primer lugar, entender tal
sistema es difícil. Si todo puede tocar todo lo demás, es difícil ver
a cualquier pieza dada de forma aislada. Estas obligado a construir
un entendimiento holístico de todo el asunto. En segundo lugar, si
quieres usar cualquiera de las funcionalidades de dicho programa en
otra situación, reescribirla podria resultar más fácil que tratar de de-
senredarla de su contexto.
El término “gran bola de barro” se usa a menudo para tales programas
grandes, sin estructura. Todo se mantiene pegado, y cuando intentas
274
sacar una pieza, todo se desarma y tus manos se ensucian.
Módulos
Los módulos son un intento de evitar estos problemas. Un módulo es
una pieza del programa que especifica en qué otras piezas este depende
( sus dependencias) y qué funcionalidad proporciona para que otros
módulos usen (su interfaz).
Las interfaces de los módulos tienen mucho en común con las inter-
faces de objetos, como las vimos en el Capítulo 6. Estas hacen parte del
módulo disponible para el mundo exterior y mantienen el resto privado.
Al restringir las formas en que los módulos interactúan entre sí, el el
sistema se parece más a un juego de LEGOS, donde las piezas interac-
túan a través de conectores bien definidos, y menos como barro, donde
todo se mezcla con todo.
Las relaciones entre los módulos se llaman dependencias. Cuando
un módulo necesita una pieza de otro módulo, se dice que depende
de ese módulo. Cuando este hecho está claramente especificado en
el módulo en sí, puede usarse para descubrir qué otros módulos deben
estar presentes para poder ser capaces de usar un módulo dado y cargar
dependencias automáticamente.
Para separar módulos de esa manera, cada uno necesita su propio
alcance privado.
Simplemente poner todo tu código JavaScript en diferentes archivos
no satisface estos requisitos. Los archivos aún comparten el mismo es-
pacio de nombres global. Pueden, intencionalmente o accidentalmente,
275
interferir con las vinculaciones de cada uno. Y la estructura de depen-
dencia sigue sin estar clara. Podemos hacerlo mejor, como veremos más
adelante en el capítulo.
Diseñar una estructura de módulo ajustada para un programa puede
ser difícil. En la fase en la que todavía estás explorando el problema,
intentando cosas diferentes para ver que funciona, es posible que desees
no preocuparte demasiado por eso, ya que puede ser una gran distrac-
ción. Una vez que tengas algo que se sienta sólido, es un buen momento
para dar un paso atrás y organizarlo.
Paquetes
Una de las ventajas de construir un programa a partir de piezas sep-
aradas, y ser capaces de ejecutar esas piezas por si mismas, es que tú
podrías ser capaz de aplicar la misma pieza en diferentes programas.
Pero cómo se configura esto? Digamos que quiero usar la función
analizarINI del Capítulo 9 en otro programa. Si está claro de qué
depende la función (en este caso, nada), puedo copiar todo el código
necesario en mi nuevo proyecto y usarlo. Pero luego, si encuentro un
error en ese código, probablemente lo solucione en el programa en el
que estoy trabajando en ese momento y me olvido de arreglarlo en el
otro programa.
Una vez que comience a duplicar código, rápidamente te encontraras
perdiendo tiempo y energía moviendo las copias alrededor y mantenién-
dolas actualizadas.
Ahí es donde los paquetes entran. Un paquete es un pedazo de código
276
que puede ser distribuido (copiado e instalado). Puede contener uno o
más módulos, y tiene información acerca de qué otros paquetes depende.
Un paquete también suele venir con documentación que explica qué es
lo que hace, para que las personas que no lo escribieron todavía puedan
hacer uso de el.
Cuando se encuentra un problema en un paquete, o se agrega una
nueva característica, el el paquete es actualizado. Ahora los programas
que dependen de él (que también pueden ser otros paquetes) pueden
actualizar a la nueva versión.
Trabajar de esta manera requiere infraestructura. Necesitamos un
lugar para almacenar y encontrar paquetes, y una forma conveniente de
instalar y actualizarlos. En el mundo de JavaScript, esta infraestructura
es provista por NPM (npmjs.org).
NPM es dos cosas: un servicio en línea donde uno puede descargar
(y subir) paquetes, y un programa (incluido con Node.js) que te ayuda
a instalar y administrarlos.
Al momento de escribir esto, hay más de medio millón de paquetes
diferentes disponibles en NPM. Una gran parte de ellos son basura, de-
bería mencionar, pero casi todos los paquetes útiles, disponibles pública-
mente, se puede encontrar allí. Por ejemplo, un analizador de archivos
INI, similar al uno que construimos en el Capítulo 9, está disponible
bajo el nombre de paquete ini.
En el Capítulo 20 veremos cómo instalar dichos paquetes de forma
local utilizando el programa de línea de comandos npm.
Tener paquetes de calidad disponibles para descargar es extremada-
mente valioso. Significa que a menudo podemos evitar tener que rein-
277
ventar un programa que cien personas han escrito antes, y obtener una
implementación sólida y bien probado con solo presionar algunas teclas.
El software es barato de copiar, por lo que una vez lo haya escrito
alguien, distribuirlo a otras personas es un proceso eficiente. Pero es-
cribirlo en el primer lugar, es trabajo y responder a las personas que
han encontrado problemas en el código, o que quieren proponer nuevas
características, es aún más trabajo.
Por defecto, tu posees el copyright del código que escribes, y otras
personas solo pueden usarlo con tu permiso. Pero ya que algunas per-
sonas son simplemente agradables, y porque la publicación de un buen
software puede ayudarte a hacerte un poco famoso entre los progra-
madores, se publican muchos paquetes bajo una licencia que explícita-
mente permite a otras personas usarlos.
La mayoría del código en NPM esta licenciado de esta manera. Al-
gunas licencias requieren que tu publiques también el código bajo la
misma licencia del paquete que estas usando. Otras son menos exi-
gentes, solo requieren que guardes la licencia con el código cuando lo
distribuyas. La comunidad de JavaScript principalmente usa ese úl-
timo tipo de licencia. Al usar paquetes de otras personas, asegúrete de
conocer su licencia.
Módulos improvisados
Hasta 2015, el lenguaje JavaScript no tenía un sistema de módulos
incorporado. Sin embargo, la gente había estado construyendo sistemas
grandes en JavaScript durante más de una década y ellos necesitaban
278
módulos.
Así que diseñaron sus propios sistema de módulos arriba del lenguaje.
Puedes usar funciones de JavaScript para crear alcances locales, y ob-
jetos para representar las interfaces de los módulos.
Este es un módulo para ir entre los nombres de los días y números
(como son retornados por el método getDay de Date). Su interfaz con-
siste en diaDeLaSemana.nombre y diaDeLaSemana.numero, y oculta su vin-
culación local nombres dentro del alcance de una expresión de función
que se invoca inmediatamente.
const diaDeLaSemana = function() {
const nombres = ["Domingo", "Lunes", "Martes", "Miercoles",
"Jueves", "Viernes", "Sabado"];
return {
nombre(numero) { return nombres[numero]; },
numero(nombre) { return nombres.indexOf(nombre); }
};
}();
console.log(diaDeLaSemana.nombre(diaDeLaSemana.numero("Domingo"))
);
// → Domingo
279
Si queremos hacer que las relaciones de dependencia sean parte del
código, tendremos que tomar el control de las dependencias que deben
ser cargadas. Hacer eso requiere que seamos capaces de ejecutar strings
como código. JavaScript puede hacer esto.
console.log(evaluarYRetornarX("var x = 2"));
// → 2
280
string que contiene el cuerpo de la función.
let masUno = Function("n", "return n + 1;");
console.log(masUno(4));
// → 5
CommonJS
El enfoque más utilizado para incluir módulos en JavaScript es llamado
módulos CommonJS. Node.js lo usa, y es el sistema utilizado por la
mayoría de los paquetes en NPM.
El concepto principal en los módulos CommonJS es una función lla-
mada require (“requerir”). Cuando la llamas con el nombre del módulo
de una dependencia, esta se asegura de que el módulo sea cargado y
retorna su interfaz.
Debido a que el cargador envuelve el código del módulo en una fun-
ción, los módulos obtienen automáticamente su propio alcance local.
Todo lo que tienen que hacer es llamar a require para acceder a sus
dependencias, y poner su interfaz en el objeto vinculado a exports (“ex-
portaciones”).
Este módulo de ejemplo proporciona una función de formateo de
fecha. Utiliza dos paquetes de NPM—ordinal para convertir números
281
a strings como "1st" y "2nd", y date-names para obtener los nombres en
inglés de los días de la semana y meses. Este exporta una sola función,
formatDate, que toma un objeto Date y un string plantilla.
El string de plantilla puede contener códigos que dirigen el formato,
como YYYY para todo el año y Do para el día ordinal del mes. Po-
drías darle un string como "MMMM Do YYYY" para obtener resultados como
“November 22nd 2017”.
const ordinal = require("ordinal");
const {days, months} = require("date-names");
282
módulo de esta manera:
const {formatDate} = require("./format-date");
function require(nombre) {
if (!(nombre in require.cache)) {
let codigo = leerArchivo(nombre);
let modulo = {exportaciones: {}};
require.cache[nombre] = modulo;
let envolvedor = Function("require, exportaciones, modulo",
codigo);
envolvedor(require, modulo.exportaciones, modulo);
}
return require.cache[nombre].exportaciones;
}
283
ceder a archivos. El ejemplo solo pretende que leerArchivo existe.
Para evitar cargar el mismo módulo varias veces, require mantiene
un (caché) almacenado de módulos que ya han sido cargados. Cuando
se llama, primero verifica si el módulo solicitado ya ha sido cargado y,
si no, lo carga. Esto implica leer el código del módulo, envolverlo en
una función y llamárla.
La interfaz del paquete ordinal que vimos antes no es un objeto,
sino una función. Una peculiaridad de los módulos CommonJS es que,
aunque el sistema de módulos creará un objeto de interfaz vacío para
ti (vinculado a exports), puedes reemplazarlo con cualquier valor al so-
brescribir module.exports. Esto lo hacen muchos módulos para exportar
un valor único en lugar de un objeto de interfaz.
Al definir require, exportaciones y modulo como parametros para
la función de envoltura generada (y pasando los valores apropiados
al llamarla), el cargador se asegura de que estas vinculaciones esten
disponibles en el alcance del módulo.
La forma en que el string dado a require se traduce a un nombre
de archivo real o dirección web difiere en diferentes sistemas. Cuando
comienza con "./" o "../", generalmente se interpreta como relativo al
nombre del archivo actual. Entonces "./format-date" sería el archivo
llamado format-date.js en el mismo directorio.
Cuando el nombre no es relativo, Node.js buscará por un paquete
instalado con ese nombre. En el código de ejemplo de este capítulo,
interpretaremos esos nombres como referencias a paquetes de NPM.
Entraremos en más detalles sobre cómo instalar y usar los módulos de
NPM en el Capítulo 20.
284
Ahora, en lugar de escribir nuestro propio analizador de archivos INI,
podemos usar uno de NPM:
const {parse} = require("ini");
Módulos ECMAScript
Los módulos CommonJS funcionan bastante bien y, en combinación
con NPM, han permitido que la comunidad de JavaScript comience a
compartir código en una gran escala.
Pero siguen siendo un poco de un truco con cinta adhesiva. La no-
tación es ligeramente incomoda—las cosas que agregas a exports no
están disponibles en el alcance local, por ejemplo. Y ya que require es
una llamada de función normal tomando cualquier tipo de argumento,
no solo un string literal, puede ser difícil de determinar las dependencias
de un módulo sin correr su código primero.
Esta es la razón por la cual el estándar de JavaScript introdujo su
propio, sistema de módulos diferente a partir de 2015. Por lo general es
llamado módulos ES, donde ES significa ECMAScript. Los principales
conceptos de dependencias e interfaces siguen siendo los mismos, pero
los detalles difieren. Por un lado, la notación está ahora integrada
en el lenguaje. En lugar de llamar a una función para acceder a una
dependencia, utilizas una palabra clave import (“importar”) especial.
285
import ordinal from "ordinal";
import {days, months} from "date-names";
286
(“como”).
import {days as nombresDias} from "date-names";
console.log(nombresDias.length);
// → 7
Construyendo y empaquetando
De hecho, muchos proyectos de JavaScript ni siquiera están, técnica-
mente, escritos en JavaScript. Hay extensiones, como el dialecto de
comprobación de tipos mencionado en el Capítulo 7, que son ampli-
amente usados. Las personas también suelen comenzar a usar exten-
siones planificadas para el lenguaje mucho antes de que estas hayan sido
287
agregadas a las plataformas que realmente corren JavaScript.
Para que esto sea posible, ellos compilan su código, traduciéndolo
del dialecto de JavaScript que eligieron a JavaScript simple y antiguo—
o incluso a una versión anterior de JavaScript, para que navegadores
antiguos puedan ejecutarlo.
Incluir un programa modular que consiste de 200 archivos diferentes
en una página web produce sus propios problemas. Si buscar un solo
archivo sobre la red tarda 50 milisegundos, cargar todo el programa
tardaria 10 segundos, o tal vez la mitad si puedes cargar varios archivos
simultáneamente. Eso es mucho tiempo perdido. Ya que buscar un
solo archivo grande tiende a ser más rápido que buscar muchos archivos
pequeños, los programadores web han comenzado a usar herramientas
que convierten sus programas (los cuales cuidadosamente estan dividos
en módulos) de nuevo en un único archivo grande antes de publicarlo
en la Web. Tales herramientas son llamado empaquetadores.
Y podemos ir más allá. Además de la cantidad de archivos, el tamaño
de los archivos también determina qué tan rápido se pueden transferir
a través de la red. Por lo tanto, la comunidad de JavaScript ha inven-
tado minificadores. Estas son herramientas que toman un programa de
JavaScript y lo hacen más pequeño al eliminar automáticamente los co-
mentarios y espacios en blanco, cambia el nombre de las vinculaciones,
y reemplaza piezas de código con código equivalente que ocupa menos
espacio.
Por lo tanto, no es raro que el código que encuentres en un paquete de
NPM o que se ejecute en una página web haya pasado por multiples eta-
pas de transformación: conversión de JavaScript moderno a JavaScript
288
histórico, del formato de módulos ES a CommonJS, empaquetado y
minificado. No vamos a entrar en los detalles de estas herramientas en
este libro, ya que tienden a ser aburridos y cambian rápidamente. Solo
ten en cuenta que el código JavaScript que ejecutas a menudo no es el
código tal y como fue escrito.
Diseño de módulos
La estructuración de programas es uno de los aspectos más sutiles de
la programación. Cualquier pieza de funcionalidad no trivial se puede
modelar de varias maneras.
Un buen diseño de programa es subjetivo—hay ventajas/desventajas
involucradas, y cuestiones de gusto. La mejor manera de aprender el
valor de una buena estructura de diseño es leer o trabajar en muchos
programas y notar lo que funciona y lo qué no. No asumas que un
desastroso doloroso es “solo la forma en que las cosas son ". Puedes
mejorar la estructura de casi todo al ponerle mas pensamiento.
Un aspecto del diseño de módulos es la facilidad de uso. Si estás
diseñando algo que está destinado a ser utilizado por varias personas—
o incluso por ti mismo, en tres meses cuando ya no recuerdes los detalles
de lo que hiciste—es útil si tu interfaz es simple y predicible.
Eso puede significar seguir convenciones existentes. Un buen ejem-
plo es el paquete ini. Este módulo imita el objeto estándar JSON al
proporcionar las funciones parse y stringify (para escribir un archivo
INI), y, como JSON, convierte entre strings y objetos simples. Entonces
la interfaz es pequeña y familiar, y después de haber trabajado con ella
289
una vez, es probable que recuerdes cómo usarla.
Incluso si no hay una función estándar o un paquete ampliamente
utilizado para imitar, puedes mantener tus módulos predecibles medi-
ante el uso de estructuras de datos simples y haciendo una cosa única y
enfocada. Muchos de los módulos de análisis de archivos INI en NPM
proporcionan una función que lee directamente tal archivo del disco
duro y lo analiza, por ejemplo. Esto hace que sea imposible de usar tales
módulos en el navegador, donde no tenemos acceso directo al sistema
de archivos, y agrega una complejidad que habría sido mejor abordada
al componer el módulo con alguna función de lectura de archivos.
Lo que apunta a otro aspecto útil del diseño de módulos—la facilidad
con la qué algo se puede componer con otro código. Módulos enfocados
que que computan valores son aplicables en una gama más amplia de
programas que módulos mas grandes que realizan acciones complicadas
con efectos secundarios. Un lector de archivos INI que insista en leer el
archivo desde el disco es inútil en un escenario donde el contenido del
archivo provenga de alguna otra fuente.
Relacionadamente, los objetos con estado son a veces útiles e incluso
necesarios, pero si se puede hacer algo con una función, usa una función.
Varios de los lectores de archivos INI en NPM proporcionan un estilo
de interfaz que requiere que primero debes crear un objeto, luego cargar
el archivo en tu objeto, y finalmente usar métodos especializados para
obtener los resultados. Este tipo de cosas es común en la tradición
orientada a objetos, y es terrible. En lugar de hacer una sola llamada de
función y seguir adelante, tienes que realizar el ritual de mover tu objeto
a través de diversos estados. Y ya que los datos ahora están envueltos
290
en un objeto de tipo especializado, todo el código que interactúa con él
tiene que saber sobre ese tipo, creando interdependencias innecesarias.
A menudo no se puede evitar la definición de nuevas estructuras
de datos—solo unas pocas básicas son provistos por el estándar de
lenguaje, y muchos tipos de datos tienen que ser más complejos que
un array o un mapa. Pero cuando el array es suficiente, usa un array.
Un ejemplo de una estructura de datos un poco más compleja es
el grafo de el Capítulo 7. No hay una sola manera obvia de rep-
resentar un grafo en JavaScript. En ese capítulo, usamos un objeto
cuya propiedades contenian arrays de strings—los otros nodos accesi-
bles desde ese nodo.
Hay varios paquetes de busqueda de rutas diferentes en NPM, pero
ninguno de ellos usa este formato de grafo. Por lo general, estos per-
miten que los bordes del grafo tengan un peso, el costo o la distancia
asociada a ellos, lo que no es posible en nuestra representación.
Por ejemplo, está el paquete dijkstrajs. Un enfoque bien conocido
par la busqueda de rutas, bastante similar a nuestra función encontrarRuta
, se llama el algoritmo de Dijkstra, después de Edsger Dijkstra, quien
fue el primero que lo escribió. El sufijo js a menudo se agrega a los
nombres de los paquetes para indicar el hecho de que están escritos en
JavaScript. Este paquete dijkstrajs utiliza un formato de grafo simi-
lar al nuestro, pero en lugar de arrays, utiliza objetos cuyos valores de
propiedad son números—los pesos de los bordes.
Si quisiéramos usar ese paquete, tendríamos que asegurarnos de que
nuestro grafo fue almacenado en el formato que este espera.
const {find_path} = require("dijkstrajs");
291
let grafo = {};
for (let node of Object.keys(roadGraph)) {
let edges = graph[node] = {};
for (let dest of roadGraph[node]) {
edges[dest] = 1;
}
}
Resumen
Los módulos proporcionan de estructura a programas más grandes al
separar el código en piezas con interfaces y dependencias claras. La
interfaz es la parte del módulo que es visible desde otros módulos, y las
dependencias son los otros módulos este que utiliza.
Debido a que históricamente JavaScript no proporcionó un sistema
de módulos, el sistema CommonJS fue construido encima. Entonces, en
algún momento, consiguio un sistema incorporado, que ahora coexiste
292
incomodamente con el sistema CommonJS.
Un paquete es una porción de código que se puede distribuir por
sí misma. NPM es un repositorio de paquetes de JavaScript. Puedes
descargar todo tipo de paquetes útiles (e inútiles) de él.
Ejercicios
Un robot modular
Estas son las vinculaciones que el proyecto del Capítulo 7 crea:
caminos
construirGrafo
grafoCamino
EstadoPueblo
correrRobot
eleccionAleatoria
robotAleatorio
rutaCorreo
robotRuta
encontrarRuta
robotOrientadoAMetas
293
Módulo de Caminos
Escribe un módulo CommonJS, basado en el ejemplo del Capítulo 7, que
contenga el array de caminos y exporte la estructura de datos grafo que
los representa como grafoCamino. Debería depender de un modulo ./
grafo, que exporta una función construirGrafo que se usa para construir
el grafo. Esta función espera un array de arrays de dos elementos (los
puntos de inicio y final de los caminos).
Dependencias circulares
Una dependencia circular es una situación en donde el módulo A de-
pende de B, y B también, directa o indirectamente, depende de A. Mu-
chos sistemas de módulos simplemente prohíbne esto porque cualquiera
que sea el orden que elijas para cargar tales módulos, no puedes asegu-
rarse de que las dependencias de cada módulo han sido cargadas antes
de que se ejecuten.
Los modulos CommonJS permiten una forma limitada de dependen-
cias cíclicas. Siempre que los módulos no reemplacen a su objeto exports
predeterminado, y no accedan a la interfaz de las demás hasta que ter-
minen de cargar, las dependencias cíclicas están bien.
La función require dada anteriormente en este capítulo es compatible
con este tipo de ciclo de dependencias. Puedes ver cómo maneja los
ciclos? Qué iría mal cuando un módulo en un ciclo reemplace su objeto
exports por defecto?
294
“Quién puede esperar tranquilamente mientras el barro se
asienta?
Quién puede permanecer en calma hasta el momento de
actuar?”
—Laozi, Tao Te Ching
Chapter 11
Programación Asincrónica
La parte central de una computadora, la parte que lleva a cabo los pasos
individuales que componen nuestros programas, es llamada procesador.
Los programas que hemos visto hasta ahora son cosas que mantienen
al procesador ocupado hasta que hayan terminado su trabajo. La ve-
locidad a la que algo como un ciclo que manipule números pueda ser
ejecutado, depende casi completamente de la velocidad del procesador.
Pero muchos programas interactúan con cosas fuera del procesador.
por ejemplo, podrian comunicarse a través de una red de computado-
ras o solicitar datos del disco duro—lo que es mucho más lento que
obtenerlos desde la memoria.
Cuando una cosa como tal este sucediendo, sería una pena dejar que
el procesador se mantenga inactivo—podría haber algún otro trabajo
que este pueda hacer en el mientras tanto. En parte, esto es manejado
por tu sistema operativo, que cambiará el procesador entre múltiples
programas en ejecución. Pero eso no ayuda cuando queremos que un
unico programa pueda hacer progreso mientras este espera una solicitud
de red.
295
Asincronicidad
En un modelo de programación sincrónico, las cosas suceden una a
la vez. Cuando llamas a una función que realiza una acción de larga
duración, solo retorna cuando la acción ha terminado y puede retornar
el resultado. Esto detiene tu programa durante el tiempo que tome la
acción.
Un modelo asincrónico permite que ocurran varias cosas al mismo
tiempo. Cuando comienzas una acción, tu programa continúa ejecután-
dose. Cuando la acción termina, el programa es informado y tiene ac-
ceso al resultado (por ejemplo, los datos leídos del disco).
Podemos comparar a la programación síncrona y asincrónica usando
un pequeño ejemplo: un programa que obtiene dos recursos de la red y
luego combina resultados.
En un entorno síncrono, donde la función de solicitud solo retorna
una vez que ha hecho su trabajo, la forma más fácil de realizar esta
tarea es realizar las solicitudes una después de la otra. Esto tiene el
inconveniente de que la segunda solicitud se iniciará solo cuando la
primera haya finalizado. El tiempo total de ejecución será como minimo
la suma de los dos tiempos de respuesta.
La solución a este problema, en un sistema síncrono, es comenzar hilos
adicionales de control. Un hilo es otro programa activo cuya ejecución
puede ser intercalada con otros programas por el sistema operativo—
ya que la mayoría de las computadoras modernas contienen múlti-
ples procesadores, múltiples hilos pueden incluso ejecutarse al mismo
tiempo, en diferentes procesadores. Un segundo hilo podría iniciar la
segunda solicitud, y luego ambos subprocesos esperan a que los resulta-
296
dos vuelvan, después de lo cual se vuelven a resincronizar para combinar
sus resultados.
En el siguiente diagrama, las líneas gruesas representan el tiempo que
el programa pasa corriendo normalmente, y las líneas finas representan
el tiempo pasado esperando la red. En el modelo síncrono, el tiempo
empleado por la red es parte de la línea de tiempo para un hilo de
control dado. En el modelo asincrónico, comenzar una acción de red
conceptualmente causa una división en la línea del tiempo. El programa
que inició la acción continúa ejecutándose, y la acción ocurre junto a
el, notificando al programa cuando está termina.
synchronous, single thread of control
asynchronous
297
Ambas de las plataformas de programación JavaScript importantes—
navegadores y Node.js—realizan operaciones que pueden tomar un tiempo
asincrónicamente, en lugar de confiar en hilos. Dado que la progra-
mación con hilos es notoriamente difícil (entender lo que hace un pro-
grama es mucho más difícil cuando está haciendo varias cosas a la vez),
esto es generalmente considerado una buena cosa.
Tecnología cuervo
La mayoría de las personas son conscientes del hecho de que los cuer-
vos son pájaros muy inteligentes. Pueden usar herramientas, planear
con anticipación, recordar cosas e incluso comunicarse estas cosas entre
ellos.
Lo que la mayoría de la gente no sabe, es que son capaces de hacer
muchas cosas que mantienen bien escondidas de nosotros. Personas de
buena reputación (un tanto excéntricas) expertas en córvidos, me han
dicho que la tecnología cuervo no esta muy por detrás de la tecnología
humana, y que nos estan alcanzando.
Por ejemplo, muchas culturas cuervo tienen la capacidad de construir
dispositivos informáticos. Estos no son electrónicos, como lo son los dis-
positivos informáticos humanos, pero operan a través de las acciones de
pequeños insectos, una especie estrechamente relacionada con las ter-
mitas, que ha desarrollado una relación simbiótica con los cuervos. Los
pájaros les proporcionan comida, y a cambio los insectos construyen
y operan sus complejas colonias que, con la ayuda de las criaturas
vivientes dentro de ellos, realizan computaciones.
298
Tales colonias generalmente se encuentran en nidos grandes de larga
vida. Las aves e insectos trabajan juntos para construir una red de
estructuras bulbosas hechas de arcilla, escondidas entre las ramitas del
nido, en el que los insectos viven y trabajan.
Para comunicarse con otros dispositivos, estas máquinas usan señales
de luz. Los cuervos incrustan piezas de material reflectante en tallos
de comunicación especial, y los insectos apuntan estos para reflejar la
luz hacia otro nido, codificando los datos como una secuencia de flashes
rápidos. Esto significa que solo los nidos que tienen una conexión visual
ininterrumpida pueden comunicarse entre ellos.
Nuestro amigo, el experto en córvidos, ha mapeado la red de nidos
de cuervo en el pueblo de Hières-sur-Amby, a orillas del río Ródano.
Este mapa muestra los nidos y sus conexiones.
299
Devolución de llamadas
Un enfoque para la programación asincrónica es hacer que las funciones
que realizan una acción lenta, tomen un argumento adicional, una fun-
ción de devolución de llamada. La acción se inicia y, cuando esta
finaliza, la función de devolución es llamada con el resultado.
Como ejemplo, la función setTimeout, disponible tanto en Node.js
como en navegadores, espera una cantidad determinada de milisegundos
(un segundo son mil milisegundos) y luego llama una función.
setTimeout(() => console.log("Tick"), 500);
300
los lugares donde hay comida escondida bajo el nombre "caches de
alimentos", que podría contener un array de nombres que apuntan a
otros datos, que describen el caché real. Para buscar un caché de ali-
mento en los bulbos de almacenamiento del nido Gran Roble, un cuervo
podría ejecutar código como este:
import {granRoble} from "./tecnologia-cuervo";
301
es manejado. Nuestro código puede definir manejadores para tipos de
solicitud específicos, y cuando se recibe una solicitud de este tipo, se
llama al controlador para que este produzca una respuesta.
La interfaz exportada por el módulo "./tecnologia-cuervo" propor-
ciona funciones de devolución de llamada para la comunicación. Los
nidos tienen un método enviar que envía una solicitud. Este espera
el nombre del nido objetivo, el tipo de solicitud y el contenido de la
solicitud como sus primeros tres argumentos, y una función a llamar
cuando llega una respuesta como su cuarto y último argumento.
granRoble.send("Pastura de Vacas", "nota", "Vamos a graznar
fuerte a las 7PM",
() => console.log("Nota entregada."));
Pero para hacer nidos capaces de recibir esa solicitud, primero ten-
emos que definir un tipo de solicitud llamado "nota". El código que
maneja las solicitudes debe ejecutarse no solo en este nido-computadora,
sino en todos los nidos que puedan recibir mensajes de este tipo. Asumire-
mos que un cuervo sobrevuela e instala nuestro código controlador en
todos los nidos.
import {definirTipoSolicitud} from "./tecnologia-cuervo";
302
La función definirTipoSolicitud define un nuevo tipo de solicitud. El
ejemplo agrega soporte para solicitudes de tipo "nota", que simplemente
envían una nota a un nido dado. Nuestra implementación llama a
console.log para que podamos verificar que la solicitud llegó. Los nidos
tienen una propiedad nombre que contiene su nombre.
El cuarto argumento dado al controlador, listo, es una función de
devolución de llamada que debe ser llamada cuando se finaliza con la so-
licitud. Si hubiesemos utilizado el valor de retorno del controlador como
el valor de respuesta, eso significaria que un controlador de solicitud no
puede realizar acciones asincrónicas por sí mismo. Una función que re-
aliza trabajos asíncronos normalmente retorna antes de que el trabajo
este hecho, habiendo arreglado que se llame una devolución de llamada
cuando este completada. Entonces, necesitamos algún mecanismo asín-
crono, en este caso, otra función de devolución de llamada—para indicar
cuándo hay una respuesta disponible.
En cierto modo, la asincronía es contagiosa. Cualquier función que
llame a una función que funcione asincrónicamente debe ser asíncrona
en si misma, utilizando una devolución de llamada o algun mecanismo
similar para entregar su resultado. Llamar devoluciones de llamada es
algo más involucrado y propenso a errores que simplemente retornar un
valor, por lo que necesitar estructurar grandes partes de tu programa
de esa manera no es algo muy bueno.
303
Promesas
Trabajar con conceptos abstractos es a menudo más fácil cuando esos
conceptos pueden ser representados por valores. En el caso de acciones
asíncronas, podrías, en lugar de organizar a una función para que esta
sea llamada en algún momento en el futuro, retornar un objeto que
represente este evento en el futuro.
Esto es para lo que es la clase estándar Promise (“Promesa”). Una
promesa es una acción asíncrona que puede completarse en algún punto
y producir un valor. Esta puede notificar a cualquier persona que esté
interesada cuando su valor este disponible.
La forma más fácil de crear una promesa es llamando a Promise.
resolve (“Promesa.resolver”). Esta función se asegura de que el valor
que le des, sea envuelto en una promesa. Si ya es una promesa, simple-
mente es retornada—de lo contrario, obtienes una nueva promesa que
termina de inmediato con tu valor como su resultado.
let quince = Promise.resolve(15);
quince.then(valor => console.log(`Obtuve ${valor}`));
// → Obtuve 15
304
Pero eso no es todo lo que hace el método then. Este retorna otra
promesa, que resuelve al valor que retorna la función del controlador o,
si esa retorna una promesa, espera por esa promesa y luego resuelve su
resultado.
Es útil pensar acerca de las promesas como dispositivos para mover
valores a una realidad asincrónica. Un valor normal simplemente esta
allí. Un valor prometido es un valor que podría ya estar allí o podría
aparecer en algún momento en el futuro. Las computaciones definidas
en términos de promesas actúan en tales valores envueltos y se ejecutan
de forma asíncrona a medida los valores se vuelven disponibles.
Para crear una promesa, puedes usar Promise como un constructor.
Tiene una interfaz algo extraña—el constructor espera una función
como argumento, a la cual llama inmediatamente, pasando una función
que puede usar para resolver la promesa. Funciona de esta manera,
en lugar de, por ejemplo, con un método resolve, de modo que solo el
código que creó la promesa pueda resolverla.
Así es como crearía una interfaz basada en promesas para la función
leerAlmacenamiento.
almacenamiento(granRoble, "enemigos")
.then(valor => console.log("Obtuve", valor));
305
Esta función asíncrona retorna un valor significativo. Esta es la prin-
cipal ventaja de las promesas—simplifican el uso de funciones asincróni-
cas. En lugar de tener que pasar devoluciones de llamadas, las funciones
basadas en promesas son similares a las normales: toman entradas como
argumentos y retornan su resultado. La única diferencia es que la salida
puede que no este disponible inmediatamente.
Fracaso
Las computaciones regulares en JavaScript pueden fallar lanzando una
excepción. Las computaciones asincrónicas a menudo necesitan algo
así. Una solicitud de red puede fallar, o algún código que sea parte de
la computación asincrónica puede arrojar una excepción.
Uno de los problemas más urgentes con el estilo de devolución de
llamadas en la programación asíncrona es que hace que sea extremada-
mente difícil asegurarte de que las fallas sean reportadas correctamente
a las devoluciones de llamada.
Una convención ampliamente utilizada es que el primer argumento
para la devolución de llamada es usado para indicar que la acción falló, y
el segundo contiene el valor producido por la acción cuando tuvo éxito.
Tales funciones de devolución de llamadas siempre deben verificar si
recibieron una excepción, y asegurarse de que cualquier problema que
causen, incluidas las excepciones lanzadas por las funciones que estas
llaman, sean atrapadas y entregadas a la función correcta.
Las promesas hacen esto más fácil. Estas pueden ser resueltas (la ac-
306
ción termino con éxito) o rechazadas (esta falló). Los controladores de
resolución (registrados con then) solo se llaman cuando la acción es ex-
itosa, y los rechazos se propagan automáticamente a la nueva promesa
que es retornada por then. Y cuando un controlador arroje una ex-
cepción, esto automáticamente hace que la promesa producida por su
llamada then sea rechazada. Entonces, si cualquier elemento en una
cadena de acciones asíncronas falla, el resultado de toda la cadena se
marca como rechazado, y no se llaman más manejadores despues del
punto en donde falló.
Al igual que resolver una promesa proporciona un valor, rechazar
una también proporciona uno, generalmente llamado la razón el rec-
hazo. Cuando una excepción en una función de controlador provoca el
rechazo, el valor de la excepción se usa como la razón. Del mismo modo,
cuando un controlador retorna una promesa que es rechazada, ese rec-
hazo fluye hacia la próxima promesa. Hay una función Promise.reject
que crea una nueva promesa inmediatamente rechazada.
Para manejar explícitamente tales rechazos, las promesas tienen un
método catch (“atraoar”) que registra un controlador para que sea lla-
mado cuando se rechaze la promesa, similar a cómo los manejadores
then manejan la resolución normal. También es muy parecido a then en
que retorna una nueva promesa, que se resuelve en el valor de la promesa
original si esta se resuelve normalmente, y al resultado del controlador
catch de lo contrario. Si un controlador catch lanza un error, la nueva
promesa también es rechazada.
Como una abreviatura, then también acepta un manejador de rechazo
como segundo argumento, por lo que puedes instalar ambos tipos de
307
controladores en un solo método de llamada.
Una función que se pasa al constructor Promise recibe un segundo
argumento, junto con la función de resolución, que puede usar para
rechazar la nueva promesa.
Las cadenas de promesas creadas por llamadas a then y catch puede
verse como una tubería a través de la cual los valores asíncronicos o
las fallas se mueven. Dado que tales cadenas se crean mediante el
registro de controladores, cada enlace tiene un controlador de éxito o
un controlador de rechazo (o ambos) asociados a ello. Controladores que
no coinciden con ese tipo de resultados (éxito o fracaso) son ignorados.
Pero los que sí coinciden son llamados, y su resultado determina qué
tipo de valor viene después—éxito cuando retorna un valor que no es
una promesa, rechazo cuando arroja una excepción, y el resultado de
una promesa cuando retorna una de esas.
Al igual que una excepción no detectada es manejada por el entorno,
Los entornos de JavaScript pueden detectar cuándo una promesa rec-
hazada no es manejada, y reportará esto como un error.
308
de un determinado período de no obtener una respuesta, una solicitud
expirará e informara de un fracaso.
A menudo, las fallas de transmisión son accidentes aleatorios, como
la luz del faro de un auto interfieriendo con las señales de luz, y simple-
mente volver a intentar la solicitud puede hacer que esta tenga éxito.
Entonces, mientras estamos en eso, hagamos que nuestra función de
solicitud automáticamente reintente el envío de la solicitud momentos
antes de que se de por vencida.
Y, como hemos establecido que las promesas son algo bueno, tam-
bien haremos que nuestra función de solicitud retorne una promesa.
En términos de lo que pueden expresar, las devoluciones de llamada y
las promesas son equivalentes. Las funciones basadas en devoluciones
de llamadas se pueden envolver para exponer una interfaz basada en
promesas, y viceversa.
Incluso cuando una solicitud y su respuesta sean entregadas exitosa-
mente, la respuesta puede indicar un error—por ejemplo, si la solicitud
intenta utilizar un tipo de solicitud que no haya sido definida o si el con-
trolador genera un error. Para soportar esto, send y definirTipoSolicitud
siguen la convención mencionada anteriormente, donde el primer ar-
gumento pasado a las devoluciones de llamada es el motivo del fallo, si
lo hay, y el segundo es el resultado real.
Estos pueden ser traducidos para prometer resolución y rechazo por
parte de nuestra envoltura.
class TiempoDeEspera extends Error {}
309
return new Promise((resolve, reject) => {
let listo = false;
function intentar(n) {
nido.send(objetivo, tipo, contenido, (fallo, value) => {
listo = true;
if (fallo) reject(fallo);
else resolve(value);
});
setTimeout(() => {
if (listo) return;
else if (n < 3) intentar(n + 1);
else reject(new TiempoDeEspera("Tiempo de espera agotado
"));
}, 250);
}
intentar(1);
});
}
310
intento de enviar una solicitud. También establece un tiempo de espera
que, si no ha regresado una respuesta después de 250 milisegundos,
comienza el próximo intento o, si este es el cuarto intento, rechaza la
promesa con una instancia de TiempoDeEspera como la razón.
Volver a intentar cada cuarto de segundo y rendirse cuando no ha lle-
gado ninguna respuesta después de un segundo es algo definitivamente
arbitrario. Es incluso posible, si la solicitud llegó pero el controlador
se esta tardando un poco más, que las solicitudes se entreguen varias
veces. Escribiremos nuestros manejadores con ese problema en mente—
los mensajes duplicados deberían de ser inofensivos.
En general, no construiremos una red robusta de clase mundial hoy.
Pero eso esta bien—los cuervos no tienen expectativas muy altas todavía
cuando se trata de la computación.
Para aislarnos por completo de las devoluciones de llamadas, seguire-
mos adelante y también definiremos un contenedor para definirTipoSolicit
que permite que la función controlador pueda retornar una promesa o
valor normal, y envia eso hasta la devolución de llamada para nosotros.
function tipoSolicitud(nombre, manejador) {
definirTipoSolicitud(nombre, (nido, contenido, fuente,
devolucionDeLlamada) => {
try {
Promise.resolve(manejador(nido, contenido, fuente))
.then(response => devolucionDeLlamada(null, response),
failure => devolucionDeLlamada(failure));
} catch (exception) {
devolucionDeLlamada(exception);
}
311
});
}
Colecciones de promesas
Cada computadora nido mantiene un array de otros nidos dentro de
la distancia de transmisión en su propiedad vecinos. Para verificar
cuáles de esos son actualmente accesibles, puede escribir una función
que intente enviar un solicitud "ping" (una solicitud que simplemente
pregunta por una respuesta) para cada de ellos, y ver cuáles regresan.
Al trabajar con colecciones de promesas que se ejecutan al mismo
tiempo, la función Promise.all puede ser útil. Esta retorna una promesa
que espera a que se resuelvan todas las promesas del array, y luego
resuelve un array de los valores que estas promesas produjeron (en el
mismo orden que en el array original). Si alguna promesa es rechazada,
312
el el resultado de Promise.all es en sí mismo rechazado.
tipoSolicitud("ping", () => "pong");
function vecinosDisponibles(nido) {
let solicitudes = nido.vecinos.map(vecino => {
return request(nido, vecino, "ping")
.then(() => true, () => false);
});
return Promise.all(solicitudes).then(resultado => {
return nido.vecinos.filter((_, i) => resultado[i]);
});
}
313
Inundación de red
El hecho de que los nidos solo pueden hablar con sus vecinos inhibe en
gran cantidad la utilidad de esta red.
Para transmitir información a toda la red, una solución es configurar
un tipo de solicitud que sea reenviada automáticamente a los vecinos.
Estos vecinos luego la envían a sus vecinos, hasta que toda la red ha
recibido el mensaje.
import {todosLados} from "./tecnologia-cuervo";
todosLados(nido => {
nido.estado.chismorreo = [];
});
314
Para evitar enviar el mismo mensaje a traves de la red por siempre,
cada nido mantiene un array de strings de chismorreos que ya ha visto.
Para definir este array, usaremos la función todosLados—que ejecuta
código en todos los nidos—para añadir una propiedad al objeto estado
del nido, que es donde mantendremos estado local del nido.
Cuando un nido recibe un mensaje de chisme duplicado, lo cual es
muy probable que suceda con todo el mundo reenviando estos a ciegas,
lo ignora. Pero cuando recibe un mensaje nuevo, emocionadamente le
dice a todos sus vecinos a excepción de quien le envió el mensaje.
Esto provocará que una nueva pieza de chismes se propague a través
de la red como una mancha de tinta en agua. Incluso cuando algunas
conexiones no estan trabajando actualmente, si hay una ruta alternativa
a un nido dado, el chisme llegará hasta allí.
Este estilo de comunicación de red se llama inundamiento-inunda la
red con una pieza de información hasta que todos los nodos la tengan.
Enrutamiento de mensajes
Si un nodo determinado quiere hablar unicamente con otro nodo, la
inundación no es un enfoque muy eficiente. Especialmente cuando la
red es grande, daría lugar a una gran cantidad de transferencias de
datos inútiles.
Un enfoque alternativo es configurar una manera en que los mensajes
salten de nodo a nodo, hasta que lleguen a su destino. La dificultad
con eso es que requiere de conocimiento sobre el diseño de la red. Para
315
enviar una solicitud hacia la dirección de un nido lejano, es necesario
saber qué nido vecino lo acerca más a su destino. Enviar la solicitud en
la dirección equivocada no servirá de mucho.
Dado que cada nido solo conoce a sus vecinos directos, no tiene la
información que necesita para calcular una ruta. De alguna manera
debemos extender la información acerca de estas conexiones a todos
los nidos. Preferiblemente en una manera que permita ser cambiada
con el tiempo, cuando los nidos son abandonados o nuevos nidos son
construidos.
Podemos usar la inundación de nuevo, pero en lugar de verificar si un
determinado mensaje ya ha sido recibido, ahora verificamos si el nuevo
conjunto de vecinos de un nido determinado coinciden con el conjunto
actual que tenemos para él.
tipoSolicitud("conexiones", (nido, {nombre, vecinos},
fuente) => {
let conexiones = nido.estado.conexiones;
if (JSON.stringify(conexiones.get(nombre)) ==
JSON.stringify(vecinos)) return;
conexiones.set(nombre, vecinos);
difundirConexiones(nido, nombre, fuente);
});
316
});
}
}
todosLados(nido => {
nido.estado.conexiones = new Map;
nido.estado.conexiones.set(nido.nombre, nido.vecinos);
difundirConexiones(nido, nido.nombre);
});
317
for (let i = 0; i < trabajo.length; i++) {
let {donde, via} = trabajo[i];
for (let siguiente of conexiones.get(donde) || []) {
if (siguiente == hasta) return via;
if (!trabajo.some(w => w.donde == siguiente)) {
trabajo.push({donde: siguiente, via: via || siguiente});
}
}
}
return null;
}
318
}
Funciones asíncronas
Para almacenar información importante, se sabe que los cuervos la du-
plican a través de los nidos. De esta forma, cuando un halcón destruye
un nido, la información no se pierde.
Para obtener una pieza de información dada que no este en su propia
bulbo de almacenamiento, una computadora nido puede consultar otros
nidos al azar en la red hasta que encuentre uno que la tenga.
tipoSolicitud("almacenamiento", (nido, nombre) => almacenamiento(
nido, nombre));
319
function encontrarEnAlmacenamiento(nido, nombre) {
return almacenamiento(nido, nombre).then(encontrado => {
if (encontrado != null) return encontrado;
else return encontrarEnAlmacenamientoRemoto(nido, nombre);
});
}
function red(nido) {
return Array.from(nido.estado.conexiones.keys());
}
320
Como conexiones es un Map, Object.keys no funciona en él. Este tiene
un metódo keys, pero que retorna un iterador en lugar de un array. Un
iterador (o valor iterable) se puede convertir a un array con la función
Array.from.
Incluso con promesas, este es un código bastante incómodo. Múltiples
acciones asincrónicas están encadenadas juntas de maneras no-obvias.
Nosotros de nuevo necesitamos una función recursiva (siguiente) para
modelar ciclos a través de nidos.
Y lo que el código realmente hace es completamente lineal—siempre
espera a que se complete la acción anterior antes de comenzar la sigu-
iente. En un modelo de programación sincrónica, sería más simple de
expresar.
La buena noticia es que JavaScript te permite escribir código pseudo-
sincrónico. Una función async es una función que retorna implícita-
mente una promesa y que puede, en su cuerpo, await (“esperar”) otras
promesas de una manera que se ve sincrónica.
Podemos reescribir encontrarEnAlmacenamiento de esta manera:
async function encontrarEnAlmacenamiento(nido, nombre) {
let local = await almacenamiento(nido, nombre);
if (local != null) return local;
321
try {
let encontrado = await solicitudRuta(nido, fuente, "
almacenamiento",
nombre);
if (encontrado != null) return encontrado;
} catch (_) {}
}
throw new Error("No encontrado");
}
322
Generadores
Esta capacidad de las funciones para pausar y luego reanudarse nue-
vamente no es exclusiva para las funciones async. JavaScript también
tiene una caracteristica llamada funciones generador. Estss son simi-
lares, pero sin las promesas.
Cuando defines una función con function* (colocando un asterisco
después de la palabra function), se convierte en un generador. Cuando
llamas un generador, este retorna un iterador, que ya vimos en el Capí-
tulo 6.
function* potenciacion(n) {
for (let actual = n;; actual *= n) {
yield actual;
}
}
323
pausa y causa que el valor arrojado se convierta en el siguiente valor
producido por el iterador. Cuando la función retorne (la del ejemplo
nunca lo hace), el iterador está completo.
Escribir iteradores es a menudo mucho más fácil cuando usas fun-
ciones generadoras. El iterador para la clase grupal (del ejercicio en el
Capítulo 6) se puede escribir con este generador:
Conjunto.prototype[Symbol.iterator] = function*() {
for (let i = 0; i < this.miembros.length; i++) {
yield this.miembros[i];
}
};
324
El ciclo de evento
Los programas asincrónicos son ejecutados pieza por pieza. Cada pieza
puede iniciar algunas acciones y programar código para que se ejecute
cuando la acción termine o falle. Entre estas piezas, el programa per-
manece inactivo, esperando por la siguiente acción.
Por lo tanto, las devoluciones de llamada no son llamadas directa-
mente por el código que las programó. Si llamo a setTimeout desde
adentro de una función, esa función habra retornado para el momento
en que se llame a la función de devolución de llamada. Y cuando la
devolución de llamada retorne, el control no volvera a la función que la
programo.
El comportamiento asincrónico ocurre en su propia función de lla-
mada de pila vacía. Esta es una de las razones por las cuales, sin
promesas, la gestión de excepciones en el código asincrónico es dificil.
Como cada devolución de llamada comienza con una pila en su mayoría
vacía, tus manejadores catch no estarán en la pila cuando lanzen una
excepción.
try {
setTimeout(() => {
throw new Error("Woosh");
}, 20);
} catch (_) {
// Esto no se va a ejecutar
console.log("Atrapado!");
}
325
No importa que tan cerca los eventos—como tiempos de espera o so-
licitudes entrantes—sucedan, un entorno de JavaScript solo ejecutará
un programa a la vez. Puedes pensar en esto como un gran ciclo alrede-
dor de tu programa, llamado ciclo de evento. Cuando no hay nada que
hacer, ese bucle está detenido. Pero a medida que los eventos entran, se
agregan a una cola, y su código se ejecuta uno después del otro. Porque
no hay dos cosas que se ejecuten al mismo tiempo, código de ejecución
lenta puede retrasar el manejo de otros eventos.
Este ejemplo establece un tiempo de espera, pero luego se retrasa
hasta después del tiempo de espera previsto, lo que hace que el tiempo
de espera este tarde.
let comienzo = Date.now();
setTimeout(() => {
console.log("Tiempo de espera corrio al ", Date.now() -
comienzo);
}, 20);
while (Date.now() < comienzo + 50) {}
console.log("Se desperdicio tiempo hasta el ", Date.now() -
comienzo);
// → Se desperdicio tiempo hasta el 50
// → Tiempo de espera corrio al 55
326
Promise.resolve("Listo").then(console.log);
console.log("Yo primero!");
// → Yo primero!
// → Listo
Errores asincrónicos
Cuando tu programa se ejecuta de forma síncrona, de una sola vez,
no hay cambios de estado sucediendo aparte de aquellos que el mismo
programa realiza. Para los programas asíncronos, esto es diferente—
estos pueden tener brechas en su ejecución durante las cuales se podria
ejecutar otro código.
Veamos un ejemplo. Uno de los pasatiempos de nuestros cuervos
es contar la cantidad de polluelos que nacen en el pueblo cada año.
Los nidos guardan este recuento en sus bulbos de almacenamiento. El
siguiente código intenta enumerar los recuentos de todos los nidos para
un año determinado.
function cualquierAlmacenamiento(nido, fuente, nombre) {
if (fuente == nido.nombre) return almacenamiento(nido, nombre);
else return solicitudRuta(nido, fuente, "almacenamiento",
nombre);
}
327
async function polluelos(nido, años) {
let lista = "";
await Promise.all(red(nido).map(async nombre => {
lista += `${nombre}: ${
await cualquierAlmacenamiento(nido, nombre, `polluelos en $
{años}`)
}\n`;
}));
return lista;
}
La parte async nombre => muestra que las funciones de flecha también
pueden ser async al poner la palabra async delante de ellas.
El código no parece sospechoso de inmediato... mapea la función de
flecha async sobre el conjunto de nidos, creando una serie de promesas,
y luego usa Promise.all para esperar a todos estas antes de retornar la
lista que estas construyen.
Pero está seriamente roto. Siempre devolverá solo una línea de salida,
enumerando al nido que fue más lento en responder.
Puedes averiguar por qué?
El problema radica en el operador +=, que toma el valor actual de
lista en el momento en que la instrucción comienza a ejecutarse, y
luego, cuando el await termina, establece que la vinculaciòn lista sea
ese valor más el string agregado.
Pero entre el momento en el que la declaración comienza a ejecutarse
y el momento donde termina hay una brecha asincrónica. La expresión
map se ejecuta antes de que se haya agregado algo a la lista, por lo que
328
cada uno de los operadores += comienza desde un string vacío y termina
cuando su recuperación de almacenamiento finaliza, estableciendo lista
como una lista de una sola línea—el resultado de agregar su línea al
string vacío.
Esto podría haberse evitado fácilmente retornando las líneas de las
promesas mapeadas y llamando a join en el resultado de Promise.all, en
lugar de construir la lista cambiando una vinculación. Como siempre,
calcular nuevos valores es menos propenso a errores que cambiar valores
existentes.
async function polluelos(nido, año) {
let lineas = red(nido).map(async nombre => {
return nombre + ": " +
await cualquierAlmacenamiento(nido, nombre, `polluelos en $
{año}`);
});
return (await Promise.all(lineas)).join("\n");
}
329
Resumen
La programación asincrónica permite expresar la espera de acciones de
larga duración sin congelar el programa durante estas acciones. Los
entornos de JavaScript suelen implementar este estilo de programación
usando devoluciones de llamada, funciones que son llaman cuando las
acciones son completadas. Un ciclo de eventos planifica que dichas
devoluciones de llamadas sean llamadas cuando sea apropiado, una de-
spués de la otra, para que sus ejecuciones no se superpongan.
La programación asíncrona se hace más fácil mediante promesas, ob-
jetos que representar acciones que podrían completarse en el futuro, y
funciones async, que te permiten escribir un programa asíncrono como
si fuera sincrónico.
Ejercicios
Siguiendo el bisturí
Los cuervos del pueblo poseen un viejo bisturí que ocasionalmente usan
en misiones especiales—por ejemplo, para cortar puertas de malla o
embalar cosas. Para ser capaces de rastrearlo rápidamente, cada vez que
se mueve el bisturí a otro nido, una entrada se agrega al almacenamiento
tanto del nido que lo tenía como al nido que lo tomó, bajo el nombre
"bisturí", con su nueva ubicación como su valor.
Esto significa que encontrar el bisturí es una cuestión de seguir la ruta
de navegación de las entradas de almacenamiento, hasta que encuentres
un nido que apunte a el nido en si mismo.
330
Escribe una función async, localizarBisturi que haga esto, comen-
zando en el nido en el que se ejecute. Puede usar la función cualquierAlmace
definida anteriormente para acceder al almacenamiento en nidos arbi-
trarios. El bisturí ha estado dando vueltas el tiempo suficiente como
para que puedas suponer que cada nido tiene una entrada bisturí en
su almacenamiento de datos.
Luego, vuelve a escribir la misma función sin usar async y await.
Las fallas de solicitud se muestran correctamente como rechazos de
la promesa devuelta en ambas versiones? Cómo?
Construyendo Promise.all
Dado un array de promesas, Promise.all retorna una promesa que es-
pera a que finalicen todas las promesas del array. Entonces tiene éxito,
produciendo un array de valores de resultados. Si una promesa en el
array falla, la promesa retornada por all también falla, con la razón de
la falla proveniente de la promesa fallida.
Implemente algo como esto tu mismo como una función regular lla-
mada Promise_all.
Recuerda que una vez que una promesa ha tenido éxito o ha fallado,
no puede tener éxito o fallar de nuevo, y llamadas subsecuentes a las
funciones que resuelven son ignoradas. Esto puede simplificar la forma
en que manejas la falla de tu promesa.
331
“El evaluador, que determina el significado de las expresiones
en un lenguaje de programación, es solo otro programa.”
—Hal Abelson and Gerald Sussman, Structure and
Interpretation of Computer Programs
Chapter 12
Proyecto: Un Lenguaje de
Programación
Construir tu propio lenguaje de programación es sorprendentemente
fácil (siempre y cuando no apuntes demasiado alto) y muy esclarecedor.
Lo principal que quiero mostrar en este capítulo es que no hay ma-
gia involucrada en la construcción de tu propio lenguaje. A menudo
he sentido que algunos inventos humanos eran tan inmensamente in-
teligentes y complicados que nunca podría llegar a entenderlos. Pero
con un poco de lectura y experimentación, a menudo resultan ser bas-
tante mundanos.
Construiremos un lenguaje de programación llamado Egg. Será un
lenguaje pequeño y simple—pero lo suficientemente poderoso como para
expresar cualquier computación que puedes pensar. Permitirá una ab-
stracción simple basada en funciones.
332
Análisis
La parte más visible de un lenguaje de programación es su sintaxis, o
notación. Un analizador es un programa que lee una pieza de texto y
produce una estructura de datos que refleja la estructura del programa
contenido en ese texto. Si el texto no forma un programa válido, el
analizador debe señalar el error.
Nuestro lenguaje tendrá una sintaxis simple y uniforme. Todo en
Egg es una expresión. Una expresión puede ser el nombre de una vin-
culación (binding), un número, una cadena de texto o una aplicación.
Las aplicaciones son usadas para llamadas de función pero también para
constructos como if o while.
Para mantener el analizador simple, las cadenas en Egg no soportarán
nada parecido a escapes de barra invertida. Una cadena es simplemente
una secuencia de caracteres que no sean comillas dobles, envueltas en
comillas dobles. Un número es un secuencia de dígitos. Los nombres
de vinculaciones pueden consistir en cualquier carácter no que sea un
espacio en blanco y eso no tiene un significado especial en la sintaxis.
Las aplicaciones se escriben tal y como están en JavaScript, poniendo
paréntesis después de una expresión y teniendo cualquier cantidad de
argumentos entre esos paréntesis, separados por comas.
hacer(definir(x, 10),
si(>(x, 5),
imprimir("grande"),
imprimir("pequeño")))
333
La uniformidad del lenguaje Egg significa que las cosas que son op-
eradores en JavaScript (como >) son vinculaciones normales en este
lenguaje, aplicadas como cualquier función. Y dado que la sintaxis no
tiene un concepto de bloque, necesitamos una construcción hacer para
representar el hecho de realizar múltiples cosas en secuencia.
La estructura de datos que el analizador usará para describir un pro-
grama consta de objetos de expresión, cada uno de los cuales tiene
una propiedad tipo que indica el tipo de expresión que este es y otras
propiedades que describen su contenido.
Las expresiones de tipo "valor" representan strings o números lit-
erales. Su propiedad valor contiene el string o valor numérico que estos
representan. Las expresiones de tipo "palabra" se usan para identifi-
cadores (nombres). Dichos objetos tienen una propiedad nombre que
contienen el nombre del identificador como un string. Finalmente, las
expresiones "aplicar" representan aplicaciones. Tienen una propiedad
operador que se refiere a la expresión que está siendo aplicada, y una
propiedad argumentos que contiene un array de expresiones de argumen-
tos.
La parte >(x, 5) del programa anterior se representaría de esta man-
era:
{
tipo: "aplicar",
operador: {tipo: "palabra", nombre: ">"},
argumentos: [
{tipo: "palabra", nombre: "x"},
{tipo: "valor", valor: 5}
]
334
}
335
línea.
Aquí debemos encontrar un enfoque diferente. Las expresiones no
están separadas en líneas, y tienen una estructura recursiva. Las expre-
siones de aplicaciones contienen otras expresiones.
Afortunadamente, este problema se puede resolver muy bien escribi-
endo una función analizadora que es recursiva en una manera que refleje
la naturaleza recursiva del lenguaje.
Definimos una función analizarExpresion, que toma un string como
entrada y retorna un objeto que contiene la estructura de datos para
la expresión al comienzo del string, junto con la parte del string que
queda después de analizar esta expresión Al analizar subexpresiones (el
argumento para una aplicación, por ejemplo), esta función puede ser
llamada de nuevo, produciendo la expresión del argumento, así como
al texto que permanece. A su vez, este texto puede contener más ar-
gumentos o puede ser el paréntesis de cierre que finaliza la lista de
argumentos.
Esta es la primera parte del analizador:
function analizarExpresion(programa) {
programa = saltarEspacio(programa);
let emparejamiento, expresion;
if (emparejamiento = /^"([^"]*)"/.exec(programa)) {
expresion = {tipo: "valor", valor: emparejamiento[1]};
} else if (emparejamiento = /^\d+\b/.exec(programa)) {
expresion = {tipo: "valor", valor: Number(emparejamiento[0])
};
} else if (emparejamiento = /^[^\s(),"]+/.exec(programa)) {
expresion = {tipo: "palabra", nombre: emparejamiento[0]};
336
} else {
throw new SyntaxError("Sintaxis inesperada: " + programa);
}
function saltarEspacio(string) {
let primero = string.search(/\S/);
if (primero == -1) return "";
return string.slice(primero);
}
337
no válido.
Luego cortamos la parte que coincidio del string del programa y
pasamos eso, junto con el objeto para la expresión, a aplicarAnalisis,
el cual verifica si la expresión es una aplicación. Si es así, analiza una
lista de los argumentos entre paréntesis.
function aplicarAnalisis(expresion, programa) {
programa = saltarEspacio(programa);
if (programa[0] != "(") {
return {expresion: expresion, resto: programa};
}
programa = saltarEspacio(programa.slice(1));
expresion = {tipo: "aplicar", operador: expresion, argumentos:
[]};
while (programa[0] != ")") {
let argumento = analizarExpresion(programa);
expresion.argumentos.push(argumento.expresion);
programa = saltarEspacio(argumento.resto);
if (programa[0] == ",") {
programa = saltarEspacio(programa.slice(1));
} else if (programa[0] != ")") {
throw new SyntaxError("Experaba ',' o ')'");
}
}
return aplicarAnalisis(expresion, programa.slice(1));
}
338
esto no es una aplicación, y aplicarAnalisis retorna la expresión que se
le dio.
De lo contrario, salta el paréntesis de apertura y crea el objeto de
árbol de sintaxis para esta expresión de aplicación. Entonces, recursiva-
mente llama a analizarExpresion para analizar cada argumento hasta
que se encuentre el paréntesis de cierre. La recursión es indirecta, a
través de aplicarAnalisis y analizarExpresion llamando una a la otra.
Dado que una expresión de aplicación puede ser aplicada a sí misma
(como en multiplicador(2)(1)), aplicarAnalisis debe, después de haber
analizado una aplicación, llamarse asi misma de nuevo para verificar si
otro par de paréntesis sigue a continuación.
Esto es todo lo que necesitamos para analizar Egg. Envolvemos esto
en una conveniente función analizar que verifica que ha llegado al final
del string de entrada después de analizar la expresión (un programa Egg
es una sola expresión), y eso nos da la estructura de datos del programa.
function analizar(programa) {
let {expresion, resto} = analizarExpresion(programa);
if (saltarEspacio(resto).length > 0) {
throw new SyntaxError("Texto inesperado despues de programa")
;
}
return expresion;
}
console.log(analizar("+(a, 10)"));
// → {tipo: "aplicar",
// operador: {tipo: "palabra", nombre: "+"},
339
// argumentos: [{tipo: "palabra", nombre: "a"},
// {tipo: "valor", valor: 10}]}
The evaluator
What can we do with the syntax tree for a program? Run it, of course!
And that is what the evaluator does. You give it a syntax tree and
a scope object that associates names with values, and it will evaluate
the expression that the tree represents and return the value that this
produces.
const specialForms = Object.create(null);
340
} else if (expresion.type == "apply") {
let {operator, args} = expresion;
if (operator.type == "word" &&
operator.name in specialForms) {
return specialForms[operator.name](expresion.args, scope);
} else {
let op = evaluate(operator, scope);
if (typeof op == "function") {
return op(...args.map(arg => evaluate(arg, scope)));
} else {
throw new TypeError("Applying a non-function.");
}
}
}
}
The evaluator has code for each of the expression types. A literal
value expression produces its value. (For example, the expression 100
just evaluates to the number 100.) For a binding, we must check
whether it is actually defined in the scope and, if it is, fetch the binding’s
value.
Applications are more involved. If they are a special form, like if,
we do not evaluate anything and pass the argument expressions, along
with the scope, to the function that handles this form. If it is a normal
call, we evaluate the operator, verify that it is a function, and call it
with the evaluated arguments.
We use plain JavaScript function values to represent Egg’s function
values. We will come back to this later, when the special form called
341
fun is defined.
The recursive structure of evaluate resembles the similar structure
of the parser, and both mirror the structure of the language itself. It
would also be possible to integrate the parser with the evaluator and
evaluate during parsing, but splitting them up this way makes the pro-
gram clearer.
This is really all that is needed to interpret Egg. It is that simple.
But without defining a few special forms and adding some useful values
to the environment, you can’t do much with this language yet.
Special forms
The specialForms object is used to define special syntax in Egg. It
associates words with functions that evaluate such forms. It is currently
empty. Let’s add if.
specialForms.si = (args, scope) => {
if (args.length != 3) {
throw new SyntaxError("Wrong number of args to si");
} else if (evaluate(args[0], scope) !== false) {
return evaluate(args[1], scope);
} else {
return evaluate(args[2], scope);
}
};
342
the first, and if the result isn’t the value false, it will evaluate the
second. Otherwise, the third gets evaluated. This if form is more
similar to JavaScript’s ternary ?: operator than to JavaScript’s if. It
is an expression, not a statement, and it produces a value, namely the
result of the second or third argument.
Egg also differs from JavaScript in how it handles the condition value
to if. It will not treat things like zero or the empty string as false, only
the precise value false.
The reason we need to represent if as a special form, rather than a
regular function, is that all arguments to functions are evaluated before
the function is called, whereas if should evaluate only either its second
or its third argument, depending on the value of the first.
The while form is similar.
specialForms.while = (args, scope) => {
if (args.length != 2) {
throw new SyntaxError("Wrong number of args to while");
}
while (evaluate(args[0], scope) !== false) {
evaluate(args[1], scope);
}
Another basic building block is hacer, which executes all its argu-
343
ments from top to bottom. Its value is the value produced by the last
argument.
specialForms.hacer = (args, scope) => {
let value = false;
for (let arg of args) {
value = evaluate(arg, scope);
}
return value;
};
344
The environment
The scope accepted by evaluate is an object with properties whose
names correspond to binding names and whose values correspond to the
values those bindings are bound to. Let’s define an object to represent
the global scope.
To be able to use the if construct we just defined, we must have
access to Boolean values. Since there are only two Boolean values, we
do not need special syntax for them. We simply bind two names to the
values true and false and use those.
const topScope = Object.create(null);
topScope.true = true;
topScope.false = false;
345
for (let op of ["+", "-", "*", "/", "==", "<", ">"]) {
topScope[op] = Function("a, b", `return a ${op} b;`);
}
346
hacer(definir(total, +(total, count)),
definir(count, +(count, 1)))),
imprimir(total))
`);
// → 55
This is the program we’ve seen several times before, which computes
the sum of the numbers 1 to 10, expressed in Egg. It is clearly uglier
than the equivalent JavaScript program—but not bad for a language
implemented in less than 150 lines of code.
Functions
A programming language without functions is a poor programming lan-
guage indeed.
Fortunately, it isn’t hard to add a fun construct, which treats its last
argument as the function’s body and uses all arguments before that as
the names of the function’s parameters.
specialForms.fun = (args, scope) => {
if (!args.length) {
throw new SyntaxError("Functions need a body");
}
let body = args[args.length - 1];
let params = args.slice(0, args.length - 1).map(expr => {
if (expr.type != "word") {
throw new SyntaxError("Parameter names must be words");
}
347
return expr.name;
});
return function() {
if (arguments.length != params.length) {
throw new TypeError("Wrong number of arguments");
}
let localScope = Object.create(scope);
for (let i = 0; i < arguments.length; i++) {
localScope[params[i]] = arguments[i];
}
return evaluate(body, localScope);
};
};
Functions in Egg get their own local scope. The function produced
by the fun form creates this local scope and adds the argument bindings
to it. It then evaluates the function body in this scope and returns the
result.
run(`
hacer(definir(plusOne, fun(a, +(a, 1))),
imprimir(plusOne(10)))
`);
// → 11
run(`
hacer(definir(pow, fun(base, exp,
si(==(exp, 0),
348
1,
*(base, pow(base, -(exp, 1)))))),
imprimir(pow(2, 10)))
`);
// → 1024
Compilation
What we have built is an interpreter. During evaluation, it acts directly
on the representation of the program produced by the parser.
Compilation is the process of adding another step between the pars-
ing and the running of a program, which transforms the program into
something that can be evaluated more efficiently by doing as much
work as possible in advance. For example, in well-designed languages it
is obvious, for each use of a binding, which binding is being referred to,
without actually running the program. This can be used to avoid look-
ing up the binding by name every time it is accessed, instead directly
fetching it from some predetermined memory location.
Traditionally, compilation involves converting the program to ma-
chine code, the raw format that a computer’s processor can execute.
But any process that converts a program to a different representation
can be thought of as compilation.
It would be possible to write an alternative evaluation strategy for
Egg, one that first converts the program to a JavaScript program, uses
Function to invoke the JavaScript compiler on it, and then runs the
349
result. When done right, this would make Egg run very fast while still
being quite simple to implement.
If you are interested in this topic and willing to spend some time on
it, I encourage you to try to implement such a compiler as an exercise.
Cheating
When we defined if and while, you probably noticed that they were
more or less trivial wrappers around JavaScript’s own if and while.
Similarly, the values in Egg are just regular old JavaScript values.
If you compare the implementation of Egg, built on top of JavaScript,
with the amount of work and complexity required to build a program-
ming language directly on the raw functionality provided by a machine,
the difference is huge. Regardless, this example hopefully gave you an
impression of the way programming languages work.
And when it comes to getting something done, cheating is more ef-
fective than doing everything yourself. Though the toy language in this
chapter doesn’t do anything that couldn’t be done better in JavaScript,
there are situations where writing small languages helps get real work
done.
Such a language does not have to resemble a typical programming
language. If JavaScript didn’t come equipped with regular expressions,
for example, you could write your own parser and evaluator for regular
expressions.
Or imagine you are building a giant robotic dinosaur and need to
program its behavior. JavaScript might not be the most effective way
350
to do this. You might instead opt for a language that looks like this:
behavior walk
perform when
destination ahead
actions
move left-foot
move right-foot
behavior attack
perform when
Godzilla in-view
actions
fire laser-eyes
launch arm-rockets
Exercises
Arrays
Add support for arrays to Egg by adding the following three functions
to the top scope: array(...values) to construct an array containing the
351
argument values, length(array) to get an array’s length, and element(
array, n)to fetch the nth element from an array.
Closure
The way we have defined fun allows functions in Egg to reference the
surrounding scope, allowing the function’s body to use local values that
were visible at the time the function was defined, just like JavaScript
functions do.
The following program illustrates this: function f returns a function
that adds its argument to f’s argument, meaning that it needs access
to the local scope inside f to be able to use binding a.
run(`
hacer(definir(f, fun(a, fun(b, +(a, b)))),
imprimir(f(4)(5)))
`);
// → 9
Go back to the definition of the fun form and explain which mecha-
nism causes this to work.
Comments
It would be nice if we could write comments in Egg. For example,
whenever we find a hash sign (#), we could treat the rest of the line as
a comment and ignore it, similar to // in JavaScript.
352
We do not have to make any big changes to the parser to support
this. We can simply change skipSpace to skip comments as if they are
whitespace so that all the points where skipSpace is called will now also
skip comments. Make this change.
Fixing scope
Currently, the only way to assign a binding a value is definir. This
construct acts as a way both to define new bindings and to give existing
ones a new value.
This ambiguity causes a problem. When you try to give a nonlocal
binding a new value, you will end up defining a local one with the same
name instead. Some languages work like this by design, but I’ve always
found it an awkward way to handle scope.
Add a special form set, similar to definir, which gives a binding a
new value, updating the binding in an outer scope if it doesn’t already
exist in the inner scope. If the binding is not defined at all, throw a
ReferenceError (another standard error type).
The technique of representing scopes as simple objects, which has
made things convenient so far, will get in your way a little at this
point. You might want to use the Object.getPrototypeOf function, which
returns the prototype of an object. Also remember that scopes do not
derive from Object.prototype, so if you want to call hasOwnProperty on
them, you have to use this clumsy expression:
Object.prototype.hasOwnProperty.call(scope, name);
353
“¡Tanto peor! ¡Otra vez la vieja historia! Cuando uno ha
acabado de construir su casa advierte que, mientras la
construía, ha aprendido, sin darse cuenta, algo que tendría
que haber sabido absolutamente antes de comenzar a
construir.”
—Friedrich Nietzsche, Más allá del bien y del mal
Chapter 13
El Modelo de Objeto del Documento
Cuando abres una página web en tu navegador, el navegador obtiene el
texto de la página HTML y lo analiza, de una manera bastante similar
a la manera en que nuestro analizador del Capítulo 12 analizaba los
programas. El navegador construye un modelo de la estructura del
documento y utiliza este modelo para dibujar la página en la pantalla.
Esta representación del documento es uno de los juguetes que un
programa de JavaScript tiene disponible en su caja de arena. Es una
estructura de datos que puedes leer o modificar. Y actúa como una
estructura en tiempo real: cuando se modifica, la página en la pantalla
es actualizada para reflejar los cambios.
354
<!doctype html>
<html>
<head>
<title>Mi página de inicio</title>
</head>
<body>
<h1>Mi página de inicio</h1>
<p>Hola, mi nombre es Marijn y esta es mi página de inicio.</
p>
<p>También escribí un libro! Léelo
<a href="http://eloquentjavascript.net">aquí</a>.</p>
</body>
</html>
355
html
head
title
Mi página de inicio
body
h1
Mi página de inicio
p
¡Hola! Mi nombre es Marijn y esta es...
p
a
También escribí un libro! Léelo aquí .
356
Árboles
Pensemos en los árboles sintácticos del Capítulo 12 por un momento.
Sus estructuras son sorprendentemente similares a la estructura de un
documento del navegador. Cada nodo puede referirse a otros nodos
hijos que, a su vez, pueden tener hijos propios. Esta forma es típica
de las estructuras anidadas donde los elementos pueden contener sub
elementos que son similares a ellos mismos.
Le damos el nombre de árbol a una estructura de datos cuando tiene
una estructura de ramificación, no tiene ciclos (un nodo no puede con-
tenerse a sí mismo, directa o indirectamente) y tiene una única raíz
bien definida. En el caso del DOM, document.documentElement hace la
función de raíz.
Los árboles aparecen constantemente en las ciencias de la computación
(computer sience). Además de representar estructuras recursivas como
los documentos HTML o programas, también son comúnmente usados
para mantener conjuntos ordenados de datos debido a que los elementos
generalmente pueden ser encontrados o agregados más eficientemente
en un árbol que en un arreglo plano.
Un árbol típico tiene diferentes tipos de nodos. El árbol sintáctico del
lenguaje Egg tenía identificadores, valores y nodos de aplicación. Los
nodos de aplicación pueden tener hijos, mientras que los identificadores
y valores son hojas, o nodos sin hijos.
Lo mismo sucede para el DOM, los nodos para los elementos, los
cuales representan etiquetas HTML, determinan la estructura del doc-
umento. Estos pueden tener nodos hijos. Un ejemplo de estos nodos es
document.body. Algunos de estos hijos pueden ser nodos hoja, como los
357
fragmentos de texto o los nodos comentario.
Cada objeto nodo DOM tiene una propiedad nodeType, la cual con-
tiene un código (numérico) que identifica el tipo de nodo. Los Elementos
tienen el código 1, que también es definido por la propiedad constante
Node.ELEMENT_NODE. Los nodos de texto, representando una sección de
texto en el documento, obtienen el código 3 (Node.TEXT_NODE). Los co-
mentarios obtienen el código 8 (Node.COMMENT_NODE).
Otra forma de visualizar nuestro árbol de documento es la siguiente:
html head title Mi página de...
p ¡Hola! Mi nom...
p También escribí..
a aquí
Las hojas son nodos de texto, y las flechas nos indican las relaciones
padre-hijo entre los nodos.
El estándar
Usar códigos numéricos crípticos para representar a los tipos de nodos
no es algo que se parezca al estilo de JavaScript para hacer las cosas.
Más adelante en este capítulo, veremos cómo otras partes de la interfaz
358
DOM también se sienten engorrosas y alienígenas. La razón de esto
es que DOM no fue diseñado solamente para JavaScript. Más bien,
intenta ser una interfaz independiente del lenguaje que puede ser usada
en otros sistemas, no solamente para HTML pero también para XML,
que es un formato de datos genérico con una sintaxis similar a la de
HTML.
Esto es desafortunado. Usualmente los estándares son bastante útiles.
Pero en este caso, la ventaja (consistencia entre lenguajes) no es tan
conveniente. Tener una interfaz que está propiamente integrada con
el lenguaje que estás utilizando te ahorrará más tiempo que tener una
interfaz familiar en distintos lenguajes.
A manera de ejemplo de esta pobre integración, considera la propiedad
childNodes que los nodos elemento en el DOM tienen. Esta propiedad
almacena un objeto parecido a un arreglo, con una propiedad length
y propiedades etiquetadas por números para acceder a los nodos hijos.
Pero es una instancia de tipo NodeList, no un arreglo real, por lo que
no tiene métodos como slice o map.
Luego, hay problemas que son simplemente un pobre diseño. Por
ejemplo, no hay una manera de crear un nuevo nodo e inmediatamente
agregar hijos o attributos. En vez de eso, tienes que crearlo primero y
luego agregar los hijos y atributos uno por uno, usando efectos secun-
darios. El código que interactúa mucho con el DOM tiende a ser largo,
repetitivo y feo.
Pero estos defectos no son fatales. Dado que JavaScript nos permite
crear nuestra propias abstracciones, es posible diseñar formas mejoradas
para expresar las operaciones que estás realizando. Muchas bibliotecas
359
destinadas a la programación del navegador vienen con esas herramien-
tas.
0 h1
Mi página de inicio
previousSibling
1 p
Hola, mi nombre es... parentNode
nextSibling
2 p
También escribí un...
lastChild
360
nientes. Las propiedades firstChild y lastChild apuntan al primer y
último elementos hijo, o tiene el valor null para nodos sin hijos. De
manera similar, las propiedades previousSibling y nextSibling apuntan
a los nodos adyacentes, los cuales, son nodos con el mismo padre que
aparecen inmediatamente antes o después del nodo. Para el primer hijo
previousSibling será null y para el último hijo, nextSibling será null.
También existe una propiedad children, que es parecida a childNodes
pero contiene únicamente hijos de tipo elemento (tipo 1), excluyendo
otros tipos de nodos. Esto puede ser útil cuando no estás interesando
en nodos de tipo texto.
Cuando estás tratando con estructuras de datos anidadas como esta,
las funciones recursivas son generalmente útiles. La siguiente función
escanea un documento por nodos de texto que contengan una cadena
dada y regresan true en caso de que encuentren una:
function hablaSobre(nodo, cadena) {
if (nodo.nodeType == Node.ELEMENT_NODE) {
for (let i = 0; i < nodo.childNodes.length; i++) {
if (hablaSobre(nodo.childNodes[i], cadena)) {
return true;
}
}
return false;
} else if (nodo.nodeType == Node.TEXT_NODE) {
return nodo.nodeValue.indexOf(cadena) > -1;
}
}
console.log(hablaSobre(document.body, "libro"));
361
// → true
Buscar elementos
Navegar por estos enlaces entre padres, hijos y hermanos suele ser útil.
Pero si queremos encontrar un nodo específico en el documento, al-
canzarlo comenzando en document.body y siguiendo un camino fijo de
propiedades es una mala idea. Hacerlo genera suposiciones en nuestro
programa sobre la estructura precisa del documento que tal vez quieras
cambiar después. Otro factor complicado es que los nodos de texto son
creados incluso para los espacios en blanco entre nodos. La etiqueta
<body> en el documento de ejemplo no tiene solamente tres hijos (<h1>
y dos elementos <p>), en realidad tiene siete: esos tres, más los espacios
posteriores y anteriores entre ellos.
Por lo que si queremos obtener el atributo href del enlace en ese
documento, no queremos decir algo como “Obten el segundo hijo del
sexto hijo del elemento body del documento”. Sería mejor si pudiéramos
decir “Obten el primer enlace en el documento”. Y de hecho podemos.
let link = document.body.getElementsByTagName("a")[0];
362
console.log(link.href);
<script>
let avestruz = document.getElementById("gertrudiz");
console.log(avestruz.src);
</script>
Actualizar el documento
Prácticamente todo sobre la estructura de datos DOM puede ser cambi-
ado. La forma del árbol de documento puede ser modificada cambiando
363
las relaciones padre-hijo. Los nodos tienen un método remove para ser
removidos de su nodo padre actual. Para agregar un nodo hijo a un
nodo elemento, podemos usar appendChild, que lo pondrá al final de
la lista de hijos, o insertBefore, que insertará el nodo en el primer
argumento antes del nodo en el segundo argumento.
<p>Uno</p>
<p>Dos</p>
<p>Tres</p>
<script>
let parrafos = document.body.getElementsByTagName("p");
document.body.insertBefore(parrafos[2], parrafos[0]);
</script>
364
Crear nodos
Digamos que queremos escribir un script que reemplace todas las ima-
genes (etiquetas <img>) en el documento con el texto contenido en sus
atributos alt, los cuales especifican una representación textual alterna-
tiva de la imagen.
Esto no solamente involucra remover las imágenes, si no que también
involucra agregar un nuevo nodo texto que las reemplace. Los nodos
texto son creados con el método document.createTextNode.
<p>El <img src="img/cat.png" alt="Gato"> en el
<img src="img/hat.png" alt="Sombrero">.</p>
<p><button onclick="sustituirImagenes()">Sustituir</button></p>
<script>
function sustituirImagenes() {
let imagenes = document.body.getElementsByTagName("img");
for (let i = imagenes.length - 1; i >= 0; i--) {
let imagen = imagenes[i];
if (imagen.alt) {
let texto = document.createTextNode(imagen.alt);
imagen.parentNode.replaceChild(texto, imagen);
}
}
}
</script>
365
insertar en el documento para hacer que aparezca en la pantalla.
El ciclo que recorre las imágenes empieza al final de la lista. Esto
es necesario dado que la lista de nodos regresada por un método como
getElementsByTagName (o una propiedad como childNodes) se actualiza en
tiempo real. Esto es, que se actualiza conforme el documento cambia. Si
empezáramos desde el frente, remover la primer imagen causaría que la
lista perdiera su primer elemento de tal manera que la segunda ocasión
que el ciclo se repitiera, donde i es 1, se detendría dado que la longitud
de la colección ahora es también 1.
Si quieres una colección de nodos sólida, a diferencia de una en tiempo
real, puedes convertir la colección a un arreglo real llamando Array.from.
let casi_arreglo = {0: "uno", 1: "dos", length: 2};
let arreglo = Array.from(casi_arreglo);
console.log(arreglo.map(s => s.toUpperCase()));
// → ["UNO", "DOS"]
366
</blockquote>
<script>
function elt(tipo, ...hijos) {
let nodo = document.createElement(tipo);
for (let hijo of hijos) {
if (typeof hijo != "string") nodo.appendChild(hijo);
else nodo.appendChild(document.createTextNode(hijo));
}
return nodo;
}
document.getElementById("cita").appendChild(
elt("footer", "—",
elt("strong", "Karl Popper"),
", prefacio de la segunda edición de ",
elt("em", "La sociedad abierta y sus enemigos"),
", 1950"));
</script>
367
Atributos
Los atributos de algunos elementos, como href para los enlaces, pueden
ser accedidos a través de una propiedad con el mismo nombre en el
objeto DOM del elemento. Este es el caso para los atributos estándar
más comúnmente utilizados.
Pero HTML te permite establecer cualquier atributo que quieras en
los nodos. Esto puede ser útil debido a que te permite almacenar
información extra en un documento. Sin embargo, si creas tus pro-
pios nombres de atributo, dichos atributos no estarán presentes como
propiedades en el nodo del elemento. En vez de eso, tendrás que utilizar
los métodos getAttribute y setAttribute para poder trabajar con ellos.
<p data-classified="secreto">El código de lanzamiento es:
00000000.</p>
<p data-classified="no-classificado">Yo tengo dos pies.</p>
<script>
let parrafos = document.body.getElementsByTagName("p");
for (let parrafo of Array.from(parrafos)) {
if (parrafo.getAttribute("data-classified") == "secreto") {
parrafo.remove();
}
}
</script>
368
atributo.
Existe un atributo comúnmente usado, class, que es una palabra
clave en el lenguaje JavaScript. Por motivos históricos, algunas im-
plementaciones antiguas de JavaScript podrían no manejar nombres
de propiedades que coincidan con las palabras clave, la propiedad uti-
lizada para acceder a este atributo tiene por nombre className. Tam-
bién puedes acceder a ella bajo su nombre real, "class", utilizando los
métodos getAttribute y setAttribute.
Layout
Tal vez hayas notado que diferentes tipos de elementos se exponen de
manera distinta. Algunos, como en el caso de los párrafos (<p>) o en-
cabezados (<h1>), ocupan todo el ancho del documento y se renderizan
en líneas separadas. A estos se les conoce como elementos block (o
bloque). Otros, como los enlaces (<a>) o el elemento <strong>, se ren-
derizan en la misma línea con su texto circundante. Dichos elementos
se les conoce como elementos inline (o en línea).
Para cualquier documento dado, los navegadores son capaces de cal-
cular una estructura (layout), que le da a cada elemento un tamaño y
una posición basada en el tipo y el contenido. Luego, esta estructura
se utiliza para trazar el documento.
Se puede acceder al tamaño y la posición de un elemento desde
JavaScript. Las propiedades offsetWidth y offsetHeight te dan el es-
pacio que el elemento utiliza en pixeles. Un píxel es la unidad básica
de las medidas del navegador. Tradicionalmente correspondía al punto
369
más pequeño que la pantalla podía trazar, pero en los monitores mod-
ernos, que pueden trazar puntos muy pequeños, este puede no ser más
el caso, por lo que un píxel del navegador puede abarcar varios puntos
en la pantalla.
De manera similar, clientWidth y clientHeight te dan el tamaño del
espacio dentro del elemento, ignorando la anchura del borde.
<p style="border: 3px solid red">
Estoy dentro de una caja
</p>
<script>
let parrafo = document.body.getElementsByTagName("p")[0];
console.log("clientHeight:", parrafo.clientHeight);
console.log("offsetHeight:", parrafo.offsetHeight);
</script>
370
obtener en los bindings pageXOffset y pageYOffset.
Estructurar un documento puede requerir mucho trabajo. En los in-
tereses de velocidad, los motores de los navegadores no reestructuran
inmediatamente un documento cada vez que lo cambias, en cambio,
se espera lo más que se pueda. Cuando un programa de JavaScript
que modifica el documento termina de ejecutarse, el navegador tendrá
que calcular una nueva estructura para trazar el documento actual-
izado en la pantalla. Cuando un programa solicita la posición o el
tamaño de algo, leyendo propiedades como offsetHeight o llamando
a getBoundingClientRect, proveer la información correcta también re-
quiere que se calcule una nueva estructura.
A un programa que alterna repetidamente entre leer la información
de la estructura DOM y cambiar el DOM, fuerza a que haya bastantes
cálculos de estructura, y por consecuencia se ejecutará lentamente. El
siguiente código es un ejemplo de esto. Contiene dos programas difer-
entes que construyen una línea de X caracteres con 2,000 pixeles de
ancho y que mide el tiempo que toma cada uno.
<p><span id="uno"></span></p>
<p><span id="dos"></span></p>
<script>
function tiempo(nombre, accion) {
let inicio = Date.now(); // Tiempo actual en milisegundos
accion();
console.log(nombre, "utilizo", Date.now() - inicio, "ms");
}
371
tiempo("inocente", () => {
let objetivo = document.getElementById("uno");
while (objetivo.offsetWidth < 2000) {
objetivo.appendChild(document.createTextNode("X"));
}
});
// → inocente utilizo 32 ms
tiempo("ingenioso", function() {
let objetivo = document.getElementById("dos");
objetivo.appendChild(document.createTextNode("XXXXX"));
let total = Math.ceil(2000 / (objetivo.offsetWidth / 5));
objetivo.firstChild.nodeValue = "X".repeat(total);
});
// → ingenioso utilizo 1 ms
</script>
Estilización
Hemos visto que diferentes elementos HTML se trazan de manera difer-
ente. Algunos son desplegados como bloques, otros en línea. Algunos
agregan estilos, por ejemplo <strong> hace que su contenido esté en
negritas y <a> lo hace azul y lo subraya.
La forma en la que una etiqueta <img> muestra una imagen o una
etiqueta <a> hace que un enlace sea seguido cuando se hace click en el,
está fuertemente atado al tipo del elemento. Pero podemos cambiar los
estilos asociados a un elemento, tales como el color o si está subrayado.
372
Este es un ejemplo que utiliza la propiedad style:
373
tos. A menudo esto es preferido sobre removerlos completamente del
documento debido a que hace más fácil mostrarlos nuevamente en el
futuro.
<script>
let parrafo = document.getElementById("parrafo");
console.log(parrafo.style.color);
parrafo.style.color = "magenta";
</script>
374
estará en mayúsculas (style.fontFamily).
Estilos en Cascada
El sistema de estilos para HTML es llamado CSS por sus siglas en
ingles Cascading Style Sheets (Hojas de estilo en cascada). Una hoja de
estilos es un conjunto de reglas sobre cómo estilizar a los elementos en
un documento. Puede estar declarado dentro de una etiqueta <style>.
<style>
strong {
font-style: italic;
color: gray;
}
</style>
<p>Ahora <strong>el texto en negritas</strong> esta en italicas y
es gris.</p>
375
vería normal, no en negritas. Los estilos en un atributo style aplicados
directamente al nodo tienen la mayor precedencia y siempre ganan.
Es posible apuntar a otras cosas que no sean nombres de etiqueta
en las reglas CSS. Una regla para .abc aplica a todos los elementos
con "abc" en su atributo class. Una regla para #xyz aplica a todos los
elementos con un atributo id con valor "xyz" (que debería ser único en
el documento).
.sutil {
color: gray;
font-size: 80%;
}
#cabecera {
background: blue;
color: white;
}
/* Elementos p con un id principal y clases a y b */
p#principal.a.b {
margin-bottom: 20px;
}
376
sobre ellas.
La notación p > a …{} aplica los estilos dados a todas las etiquetas <a
> que son hijas directas de las etiquetas <p>. De manera similar, p a …{}
aplica a todas las etiquetas <a> dentro de etiquetas <p>, sin importar
que sean hijas directas o indirectas.
Selectores de consulta
No utilizaremos mucho las hojas de estilo en este libro. Entenderlas es
útil cuando se programa en el navegador, pero son lo suficientemente
complicadas para justificar un libro por separado.
La principal razón por la que introduje la sintaxis de selección—la
notación usada en las hojas de estilo para determinar a cuales elementos
aplicar un conjunto de estilos—es que podemos utilizar el mismo mini-
lenguaje como una manera efectiva de encontrar elementos DOM.
El método querySelectorAll, que se encuentra definido tanto en el
objeto document como en los nodos elemento, toma la cadena de un
selector y regresa una NodeList que contiene todos los elementos que
coinciden con la consulta.
<p>And if you go chasing
<span class="animal">rabbits</span></p>
<p>And you know you're going to fall</p>
<p>Tell 'em a <span class="caracter">hookah smoking
<span class="animal">caterpillar</span></span></p>
<p>Has given you the call</p>
<script>
377
function contar(selector) {
return document.querySelectorAll(selector).length;
}
console.log(contar("p")); // Todos los elementos <p>
// → 4
console.log(contar(".animal")); // Clase animal
// → 2
console.log(contar("p .animal")); // Animales dentro de <p>
// → 2
console.log(contar("p > .animal")); // Hijos directos de <p>
// → 1
</script>
Posicionamiento y animaciones
La propiedad de estilo position influye de un manera poderosa sobre
la estructura. Por defecto, tiene el valor de static, eso significa que
378
el elemento se coloca en su lugar normal en el documento. Cuando
se establece como relative, el elemento sigue utilizando espacio en el
documento pero ahora las propiedades top y left pueden ser utilizadas
para moverlo relativamente a ese espacio normal. Cuando position
se establece como absolute, el elemento es removido del flujo normal
del documento—esto es, deja de tomar espacio y puede encimarse con
otros elementos. Además, sus propiedades top y left pueden ser uti-
lizadas para posicionarlo absolutamente con relación a la esquina su-
perior izquierda del elemento envolvente más cercano cuya propiedad
position no sea static, o con relación al documento si dicho elemento
envolvente no existe.
Podemos utilizar esto para crear una animación. El siguiente docu-
mento despliega una imagen de un gato que se mueve alrededor de una
elipse:
<p style="text-align: center">
<img src="img/cat.png" style="position: relative">
</p>
<script>
let gato = document.querySelector("img");
let angulo = Math.PI / 2;
function animar(tiempo, ultimoTiempo) {
if (ultimoTiempo != null) {
angulo += (tiempo - ultimoTiempo) * 0.001;
}
gato.style.top = (Math.sin(angulo) * 20) + "px";
gato.style.left = (Math.cos(angulo) * 200) + "px";
requestAnimationFrame(nuevoTiempo => animar(nuevoTiempo,
tiempo));
379
}
requestAnimationFrame(animar);
</script>
380
por esto que necesitamos a requestAnimationFrame—le permite al nave-
gador saber que hemos terminado por el momento, y que puede empezar
a hacer las cosas que le navegador hace, cómo actualizar la pantalla y
responder a las acciones del usuario.
A la función animar se le pasa el tiempo actual como un argumento.
Para asegurarse de que el movimiento del gato por milisegundo es es-
table, basa la velocidad a la que cambia el ángulo en la diferencia entre
el tiempo actual y la última vez que la función se ejecutó. Si solamente
movieramos el ángulo una cierta cantidad por paso, la animación tarta-
mudearía si, por ejemplo, otra tarea pesada se encontrara ejecutándose
en la misma computadora que pudiera prevenir que la función se ejecu-
tará por una fracción de segundo.
Moverse en círculos se logra a través de las funciones Math.cos y Math.
sin. Para aquellos que no estén familiarizados con estas, las introduciré
brevemente dado que las usaremos ocasionalmente en este libro.
Las funciones Math.cos y Math.sin son útiles para encontrar puntos
que recaen en un círculo alrededor del punto (0,0) con un radio de uno.
Ambas funciones interpretan sus argumentos como las posiciones en el
círculo, con cero denotando el punto en la parte más alejada del lado
derecho del círculo, moviéndose en el sentido de las manecillas del reloj
hasta que 2π (cerca de 6.28) nos halla tomado alrededor de todo el
círculo. Math.cos indica la coordenada x del punto que corresponde con
la posición dada, y Math.sin indica la coordenada y. Las posiciones (o
ángulos) mayores que 2π o menores que 0 son válidas—la rotación se
repite por lo que a+2π se refiere al mismo ángulo que a.
Esta unidad para medir ángulos se conoce como radianes-un círculo
381
completo corresponde a 2π radianes, de manera similar a 360 grados
cuando se utilizan grados. La constante π está disponible como Math.PI
en JavaScript.
cos(-⅔π)
sin(-⅔π)
sin(¼π)
cos(¼π)
382
Resumen
Los programas de JavaScript pueden inspeccionar e interferir con el
documento que el navegador está desplegando a través de una estruc-
tura de datos llamada el DOM. Esta estructura de datos representa
el modelo del navegador del documento, y un programa de JavaScript
puede modificarlo para cambiar el documento visible.
El DOM está organizado como un árbol, en el cual los elementos están
ordenados jerárquicamente de acuerdo a la estructura del documento.
Estos objetos que representan a los elementos tienen propiedades como
parentNode y childNodes, que pueden ser usadas para navegar a través
de este árbol.
La forma en que un documento es desplegado puede ser influenciada
por la estilización, tanto agregando estilos directamente a los nodos
cómo definiendo reglas que coincidan con ciertos nodos. Hay muchas
propiedades de estilo diferentes, tales como color o display. El código
de JavaScript puede manipular el estilo de un elemento directamente a
través de su propiedad de style.
Ejercicios
Construye una tabla
Una tabla HTML se construye con la siguiente estructura de etiquetas:
<table>
<tr>
<th>nombre</th>
383
<th>altura</th>
<th>ubicacion</th>
</tr>
<tr>
<td>Kilimanjaro</td>
<td>5895</td>
<td>Tanzania</td>
</tr>
</table>
Para cada fila, la etiqueta <table> contiene una etiqueta <tr>. Dentro
de estas etiquetas <tr>, podemos poner ciertos elementos: ya sean celdas
cabecera (<th>) o celdas regulares (<td>).
Dado un conjunto de datos de montañas, un arreglo de objetos con
propiedades nombre, altura y lugar, genera la estructura DOM para una
tabla que enlista esos objetos. Deberá tener una columna por llave y
una fila por objeto, además de una fila cabecera con elementos <th> en
la parte superior, listando los nombres de las columnas.
Escribe esto de manera que las columnas se deriven automáticamente
de los objetos, tomando los nombres de propiedad del primer objeto en
los datos.
Agrega la tabla resultante al elemento con el atributo id de "montañas
" de manera que se vuelva visible en el documento.
Una vez que lo tengas funcionando, alinea a la derecha las celdas
que contienen valores numéricos, estableciendo su propiedad style.
textAlign cómo "right".
384
Elementos por nombre de tag
El método document.getElementsByTagName regresa todos los elementos
hijo con un nombre de etiqueta dado. Implementa tu propia versión de
esto como una función que toma un nodo y una cadena (el nombre de
la etiqueta) como argumentos y regresa un arreglo que contiene todos
los nodos elemento descendientes con el nombre del tag dado.
Para encontrar el nombre del tag de un elemento, utiliza su propiedad
nodeName. Pero considera que esto regresará el nombre de la etiqueta
todo en mayúsculas. Utiliza las funciones de las cadenas (string),
toLowerCase o toUpperCase para compensar esta situación.
385
“Tienes poder sobre tu mente, no sobre los acontecimientos.
Date cuenta de esto, y encontrarás la fuerza.”
—Marco Aurelio, Meditaciones
Chapter 14
Manejo de Eventos
Algunos programas funcionan con la entrada directa del usuario, como
las acciones del mouse y el teclado. Ese tipo de entrada no está disponible
como una estructura de datos bien organizada, viene pieza por pieza,
en tiempo real, y se espera que el programa responda a ella a medida
que sucede.
Manejador de eventos
Imagina una interfaz en donde la única forma de saber si una tecla
del teclado está siendo presionada es leer el estado actual de esa tecla.
Para poder reaccionar a las pulsaciones de teclas, tendrías que leer
constantemente el estado de la tecla para poder detectarla antes de
que se vuelva a soltar. Esto sería peligroso al realizar otros cálculos
que requieran mucho tiempo, ya que se podría perder una pulsación de
tecla.
Algunas máquinas antiguas manejan las entradas de esa forma. Un
paso adelante de esto sería que el hardware o el sistema operativo de-
386
tectaran la pulsación de la tecla y lo pusieran en una cola. Luego, un
programa puede verificar periódicamente la cola por nuevos eventos y
reaccionar a lo que encuentre allí.
Por supuesto, este tiene que recordar de mirar la cola, y hacerlo con
frecuencia, porque en cualquier momento entre que se presione la tecla
y que el programa se de cuenta del evento causará que que el programa
no responda. Este enfoque es llamado sondeo. La mayororía de los
programadores prefieren evitarlo.
Un mejor mecanismo es que el sistema notifique activamente a nue-
stro código cuando un evento ocurre. Los navegadores hacen esto per-
mitiéndonos registrar funciones como manejadores (manejadores) para
eventos específicos.
<p>Da clic en este documento para activar el manejador.</p>
<script>
window.addEventListener("click", () => {
console.log("¿Tocaste?");
});
</script>
387
Eventos y nodos DOM
Cada manejador de eventos del navegador es registrado dentro de un
contexto. En el ejemplo anterior llamamos a addEventListener en el
objeto window para registrar un manejador para toda la ventana. Este
método puede también ser encontrado en elementos DOM y en algunos
otros tipos de objetos. Los controladores de eventos son llamados úni-
camente cuando el evento ocurra en el contexto del objeto en que están
registrados.
<button>Presióname</button>
<p>No hay manejadores aquí.</p>
<script>
let boton = document.querySelector("button");
boton.addEventListener("click", () => {
console.log("Botón presionado.");
});
</script>
Este ejemplo adjunta un manejador al nodo del botón. Los clics sobre
el botón hacen que se ejecute ese manejador, pero los clics sobre el resto
del documento no.
Dar a un nodo un atributo onclick tiene un efecto similar. Esto
funciona para la mayoría de tipos de eventos—se puede adjuntar un
manejador a través del atributo cuyo nombre es el nombre del evento
con on en frente de este.
Pero un nodo puede tener únicamente un atributo onclick, por lo
388
que se puede registrar únicamente un manejador por nodo de esa man-
era. El método addEventListener permite agregar cualquier número de
manejadores siendo seguro agregar manejadores incluso si ya hay otro
manejador en el elemento.
El método removeEventListener, llamado con argumentos similares a
addEventListener, remueve un manejador:
Objetos de evento
Aunque lo hemos ignorado hasta ahora, las funciones del manejador de
eventos reciben un argumento: el objeto de evento. Este objeto contiene
información adicional acerca del evento. Por ejemplo, si queremos saber
389
cuál botón del mouse fue presionado, se puede ver la propiedad button
del objeto de evento.
<button>Haz clic en mí de la forma que quieras</button>
<script>
let boton = document.querySelector("button");
boton.addEventListener("mousedown", evento => {
if (evento.button == 0) {
console.log("Botón izquierdo");
} else if (evento.button == 1) {
console.log("Botón central");
} else if (evento.button == 2) {
console.log("Botón derecho");
}
});
</script>
Propagación
Para la mayoría de tipos de eventos, los manejadores registrados en
nodos con hijos también recibirán los eventos que sucedan en los hijos.
Si se hace clic a un botón dentro de un párrafo, los manejadores de
eventos del párrafo también verán el evento clic.
390
Pero si tanto el párrafo como el botón tienen un manejador, el mane-
jador más específico—el del botón—es el primero en lanzarse. Se dice
que el evento se propaga hacia afuera, desde el nodo donde este sucedió
hasta el nodo padre del nodo y hasta la raíz del documento. Final-
mente, después de que todos los manejadores registrados en un nodo
específico hayan tenido su turno, los manejadores registrados en general
ventana tienen la oportunidad de responder al evento.
En cualquier momento, un manejador de eventos puede llamar al
método stopPropagation en el objeto de evento para evitar que los mane-
jadores que se encuentran más arriba reciban el evento. Esto puede ser
útil cuando, por ejemplo, si tienes un botón dentro de otro elemento en
el que se puede hacer clic y que no se quiere que los clics sobre el botón
activen el comportamiento de clic del elemento exterior.
El siguiente ejemplo registra manejadores "mousedown" tanto en un
botón como el párrafo que lo rodea. Cuando se hace clic con el botón
derecho del mouse, el manejador del botón llama a stopPropagation, lo
que evitará que se ejecute el manejador del párrafo. Cuando se hace clic
en el botón con otro botón del mouse, ambos manejadores se ejecutarán.
<p>Un párrafo con un <button>botón</button>.</p>
<script>
let parrafo = document.querySelector("p");
let boton = document.querySelector("button");
parrafo.addEventListener("mousedown", () => {
console.log("Manejador del párrafo.");
});
boton.addEventListener("mousedown", evento => {
console.log("Manejador del botón.");
391
if (evento.button == 2) evento.stopPropagation();
});
</script>
392
Acciones por defecto
La mayoría de eventos tienen una acción por defecto asociada a ellos. Si
haces clic en un enlace, se te dirigirá al destino del enlace. Si presionas
la flecha hacia abajo, el navegador desplazará la página hacia abajo. Si
das clic derecho, se obtendrá un menú contextual. Y así.
Para la mayoría de los tipos de eventos, los manejadores de eventos de
JavaScript se llamarán antes de que el comportamiento por defecto se
produzca. Si el manejador no quiere que suceda este comportamiento
por defecto, normalmente porque ya se ha encargado de manejar el
evento, se puede llamar al método preventDefault en el objeto de evento.
Esto puede ser utilizado para implementar un atajo de teclado propio
o menú contextual. Esto también puede ser utilizado para interferir de
forma desagradable el comportamiento que los usuarios esperan. Por
ejemplo, aquí hay un enlace que no se puede seguir:
<a href="https://developer.mozilla.org/">MDN</a>
<script>
let enlace = document.querySelector("a");
enlace.addEventListener("click", evento => {
console.log("Nope.");
evento.preventDefault();
});
</script>
Trata de no hacer tales cosas a menos que tengas una buena razón
para hacerlo. Será desagradable para las personas que usan tu página
393
cuando el comportamiento esperado no funcione.
Dependiendo del navegador, algunos eventos no pueden ser intercep-
tados en lo absoluto. En Chrome, por ejemplo, el atajo de teclado para
cerrar la pestaña actual (control-W o command-W) no se puede
manejar con JavaScript.
Eventos de teclado
Cuando una tecla del teclado es presionado, el navegador lanza un
evento "keydown". Cuando este es liberado, se obtiene un evento "keyup
".
394
tecla es físicamente presionada. Cuando una tecla se presiona y se
mantiene presionada, el evento se lanza una vez más cada que la tecla
se repite. Algunas veces se debe tener cuidado con esto. Por ejemplo,
si tienes un botón al DOM cuando el botón es presionado y removido
cuando la tecla es liberada, puedes agregar accidentalmente cientos de
botones cuando la tecla se mantiene presionada por más tiempo.
El ejemplo analizó la propiedad key del objeto de evento para ver de
qué tecla se trata el evento. Esta propiedad contiene una cadena que,
para la mayoría de las teclas, corresponde a lo que escribiría al presionar
esa tecla. Para teclas especiales como enter, este contiene una cadena
que nombre la tecla {"Enter", en este caso}. Si mantienes presionado
shift mientras que presionas una tecla, esto también puede influir en
el nombre de la tecla-"v" se convierte en "V" y "1" puede convertirse en
"!", es lo que se produce al presionar shift-1 en tu teclado.
La teclas modificadoras como shift, control, alt y meta (command
en Mac) generan eventos de teclado justamente como las teclas nor-
males. Pero cuando se busque combinaciones de teclas, también se
puede averiguar si estas teclas se mantienen presionadas viendo las
propiedades shiftKey, ctrlKey, altKey y metaKey de los eventos de teclado
y mouse.
<p>Presiona Control-Espacio para continuar.</p>
<script>
window.addEventListener("keydown", evento => {
if (evento.key == " " && evento.ctrlKey) {
console.log("¡Continuando!");
}
});
395
</script>
396
Eventos de puntero
Actualmente, hay dos formas muy utilizadas para señalar en una pan-
talla: mouse (incluyendo dispositivos que actuan como mouse, como
páneles táctiles y trackballs) y pantallas táctiles. Estos producen difer-
entes tipos de eventos.
Clics de mouse
Al presionar un botón del mouse se lanzan varios eventos. Los eventos
"mousedown" y "mouseup" son similares a "keydown" y "keyup" y se lanzan
cuando el boton es presionado y liberado. Estos ocurren en los nodos del
DOM que están inmediatamente debajo del puntero del mouse cuando
ocurre el evento.
Después del evento "mouseup", un evento "click" es lanzado en el
nodo más específico que contiene la pulsación y la liberación del botón.
Por ejemplo, si presiono el botón del mouse sobre un párrafo y entonces
muevo el puntero a otro párrafo y suelto el botón, el evento "click"
ocurrirá en el elemento que contiene ambos párrafos.
Si se producen dos clics juntos, un evento "dblclick" (doble-clic)
también se lanza, después del segundo evento de clic.
Para obtener la información precisa sobre el lugar en donde un evento
del mouse ocurrió, se puede ver en las propiedades clientX y clientY,
los cuales contienen las coordenadas (en pixeles) relativas a la esquina
superior izquierda de la ventana o pageX y pageY, las cuales son relativas
a la esquina superior izquierda de todo el documento (el cual puede ser
diferente cuando la ventana ha sido desplazada).
397
Lo siguiente implementa un programa de dibujo primitivo. Cada vez
que se hace clic en el documento, agrega un punto debajo del puntero
del mouse. Ver Chapter ? para un programa de dibujo menos primitivo.
<style>
body {
height: 200px;
background: beige;
}
.punto {
height: 8px; width: 8px;
border-radius: 4px; /* redondea las esquinas */
background: blue;
position: absolute;
}
</style>
<script>
window.addEventListener("click", evento => {
let punto = document.createElement("div");
punto.className = "punto";
punto.style.left = (evento.pageX - 4) + "px";
punto.style.top = (evento.pageY - 4) + "px";
document.body.appendChild(punto);
});
</script>
398
Movimiento del mouse
Cada vez que el puntero del mouse se mueve, un evento "mousemove" es
lanzado. Este evento puede ser utilizado para rastrear la posición del
mouse. Una situación común en la cual es útil es cuando se implementa
una funcionalidad de mouse-arrastrar.
Como un ejemplo, el siguiente programa muestra una barra y con-
figura los manejadores de eventos para que cuando se arrastre hacia
la izquierda o a la derecha en esta barra la haga más estrecha o más
ancha:
<p>Dibuja la barra para cambiar su anchura:</p>
<div style="background: orange; width: 60px; height: 20px">
</div>
<script>
let ultimoX; // Rastrea la última posición de X del mouse
observado
let barra = document.querySelector("div");
barra.addEventListener("mousedown", evento => {
if (evento.button == 0) {
ultimoX = evento.clientX;
window.addEventListener("mousemove", movido);
evento.preventDefault(); // Evitar la selección
}
});
function movido(evento) {
if (evento.buttons == 0) {
window.removeEventListener("mousemove", movido);
} else {
399
let distancia = event.clientX - lastX;
let nuevaAnchura = Math.max(10, barra.offsetWidth +
distancia);
barra.style.width = nuevaAnchura + "px";
ultimoX = evento.clientX;
}
}
</script>
400
de programación del navegador.
Eventos de toques
El estilo del navegador gráfico que usamos fue diseñado con la interfaz
de mouse en mente, en un tiempo en el cual las pantallas táctiles eran
raras. Para hacer que la Web “funcionara” en los primeros teléfonos con
pantalla táctil, los navegadores de esos dispositivos pretendían, hasta
cierto punto, que los eventos táctiles fueran eventos del mouse. Si se
toca la pantalla, se obtendrán los eventos "mousedown", "mouseup" y "
click".
Pero esta ilusión no es muy robusta. Una pantalla táctil funciona de
manera diferente a un mouse: no tiene multiples botones, no puedes
rastrear el dedo cuando no está en la pantalla (para simular "mousemove
") y permite que multiples dedos estén en la pantalla al mismo tiempo.
Los eventos del mouse cubren las interacciones solamente en casos
sencillos—si se agrega un manejador "click" a un butón, los usuarios
táctiles aún podrán usarlo. Pero algo como la barra redimensionable
del ejemplo anterior no funciona en una pantalla táctil.
Hay tipos de eventos específicos lanzados por la interacción táctil.
Cuando un dedo inicia tocando la pantalla, se obtiene el evento "
touchstart". Cuando este es movido mientras se toca, se lanza el evento
"touchmove". Finalmente, cuando se deja de tocar la pantalla, se lanzará
un evento "touchend".
Debido a que muchas pantallas táctiles pueden detectar multiples
dedos al mismo tiempo, estos eventos no tienen un solo conjunto de
coordenadas asociados a ellos. Más bien, sus objetos de evento tienen
401
una propiedad touches, el cual contiene un objeto tipo matriz de puntos,
cada uno de ellos tiene sus propias propiedades clientX, clientY, pageX
y pageY.
Se puede hacer algo como esto para mostrar circulos rojos alrededor
de cada que toca:
<style>
punto { position: absolute; display: block;
border: 2px solid red; border-radius: 50px;
height: 100px; width: 100px; }
</style>
<p>Toca esta página</p>
<script>
function actualizar(event) {
for (let punto; punto = document.querySelector("punto");) {
punto.remove();
}
for (let i = 0; i < evento.touches.length; i++) {
let {paginaX, paginaY} = evento.touches[i];
let punto = document.createElement("punto");
punto.style.left = (paginaX - 50) + "px";
punto.style.top = (paginaY - 50) + "px";
document.body.appendChild(punto);
}
}
window.addEventListener("touchstart", actualizar);
window.addEventListener("touchmove", actualizar);
window.addEventListener("touchend", actualizar);
</script>
402
A menudo se querrá llamar a preventDefault en un manejador de
eventos táctiles para sobreescribir el comportamiento por defecto del
navegador (que puede incluir desplazarse por la paǵina al deslizar el
dedo) y para evitar que los eventos del mouse se lancen, para lo cual se
puede tener también un manejador.
Eventos de desplazamiento
Siempre que un elemento es desplazado, un evento "scroll" es lanzado.
Esto tiene varios usos, como son saber qué está mirando el usuario
actualmente (para deshabilitar la animación fuera de pantalla o en-
viar informes espías a su sede maligna) o mostrar alguna indicación de
progreso (resaltando parte de una tabla de contenido o mostrando un
número de página).
El siguiente ejemplo dibuja una barra de progreso sobre el documento
y lo actualiza para que se llene a medida que se desplza hacia abajo:
<style>
#progreso {
border-bottom: 2px solid blue;
width: 0;
position: fixed;
top: 0; left: 0;
}
</style>
<div id="progreso"></div>
<script>
403
// Crear algo de contenido
document.body.appendChild(document.createTextNode(
"supercalifragilisticoespialidoso ".repeat(1000)));
404
Eventos de foco
Cuando un elemento gana el foco, el navegador lanza un evento de "
focus" en él. Cuando este pierde el foco, el elemento obtiene un evento
"blur".
A diferencia de los eventos discutidos con anterioridad, estos dos
eventos no se propagan. Un manejador en un elemento padre no es
notificado cuando un elemento hijo gana o pierde el foco.
El siguiente ejemplo muestra un texto de ayuda para campo de texto
que actualmente tiene el foco:
<p>Nombre: <input type="text" dato-ayuda="Tu nombre"></p>
<p>Edad: <input type="text" dato-ayuda="Tu edad en años"></p>
<p id="ayuda"></p>
<script>
let ayuda = document.querySelector("#ayuda");
let campos = document.querySelectorAll("input");
for (let campo of Array.from(campos)) {
campo.addEventListener("focus", evento => {
let texto = evento.target.getAttribute("dato-ayuda");
ayuda.textContent = texto;
});
field.addEventListener("blur", evento => {
ayuda.textContent = "";
});
}
</script>
405
Esta captura de pantalla muestra el texto de ayuda para el campo
edad.
{{figure {url: “img/help-field.png”, alt: “Brindar ayuda cuando un
campo está enfocado”, width: “4.4cm”}}}
El objeto window recibirá eventos de "focus" y "blur" cuando el
usuario se mueva desde o hacia la pestaña o ventana del navegador
en la que se muestra el documento.
Evento de carga
Cuando una página termina de cargarse, el evento "load" es lanzado en
la ventana y en los objetos del cuerpo del documento. Esto es usado
a menudo para programar acciones de inicialización que requieren que
todo el documento haya sido construido. Recordar que el contenido de
las etiquetas <script> se ejecutan inmediatamente cuando la etiqueta
es encontrada. Esto puede ser demasiado pronto, por ejemplo cuando
el guión necesita hacer algo con las partes del documento que aparecen
después de la etiqueta <script>.
Elementos como imagenes y etiquetas de guiones que cargan un archivo
externo también tienen un evento "load" que indica que se cargaron los
archivos a los que hacen referencia. Al igual que los eventos relacionado
con el foco, los eventos de carga no se propagan.
Cuando una página se cierra o se navega fuera de ella (por ejemplo,
siguiendo un enlace), un evento "beforeunload" es lanzado. El principal
uso de este evento es evitar que el usuario pierda su trabajo acciden-
talmente al cerrar un documento. Si se evita el comportamiento por
406
defecto en este evento y se establece la propiedad returnValue en el
objeto de evento a una cadena, el navegador le mostrará al usuario
un diálogo preguntándole si realmente quiere salir de la página. Ese
cuadro de diálogo puede incluir una cadena de texto, pero debido a que
algunos sitios intentan usar estos cuadros de diálogo para confundir a
las personas para que permanezcan en su página para ver anuncios poco
fiables sobre la pérdida de peso, la mayoría de los navegadores ya no lo
muestran.
407
JavaScript que se jecuta junto con el guión principal, en su propia línea
de tiempo.
Imaginar que se eleva un número al cuadrado es un cálculo pesado y
de larga duración que se quiere realizar en un hilo separado. Se podría
escribir un archivo llamado code/cuadradoworker.js que responde a los
mensajes calculando un cuadrado y enviando de vuelta un mensaje.
addEventListener("message", evento => {
enviarMensaje(evento.data * evento.data);
});
408
vía y recibe mensajes a través del objeto Worker, mientras que el worker
habla con el guión que lo creó enviando y escuchando directamente en
su alcance global. Solo los valores que pueden ser representados como
un JSON pueden ser enviados como un mensaje—el otro lado recibirá
una copia de ellos, en lugar del valor en sí.
Temporizadores
Se mostró la función establecerTiempoEspera en el Chapter 11. Este
programa otra función para que se llame más tarde, después de un
número determinado de milisegundos.
Algunas veces se necesita cancelar una función que se haya progra-
mado. Esto se hace almacenando el valor retornado por establecerTiempoEs
y llamando a reinicarTiempoEspera en él.
let temporizadorBomba = setTimeout(() => {
console.log("¡BOOM!");
}, 500);
409
Un conjunto similar de funciones, setInterval y clearInterval, son
usadas para restablecer los temporizadores que deberían repetirse cada
X milisegundos.
let tictac = 0;
let reloj = setInterval(() => {
console.log("tictac", tictac++);
if (tictac == 10) {
clearInterval(reloj);
console.log("Detener.");
}
}, 200);
Antirrebote
Algunos tipos de eventos tienen el potencial de ser lanzados rápida-
mente, muchas veces seguidos (los eventos "mousemove" y "scroll", por
ejmplo). Cuando se menejan tales eventos, se debe tener cuidado de
no hacer nada que consuma demasiado tiempo o su manejador tomará
tanto tiempo que la interacción con el documento comenzará a sentirse
lenta.
Si necesitas hacer algo no trivial en algún manejador de este tipo,
se puede usar setTimeout para asegurarse de que no se está haciendo
con demasiada frecuencia. Esto generalmente se llama antirrebote del
evento. Hay varios enfoques ligeramente diferentes para esto.
En el primer ejemplo, se quiere reaccionar cuando el usuario ha es-
410
crito algo, pero no se quiere hacer inmediatamente por cada evento de
entrada. Cuando se está escribiendo rápidamente, se requiere esperar
hasta que se produzca una pausa. En lugar de realizar inmediatamente
una acción en el manejador de eventos, se establece un tiempo de es-
pera. También se borra el tiempo de espera anterior (si lo hay) para
que cuando los eventos ocurran muy juntos (más cerca que el tiempo
de espera), el tiempo de espera del evento anterior será cancelado.
<textarea>Escribe algo aquí...</textarea>
<script>
let areaTexto = document.querySelector("textarea");
let tiempoEspera;
areaTexto.addEventListener("input", () => {
clearTimeout(tiempoEspera);
tiempoEspera = setTimeout(() => console.log("¡Escribió!"),
500);
});
</script>
411
cada 250 milisegundos.
<script>
let programado = null;
window.addEventListener("mousemove", evento => {
if (!programado) {
setTimeout(() => {
document.body.textContent =
`Mouse en ${programado.pageX}, ${programado.pageY}`;
programado = null;
}, 250);
}
programado = evento;
});
</script>
Resumen
Los manejadores de eventos hacen posible detectar y reaccionar a even-
tos que suceden en nuestra página web. El método addEventListener es
usado para registrar un manejador de eventos.
Cada evento tiene un tipo ("keydown", "focus", etc.) que lo identifica.
La mayoría de eventos son llamados en un elemento DOM específico y
luego se propagan a los ancentros de ese elemento, lo que permite que
los manejadores asociados con esos elementos los manejen.
Cuando un manejador de evento es llamado, se le pasa un objeto
evento con información adicional acerca del evento. Este objeto también
412
tiene métodos que permiten detener una mayor propagación (stopPropagati
) y evitar que el navegador maneje el evento por defecto (preventDefault
).
Al presiosar una tecla se lanza los eventos "keydown" y "keyup". Al
presionar un botón del mouse se lanzan los eventos "mousedown", "
mouseup" y "click". Al mover el mouse se lanzan los eventos "mousemove
". Las interacción de la pantalla táctil darán como resultado eventos
"touchstart", "touchmove" y "touchend".
El desplazamiento puede ser detectado con el evento "scroll" y los
cambios de foco pueden ser detactados con los eventos "focus" y "blur".
Cuando el documento termina de cargarse, se lanza el evento "load" en
la ventana.
Ejercicios
Globo
Escribe una página que muestre un globo (usando el globo emoji, 🎈).
Cuando se presione la flecha hacia arriba, debe inflarse (crecer) un 10
por cierto, y cuando se presiona la flecha hacia abajo, debe desinflarse
(contraerse) un 10 por cierto)
Puedes controlar el tamaño del texto (los emojis son texto) config-
urando la propiedad CSS font-size (style.fontSize) en su elemento
padre. Recuerda incluir una unidad en el valor, por ejemplo pixeles
(10px).
Los nombres de las teclas de flecha son "ArrowUp" y "ArrowDown"
. Asegúratede que las teclas cambien solo al globo, sin desplazar la
413
página.
Cuando eso funcione, agrega una nueva función en la que, si infla
el globo más allá de cierto tamaño, explote. En este caso, explotar
significa que se reemplaza con un emoji 💥, y se elimina el manejador
de eventos (para que no se pueda inflar o desinflar la explosión).
Mouse trail
En los primeros días de JavaScript, que era el momento de páginas
de inicio llamativas con muchas imágenes, a la gente se le ocurrieron
formas realmente inspiradoras de usar el lenguaje.
Uno de estos fue el rastro del mouse, una serie de elementos que
seguirían el puntero del mouse mientras lo movías por la página.
En este ejercicio, quiero que implementes un rastro del mouse. Utiliza
elementos <div> con un tamaño fijo y un color de fondo (consulta a code
en la sección “Clics del mouse” por un ejemplo). Crea un montón de
estos elementos y, cuando el mouse se mueva, muestralos después del
puntero del mouse.
Hay varios enfoques posibles aquí. Puedes hacer tu solución tan
simple o tan compleja como desees. Una solución simple para comenzar
es mantener un número de elementos de seguimiento fijos y recorrerlos,
moviendo el sigueinte a la posición actual del mouse cada vez que ocurra
un evento "mousemove".
414
Pestañas
Los paneles con pestañas son utilizados ampliamente en las interfaces
de usuario. Te permiten seleccionar un panel de interfaz eligiendo entre
una serie de pestañas “que sobresalen” sobre un elemento.
En este ejercicio debes implementar una interfaz con pestañas simple.
Escribe una función, asTabs, que tome un nodo DOM y cree una inter-
faz con pestañas que muestre los elementos secundarios de ese nodo.
Se debe insertar una lista de elementos <button> en la parte superior
del nodo, uno para cada elemento hijo, que contenga texto recuperado
del atributo data-tabname del hijo. Todos menos uno de los elemen-
tos secundarios originales deben estar ocultos (dado un estilo display
denone). El nodo visible actualmente se puede seleccionar haciendo clic
en los botones.
Cuando eso funcione, extiéndelo para diseñar el botón de la pestaña
seleccionada actualmente de manera diferente para que sea obvio qué
pestaña está seleccionada.
415
“Toda realidad es un juego.”
—Iain Banks, El Jugador de Juegos
Chapter 15
Proyecto: Un Juego de Plataforma
Mucha de mi fascinación inicial con computadoras, como la de muchos
niños nerd, tiene que ver con juegos de computadora. Fui atraído a
los pequeños mundos simulados que podía manipular y en los que las
historias (algo así) revelaban más, yo supongo, por la manera en que yo
proyectaba mi imaginación en ellas más que por las posibilidades que
realmente ofrecían.
No le deseo a nadie una carrera en programación de juegos. Tal
como la industria musical, la discrepancia entre el número de personas
jóvenes ansiosas queriendo trabajar en ella y la actual demanda de tales
personas crea un ambiente bastante malsano.
Este capítulo recorrerá la implementación de un juego de plataforma
pequeño. Los juegos de plataforma (o juegos de “brincar y correr”)
son juegos que esperan que el jugador mueva una figura a través de un
mundo, el cual usualmente es bidimensional y visto de lado, mientras
brinca encima y sobre cosas.
416
El juego
Nuestro juego estará ligeramente basado en Dark Blue (www.lessmilk.com/
games/10) de Thomas Palef. Escogí ese juego porque es tanto en-
tretenido como minimalista y porque puede ser construido sin demasi-
ado código. Se ve así:
417
con los elementos móviles superpuestos sobre ese fondo. Cada campo
en la cuadrícula está ya sea vacío, sólido o lava. Las posiciones de estos
elementos no están restringidas a la cuadrícula-sus coordenadas pueden
ser fraccionales, permitiendo un movimiento suave.
La tecnología
Usaremos el DOM del navegador para mostrar el juego, y leeremos la
entrada del usuario por medio del manejo los eventos de teclas.
El código relacionado con la pantalla y el teclado es sólo una pequeña
parte del trabajo que necesitamos hacer para construir este juego. Ya
que todo parece como cajas coloreadas, dibujar es no es complicado:
creamos los elementos del DOM y usamos estilos para darlos un color
de fondo, tamaño y posición.
Podemos representar el fondo como una tabla ya que es una cuadrícula
invariable de cuadrados. Los elementos libres de moverse pueden ser
superpuestos usando elementos posicionados absolutamente.
En los juegos y otros programas que deben animar gráficos y respon-
der a la entrada del usuario sin retraso notable, la eficiencia es impor-
tante. Aunque el DOM no fue diseñado originalmente para gráficos
de alto rendimiento, es realmente mejor en ello de lo que se esperaría.
Viste algunas animaciones en Chapter 13. En una máquina moderna,
un simple juego como este desempeña bien, aún si no nos preocupamos
mucho de la optimización.
En el siguiente capítulo, exploraremos otra tecnología del navegador,
la etiqueta <canvas>, la cual provee un forma más tradicional de dibujar
418
gráficos, trabajando en término de formas y pixeles más que elementos
del DOM.
Niveles
Vamos a querer una forma de especificar niveles que sea fácilmente leíble
y editable por humanos. Ya que está bien que todo empiece sobre una
cuadrícula, podríamos usar cadenas de caracteres largas en las que cada
caracter representa un elemento-ya sea una parte de la cuadrícula de
fondo o un elemento móvil.
El plano para un nivel pequeño podría lucir como esto:
let planoDeNivel = `
......................
..#................#..
..#..............=.#..
..#.........o.o....#..
..#.@......#####...#..
..#####............#..
......#++++++++++++#..
......##############..
......................`;
Los puntos son espacios vacíos, los signos de número (#) son muros
y los signos de suma son lava. La posición inicial del jugador es el
arroba (@). Cada caracter O es una moneda y el signo de igual (=) en
la parte superior es un bloque de lava que se mueve de un lado a otro
419
horizontalmente.
Vamos a soportar dos tipos adicionales de lava en movimento: el
caracter de la barra vertical (|) crea gotas que mueven verticalmente
y la v indica lava goteando-la lava que se mueve verticalmente que no
rebota de un lado a otro sino que sólo se mueve hacia abajo, brincando
a su posición inicial cuando toca el suelo.
Un juego completo consiste en múltiples niveles que el jugador debe
completar. Un nivel es completado cuando todas las monedass han sido
recolectadas. Si el jugador toca la lava, el nivel actual se restaura a su
posición inicial y el juego puede intentar de nuevo.
Leyendo un nivel
La siguiente clase guarda un objeto de nivel. Su argumento debe ser la
cadena de carateres que define el nivel.
class Nivel {
constructor(plano) {
let filas = plano.trim().split("\n").map(l => [...l]);
this.height = filas.length;
this.width = filas[0].length;
this.iniciarActores = [];
420
tipo.create(new Vector(x, y), car));
return "vacío";
});
});
}
}
421
cadenas de caracteres y caracteres de actores a clases. Cuando tipo
está en la clase actor, su método estático create es usado para crear
un objeto, el cual es agregado a iniciarActores y la función de mapeo
regresa "vacío" para este cuadrado de fondo.
La posición del actor es guardada como un objeto Vector. Este es un
vector bidimensional, un objeto con propiedades x y y, como se vió en
los ejercicios de Chapter 6.
Mientras el juego se ejecuta, los actores terminarán en diferentes
lugares o incluso desaparecerán completamente (como las monedas lo
hacen cuando son recolectadas). Usaremos una clase Estado para dar
seguimiento al estado de un juego que en ejecución.
class Estado {
constructor(nivel, actores, estatus) {
this.nivel = nivel;
this.actores = actores;
this.estatus = estatus;
}
static start(nivel) {
return new Estado(nivel, nivel.iniciarActores, "jugando");
}
get jugador() {
return this.actores.find(a => a.tipo == "jugador");
}
}
422
La propiedad estatus cambiará a "perdido" or "ganado" cuando el
juego haya terminado.
Esto es de nuevo una estructura de datos persistente-actualizar el
estado del juego crea un nuevo estado y deja el anterior intacto.
Actors
Actor objects represent the current position and state of a given moving
element in our game. All actor objects conform to the same interface.
Their pos property holds the coordinates of the element’s top-left corner,
and their size property holds its size.
Then they have an update method, which is used to compute their
new state and position after a given time step. It simulates the thing
the actor does—moving in response to the arrow keys for the player
and bouncing back and forth for the lava—and returns a new, updated
actor object.
A type property contains a string that identifies the type of the
actor—"player", "coin", or "lava". This is useful when drawing the
game—the look of the rectangle drawn for an actor is based on its
type.
Actor classes have a static create method that is used by the Level
constructor to create an actor from a character in the level plan. It is
given the coordinates of the character and the character itself, which is
needed because the Lava class handles several different characters.
This is the Vector class that we’ll use for our two-dimensional values,
such as the position and size of actors.
423
class Vector {
constructor(x, y) {
this.x = x; this.y = y;
}
plus(other) {
return new Vector(this.x + other.x, this.y + other.y);
}
times(factor) {
return new Vector(this.x * factor, this.y * factor);
}
}
424
static create(pos) {
return new Player(pos.plus(new Vector(0, -0.5)),
new Vector(0, 0));
}
}
425
class Lava {
constructor(pos, speed, reset) {
this.pos = pos;
this.speed = speed;
this.reset = reset;
}
Coin actors are relatively simple. They mostly just sit in their place.
But to liven up the game a little, they are given a “wobble”, a slight
vertical back-and-forth motion. To track this, a coin object stores a
base position as well as a wobble property that tracks the phase of the
bouncing motion. Together, these determine the coin’s actual position
(stored in the pos property).
426
class Coin {
constructor(pos, basePos, wobble) {
this.pos = pos;
this.basePos = basePos;
this.wobble = wobble;
}
static create(pos) {
let basePos = pos.plus(new Vector(0.2, 0.1));
return new Coin(basePos, basePos,
Math.random() * Math.PI * 2);
}
}
427
to either background grid types or actor classes.
const levelChars = {
".": "empty", "#": "wall", "+": "lava",
"@": Player, "o": Coin,
"=": Lava, "|": Lava, "v": Lava
};
The task ahead is to display such levels on the screen and to model
time and motion inside them.
Encapsulation as a burden
Most of the code in this chapter does not worry about encapsulation
very much for two reasons. First, encapsulation takes extra effort. It
makes programs bigger and requires additional concepts and interfaces
to be introduced. Since there is only so much code you can throw at
a reader before their eyes glaze over, I’ve made an effort to keep the
program small.
Second, the various elements in this game are so closely tied together
that if the behavior of one of them changed, it is unlikely that any of the
428
others would be able to stay the same. Interfaces between the elements
would end up encoding a lot of assumptions about the way the game
works. This makes them a lot less effective—whenever you change one
part of the system, you still have to worry about the way it impacts the
other parts because their interfaces wouldn’t cover the new situation.
Some cutting points in a system lend themselves well to separation
through rigorous interfaces, but others don’t. Trying to encapsulate
something that isn’t a suitable boundary is a sure way to waste a lot of
energy. When you are making this mistake, you’ll usually notice that
your interfaces are getting awkwardly large and detailed and that they
need to be changed often, as the program evolves.
There is one thing that we will encapsulate, and that is the drawing
subsystem. The reason for this is that we’ll display the same game in
a different way in the next chapter. By putting the drawing behind an
interface, we can load the same game program there and plug in a new
display module.
Drawing
The encapsulation of the drawing code is done by defining a display
object, which displays a given level and state. The display type we
define in this chapter is called DOMDisplay because it uses DOM elements
to show the level.
We’ll be using a style sheet to set the actual colors and other fixed
properties of the elements that make up the game. It would also be
possible to directly assign to the elements’ style property when we
429
create them, but that would produce more verbose programs.
The following helper function provides a succinct way to create an
element and give it some attributes and child nodes:
function elt(name, attrs, ...children) {
let dom = document.createElement(name);
for (let attr of Object.keys(attrs)) {
dom.setAttribute(attr, attrs[attr]);
}
for (let child of children) {
dom.appendChild(child);
}
return dom;
}
clear() { this.dom.remove(); }
}
430
Actors are redrawn every time the display is updated with a given
state. The actorLayer property will be used to track the element that
holds the actors so that they can be easily removed and replaced.
Our coordinates and sizes are tracked in grid units, where a size or
distance of 1 means one grid block. When setting pixel sizes, we will
have to scale these coordinates up—everything in the game would be
ridiculously small at a single pixel per square. The scale constant gives
the number of pixels that a single unit takes up on the screen.
const scale = 20;
function drawGrid(level) {
return elt("table", {
class: "background",
style: `width: ${level.width * scale}px`
}, ...level.rows.map(row =>
elt("tr", {style: `height: ${scale}px`},
...row.map(type => elt("td", {class: type})))
));
}
431
.background { background: rgb(52, 166, 251);
table-layout: fixed;
border-spacing: 0; }
.background td { padding: 0; }
.lava { background: rgb(255, 100, 100); }
.wall { background: white; }
432
rect.style.top = `${actor.pos.y * scale}px`;
return rect;
}));
}
To give an element more than one class, we separate the class names
by spaces. In the CSS code shown next, the actor class gives the actors
their absolute position. Their type name is used as an extra class to
give them a color. We don’t have to define the lava class again because
we’re reusing the class for the lava grid squares we defined earlier.
.actor { position: absolute; }
.coin { background: rgb(241, 229, 89); }
.player { background: rgb(64, 64, 64); }
The syncState method is used to make the display show a given state.
It first removes the old actor graphics, if any, and then redraws the
actors in their new positions. It may be tempting to try to reuse the
DOM elements for actors, but to make that work, we would need a lot
of additional bookkeeping to associate actors with DOM elements and
to make sure we remove elements when their actors vanish. Since there
will typically be only a handful of actors in the game, redrawing all of
them is not expensive.
DOMDisplay.prototype.syncState = function(state) {
if (this.actorLayer) this.actorLayer.remove();
this.actorLayer = drawActors(state.actors);
433
this.dom.appendChild(this.actorLayer);
this.dom.className = `game ${state.status}`;
this.scrollPlayerIntoView(state);
};
After touching lava, the player’s color turns dark red, suggesting
scorching. When the last coin has been collected, we add two blurred
white shadows—one to the top left and one to the top right—to create
a white halo effect.
We can’t assume that the level always fits in the viewport—the ele-
ment into which we draw the game. That is why the scrollPlayerIntoView
call is needed. It ensures that if the level is protruding outside the
viewport, we scroll that viewport to make sure the player is near its
center. The following CSS gives the game’s wrapping DOM element
a maximum size and ensures that anything that sticks out of the ele-
434
ment’s box is not visible. We also give it a relative position so that the
actors inside it are positioned relative to the level’s top-left corner.
.game {
overflow: hidden;
max-width: 600px;
max-height: 450px;
position: relative;
}
// The viewport
let left = this.dom.scrollLeft, right = left + width;
let top = this.dom.scrollTop, bottom = top + height;
435
this.dom.scrollLeft = center.x - margin;
} else if (center.x > right - margin) {
this.dom.scrollLeft = center.x + margin - width;
}
if (center.y < top + margin) {
this.dom.scrollTop = center.y - margin;
} else if (center.y > bottom - margin) {
this.dom.scrollTop = center.y + margin - height;
}
};
The way the player’s center is found shows how the methods on our
Vector type allow computations with objects to be written in a relatively
readable way. To find the actor’s center, we add its position (its top-left
corner) and half its size. That is the center in level coordinates, but we
need it in pixel coordinates, so we then multiply the resulting vector by
our display scale.
Next, a series of checks verifies that the player position isn’t out-
side of the allowed range. Note that sometimes this will set nonsense
scroll coordinates that are below zero or beyond the element’s scrollable
area. This is okay—the DOM will constrain them to acceptable values.
Setting scrollLeft to -10 will cause it to become 0.
It would have been slightly simpler to always try to scroll the player
to the center of the viewport. But this creates a rather jarring effect.
As you are jumping, the view will constantly shift up and down. It
is more pleasant to have a “neutral” area in the middle of the screen
where you can move around without causing any scrolling.
436
We are now able to display our tiny level.
<link rel="stylesheet" href="css/game.css">
<script>
let simpleLevel = new Level(simpleLevelPlan);
let display = new DOMDisplay(document.body, simpleLevel);
display.syncState(State.start(simpleLevel));
</script>
437
are expressed in units per second.
Moving things is easy. The difficult part is dealing with the interac-
tions between the elements. When the player hits a wall or floor, they
should not simply move through it. The game must notice when a given
motion causes an object to hit another object and respond accordingly.
For walls, the motion must be stopped. When hitting a coin, it must
be collected. When touching lava, the game should be lost.
Solving this for the general case is a big task. You can find libraries,
usually called physics engines, that simulate interaction between phys-
ical objects in two or three dimensions. We’ll take a more modest
approach in this chapter, handling only collisions between rectangular
objects and handling them in a rather simplistic way.
Before moving the player or a block of lava, we test whether the
motion would take it inside of a wall. If it does, we simply cancel the
motion altogether. The response to such a collision depends on the type
of actor—the player will stop, whereas a lava block will bounce back.
This approach requires our time steps to be rather small since it will
cause motion to stop before the objects actually touch. If the time
steps (and thus the motion steps) are too big, the player would end up
hovering a noticeable distance above the ground. Another approach,
arguably better but more complicated, would be to find the exact col-
lision spot and move there. We will take the simple approach and hide
its problems by ensuring the animation proceeds in small steps.
This method tells us whether a rectangle (specified by a position and
a size) touches a grid element of the given type.
Level.prototype.touches = function(pos, size, type) {
438
var xStart = Math.floor(pos.x);
var xEnd = Math.ceil(pos.x + size.x);
var yStart = Math.floor(pos.y);
var yEnd = Math.ceil(pos.y + size.y);
The method computes the set of grid squares that the body overlaps
with by using Math.floor and Math.ceil on its coordinates. Remember
that grid squares are 1 by 1 units in size. By rounding the sides of a
box up and down, we get the range of background squares that the box
touches.
We loop over the block of grid squares found by rounding the coordi-
439
nates and return true when a matching square is found. Squares outside
of the level are always treated as "wall" to ensure that the player can’t
leave the world and that we won’t accidentally try to read outside of
the bounds of our rows array.
The state update method uses touches to figure out whether the player
is touching lava.
State.prototype.update = function(time, keys) {
let actors = this.actors
.map(actor => actor.update(time, this, keys));
let newState = new State(this.level, actors, this.status);
The method is passed a time step and a data structure that tells it
which keys are being held down. The first thing it does is call the update
440
method on all actors, producing an array of updated actors. The actors
also get the time step, the keys, and the state, so that they can base
their update on those. Only the player will actually read keys, since
that’s the only actor that’s controlled by the keyboard.
If the game is already over, no further processing has to be done
(the game can’t be won after being lost, or vice versa). Otherwise, the
method tests whether the player is touching background lava. If so, the
game is lost, and we’re done. Finally, if the game really is still going
on, it sees whether any other actors overlap the player.
Overlap between actors is detected with the overlap function. It takes
two actor objects and returns true when they touch—which is the case
when they overlap both along the x-axis and along the y-axis.
function overlap(actor1, actor2) {
return actor1.pos.x + actor1.size.x > actor2.pos.x &&
actor1.pos.x < actor2.pos.x + actor2.size.x &&
actor1.pos.y + actor1.size.y > actor2.pos.y &&
actor1.pos.y < actor2.pos.y + actor2.size.y;
}
If any actor does overlap, its collide method gets a chance to update
the state. Touching a lava actor sets the game status to "lost". Coins
vanish when you touch them and set the status to "won" when they are
the last coin of the level.
Lava.prototype.collide = function(state) {
return new State(state.level, state.actors, "lost");
};
441
Coin.prototype.collide = function(state) {
let filtered = state.actors.filter(a => a != this);
let status = state.status;
if (!filtered.some(a => a.type == "coin")) status = "won";
return new State(state.level, filtered, status);
};
Actor updates
Actor objects’ update methods take as arguments the time step, the
state object, and a keys object. The one for the Lava actor type ignores
the keys object.
Lava.prototype.update = function(time, state) {
let newPos = this.pos.plus(this.speed.times(time));
if (!state.level.touches(newPos, this.size, "wall")) {
return new Lava(newPos, this.speed, this.reset);
} else if (this.reset) {
return new Lava(this.reset, this.speed, this.reset);
} else {
return new Lava(this.pos, this.speed.times(-1));
}
};
442
blocks that new position, it moves there. If there is an obstacle, the
behavior depends on the type of the lava block—dripping lava has a
reset position, to which it jumps back when it hits something. Bouncing
lava inverts its speed by multiplying it by -1 so that it starts moving in
the opposite direction.
Coins use their update method to wobble. They ignore collisions
with the grid since they are simply wobbling around inside of their own
square.
const wobbleSpeed = 8, wobbleDist = 0.07;
Coin.prototype.update = function(time) {
let wobble = this.wobble + time * wobbleSpeed;
let wobblePos = Math.sin(wobble) * wobbleDist;
return new Coin(this.basePos.plus(new Vector(0, wobblePos)),
this.basePos, wobble);
};
443
const jumpSpeed = 17;
444
account for gravity.
We check for walls again. If we don’t hit any, the new position is
used. If there is a wall, there are two possible outcomes. When the up
arrow is pressed and we are moving down (meaning the thing we hit is
below us), the speed is set to a relatively large, negative value. This
causes the player to jump. If that is not the case, the player simply
bumped into something, and the speed is set to zero.
The gravity strength, jumping speed, and pretty much all other con-
stants in this game have been set by trial and error. I tested values
until I found a combination I liked.
Tracking keys
For a game like this, we do not want keys to take effect once per key-
press. Rather, we want their effect (moving the player figure) to stay
active as long as they are held.
We need to set up a key handler that stores the current state of the
left, right, and up arrow keys. We will also want to call preventDefault
for those keys so that they don’t end up scrolling the page.
The following function, when given an array of key names, will return
an object that tracks the current position of those keys. It registers
event handlers for "keydown" and "keyup" events and, when the key code
in the event is present in the set of codes that it is tracking, updates
the object.
function trackKeys(keys) {
let down = Object.create(null);
445
function track(event) {
if (keys.includes(event.key)) {
down[event.key] = event.type == "keydown";
event.preventDefault();
}
}
window.addEventListener("keydown", track);
window.addEventListener("keyup", track);
return down;
}
const arrowKeys =
trackKeys(["ArrowLeft", "ArrowRight", "ArrowUp"]);
The same handler function is used for both event types. It looks at
the event object’s type property to determine whether the key state
should be updated to true ("keydown") or false ("keyup").
446
it a function that expects a time difference as an argument and draws
a single frame. When the frame function returns the value false, the
animation stops.
function runAnimation(frameFunc) {
let lastTime = null;
function frame(time) {
if (lastTime != null) {
let timeStep = Math.min(time - lastTime, 100) / 1000;
if (frameFunc(timeStep) === false) return;
}
lastTime = time;
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
}
447
lets the user play through it. When the level is finished (lost or won),
runLevel waits one more second (to let the user see what happens) and
then clears the display, stops the animation, and resolves the promise
to the game’s end status.
function runLevel(level, Display) {
let display = new Display(document.body, level);
let state = State.start(level);
let ending = 1;
return new Promise(resolve => {
runAnimation(time => {
state = state.update(time, arrowKeys);
display.syncState(state);
if (state.status == "playing") {
return true;
} else if (ending > 0) {
ending -= time;
return true;
} else {
display.clear();
resolve(state.status);
return false;
}
});
});
}
448
level. This can be expressed by the following function, which takes an
array of level plans (strings) and a display constructor:
async function runGame(plans, Display) {
for (let level = 0; level < plans.length;) {
let status = await runLevel(new Level(plans[level]),
Display);
if (status == "won") level++;
}
console.log("You've won!");
}
<body>
<script>
runGame(GAME_LEVELS, DOMDisplay);
</script>
</body>
449
Exercises
Game over
It’s traditional for platform games to have the player start with a limited
number of lives and subtract one life each time they die. When the
player is out of lives, the game restarts from the beginning.
Adjust runGame to implement lives. Have the player start with three.
Output the current number of lives (using console.log) every time a
level starts.
450
A monster
It is traditional for platform games to have enemies that you can jump
on top of to defeat. This exercise asks you to add such an actor type
to the game.
We’ll call it a monster. Monsters move only horizontally. You can
make them move in the direction of the player, bounce back and forth
like horizontal lava, or have any movement pattern you want. The class
doesn’t have to handle falling, but it should make sure the monster
doesn’t walk through walls.
When a monster touches the player, the effect depends on whether
the player is jumping on top of them or not. You can approximate this
by checking whether the player’s bottom is near the monster’s top. If
this is the case, the monster disappears. If not, the game is lost.
451
“Un estudiante preguntó: ‘Los programadores de antaño sólo
usaban máquinas simples y ningún lenguaje de programación,
pero hicieron programas hermosos. ¿Por qué usamos
máquinas y lenguajes de programación complicados?’. Fu-Tzu
respondió, Los constructores de antaño sólo usaban palos y
arcilla, pero hicieron bellas chozas.”’
—Master Yuan-Ma, The Book of Programming
Chapter 16
Node.js
Hasta ahora, hemos usado el lenguaje JavaScript en un solo entorno: el
navegador. Este capítulo y el siguiente presentarán brevemente Node.js,
un programa que te permite aplicar tus habilidades de JavaScript fuera
del navegador. Con él, puedes crear cualquier cosa, desde pequeñas her-
ramientas de línea de comandos hasta servidores HTTP que potencian
los sitios web dinámicos.
Estos capítulos tienen como objetivo enseñarte los principales con-
ceptos que Node.js utiliza y darte suficiente información para escribir
programas útiles de este. No intentan ser un tratamiento completo, ni
siquiera minucioso, de la plataforma.
Si quieres seguir y ejecutar el código de este capítulo, necesitarás in-
stalar Node.js versión 10.1 o superior. Para ello, dirigete a https://nodejs.or
y sigue las instrucciones de instalación para tu sistema operativo. Tam-
bién podrás encontrar más documentación para Node.js allí.
452
Antecedentes
Uno de los problemas más difíciles de los sistemas de escritura que se
comunican a través de la red es la gestión de entrada y salida, es decir,
la lectura y la escritura de datos hacia y desde la red y el disco duro.
Mover los datos toma tiempo, y programarlos con habilidad puede hacer
una gran diferencia en que tan rápido un sistema responde al usuario o
a las peticiones de red.
En tales programas, la programación asíncrona suele ser útil. Permite
que el programa envíe y reciba datos desde y hacia múltiples dispositivos
al mismo tiempo sin necesidad de una complicada administración y
sincronización de hilos.
Node fue concebido inicialmente con el propósito de hacer la progra-
mación asíncrona fácil y conveniente. JavaScript se presta bien a un
sistema como Node. Es uno de los pocos lenguajes de programación
que no tiene una forma incorporada de hacer entrada- y salida. Por
lo tanto, JavaScript podría encajar en un enfoque bastante excéntrico
de Node para hacer entrada y salida sin terminar con dos interfaces
inconsistentes. En 2009, cuando se diseñó Node, la gente ya estaba
haciendo programación basada en llamadas en el navegador, por lo que
la comunidad en torno al lenguaje estaba acostumbrada a un estilo de
programación asíncrono.
453
El comando Node
Cuando Node.js se instala en un sistema, proporciona un programa
llamado node, que se utiliza para ejecutar archivos JavaScript. Digamos
que tienes un archivo hello.js, que contiene este código:
let message = "Hello world";
console.log(message);
454
[1, 2, 3]
> process.exit(0)
$
455
Módulos
Más allá de las referencias que mencioné, como console y process, Node
pone unas cuantas referencias adicionales en el ámbito global. Si quieres
acceder a la funcionalidad integrada, tienes que pedírsela al sistema de
módulos.
El sistema de módulos CommonJS, basado en la función require, se
describió en el Capítulo 10. Este sistema está incorporado en Node y se
usa para cargar cualquier cosa desde módulos incorporados hasta pa-
quetes descargados y archivos que forman parte de tu propio programa.
Cuando se llama a require, Node tiene que resolver la cadena dada a
un archivo real que pueda cargar. Las rutas que empiezan por /, ./, o
../ se resuelven relativamente a la ruta del módulo actual, donde . se
refiere al directorio actual, ../ a un directorio superior, y / a la raíz del
sistema de archivos. Así que si pide "./graph" del archivo /tmp/robot/
robot.js, Node intentará cargar el archivo /tmp/robot/graph.js.
La extension .js puede ser omitida, y Node la añadirá si existe tal
archivo. Si la ruta requerida se refiere a un directorio, Node intentará
cargar el archivo llamado index.js en ese directorio.
Cuando una cadena que no parece una ruta relativa o absoluta es
dada a require, se asume que se refiere a un módulo incorporado o a un
módulo instalado en un directorio node_modules. Por ejemplo, require("
fs") te dará el módulo incorporado del sistema de archivos de Node. Y
require("robot") podría intentar cargar la biblioteca que se encuentra
en node_modules/robot/. Una forma común de instalar tales librerías es
usando NPM, al cual volveremos en un momento.
Vamos a crear un pequeño proyecto que consiste en dos archivos. El
456
primero, llamado main.js, define un script que puede ser llamado desde
la línea de comandos para invertir una cadena.
const {reverse} = require("./reverse");
console.log(reverse(argument));
457
Instalación con NPM
NPM, el cual fue introducido en el Capítulo 10, es un repositorio en
línea de módulos JavaScript, muchos de los cuales están escritos especí-
ficamente para Node. Cuando instalas Node en tu ordenador, también
obtienes el comando npm, que puedes usar para interactuar con este
repositorio.
El uso principal de NPM es descargar paquetes. Vimos el paquete ini
en el Capítulo 10. nosotros podemos usar NPM para buscar e instalar
ese paquete en nuestro ordenador.
$ npm install ini
npm WARN enoent ENOENT: no such file or directory,
open '/tmp/package.json'
+ ini@1.3.5
added 1 package in 0.552s
$ node
> const {parse} = require("ini");
> parse("x = 1\ny = 2");
{ x: '1', y: '2' }
458
llamamos a require("ini"), esta librería se carga, y podemos llamar a
la propiedad parse para analizar un archivo de configuración.
Por defecto, NPM instala los paquetes bajo el directorio actual, en
lugar de una ubicación central. Si está acostumbrado a otros gestores
de paquetes, esto puede parecer inusual, pero tiene ventajas, pone a
cada aplicación en control total de los paquetes que instala y facilita la
gestión de versiones y la limpieza al eliminar una aplicación.
Archivos de paquetes
En el ejemplo npm install, se podía ver una advertencia sobre el hecho
de que el archivo package.json no existía. Se recomienda crear dicho
archivo para cada proyecto, ya sea manualmente o ejecutando npm init
. Este contiene alguna información sobre el proyecto, como lo son su
nombre y versión, y enumera sus dependencias.
La simulación del robot del Capítulo 7, tal como fue modularizada en
el ejercicio del Capítulo 10, podría tener un archivo package.json como
este:
{
"author": "Marijn Haverbeke",
"name": "eloquent-javascript-robot",
"description": "Simulation of a package-delivery robot",
"version": "1.0.0",
"main": "run.js",
"dependencies": {
"dijkstrajs": "^1.0.1",
"random-item": "^1.0.0"
459
},
"license": "ISC"
}
Versiones
Un archivo package.json lista tanto la versión propia del programa como
las versiones de sus dependencias. Las versiones son una forma de
lidiar con el hecho de que los paquetes evolucionan por separado, y el
código escrito para trabajar con un paquete tal y como existía en un
determinado momento pueda no funcionar con una versión posterior y
modificada del paquete.
NPM exige que sus paquetes sigan un esquema llamado versionado
semántico, el cual codifica cierta información sobre qué versiones son
compatibles (no rompan la vieja interfaz) en el número de versión. Una
versión semántica consiste en tres números, separados por puntos, como
lo es 2.3.0. Cada vez que una nueva funcionalidad es agregada, el
número intermedio tiene que ser incrementado. Cada vez que se rompe
la compatibilidad, de modo que el código existente que utiliza el paquete
podría no funcionar con esa nueva versión, el primer número tiene que
ser incrementado.
Un carácter cuña (^) delante del número de versión para una depen-
460
dencia en el package.json indica que cualquier versión compatible con
el número dado puede ser instalada. Así, por ejemplo, "^2.3.0" signifi-
caría que cualquier versión mayor o igual a 2.3.0 y menor a 3.0.0 está
permitida.
El comando npm también se usa para publicar nuevos paquetes o
nuevas versiones de paquetes. Si ejecutas npm publish en un directo-
rio que tiene un archivo package.json, esto publicará un paquete con el
nombre y la versión que aparece en el archivo JSON. Cualquiera puede
publicar paquetes en NPM, aunque sólo bajo un nombre de paquete
que no esté en uso todavía, ya que sería algo aterrador si personas al
azar pudieran actualizar los paquetes existentes.
Dado que el programa npm es una pieza de software que habla con un
sistema abierto—El registro de paquetes— no es el único que lo hace.
Otro programa, yarn, el cual puede ser instalado desde el registro NPM,
cumple el mismo papel que npm usando una interfaz y una estrategia de
instalación algo diferente.
Este libro no profundizará en los detalles del uso del NPM. Consulte
https://npmjs.org para obtener más documentación y formas para bus-
car paquetes.
461
un callback con el contenido del archivo.
let {readFile} = require("fs");
readFile("file.txt", "utf8", (error, text) => {
if (error) throw error;
console.log("The file contains:", text);
});
462
const {writeFile} = require("fs");
writeFile("graffiti.txt", "Node was here", err => {
if (err) console.log(`Failed to write file: ${err}`);
else console.log("File written.");
});
463
devolución de llamada.
const {readFile} = require("fs").promises;
readFile("file.txt", "utf8")
.then(text => console.log("The file contains:", text));
464
let server = createServer((request, response) => {
response.writeHead(200, {"Content-Type": "text/html"});
response.write(`
<h1>Hello!</h1>
<p>You asked for <code>${request.url}</code></p>`);
response.end();
});
server.listen(8000);
console.log("Listening! (port 8000)");
If you run this script on your own machine, you can point your web
browser at http://localhost:8000/hello to make a request to your server.
It will respond with a small HTML page.
The function passed as argument to createServer is called every time
a client connects to the server. The request and response bindings are
objects representing the incoming and outgoing data. The first contains
information about the request, such as its url property, which tells us
to what URL the request was made.
So, when you open that page in your browser, it sends a request to
your own computer. This causes the server function to run and send
back a response, which you can then see in the browser.
To send something back, you call methods on the response object.
The first, writeHead, will write out the response headers (see Chapter
?). You give it the status code (200 for “OK” in this case) and an object
that contains header values. The example sets the Content-Type header
to inform the client that we’ll be sending back an HTML document.
Next, the actual response body (the document itself) is sent with
465
response.write. You are allowed to call this method multiple times if
you want to send the response piece by piece, for example to stream
data to the client as it becomes available. Finally, response.end signals
the end of the response.
The call to server.listen causes the server to start waiting for con-
nections on port 8000. This is why you have to connect to localhost:8000
to speak to this server, rather than just localhost, which would use the
default port 80.
When you run this script, the process just sits there and waits. When
a script is listening for events—in this case, network connections—node
will not automatically exit when it reaches the end of the script. To
close it, press control-C.
A real web server usually does more than the one in the example—it
looks at the request’s method (the method property) to see what action
the client is trying to perform and looks at the request’s URL to find
out which resource this action is being performed on. We’ll see a more
advanced server later in this chapter.
To act as an HTTP client, we can use the request function in the
http module.
466
response.statusCode);
});
requestStream.end();
Streams
We have seen two instances of writable streams in the HTTP examples—
namely, the response object that the server could write to and the re-
467
quest object that was returned from request.
Writable streams are a widely used concept in Node. Such objects
have a write method that can be passed a string or a Buffer object to
write something to the stream. Their end method closes the stream and
optionally takes a value to write to the stream before closing. Both of
these methods can also be given a callback as an additional argument,
which they will call when the writing or closing has finished.
It is possible to create a writable stream that points at a file with
the createWriteStream function from the fs module. Then you can use
the write method on the resulting object to write the file one piece at
a time, rather than in one shot as with writeFile.
Readable streams are a little more involved. Both the request binding
that was passed to the HTTP server’s callback and the response binding
passed to the HTTP client’s callback are readable streams—a server
reads requests and then writes responses, whereas a client first writes
a request and then reads a response. Reading from a stream is done
using event handlers, rather than methods.
Objects that emit events in Node have a method called on that is
similar to the addEventListener method in the browser. You give it an
event name and then a function, and it will register that function to be
called whenever the given event occurs.
Readable streams have "data" and "end" events. The first is fired ev-
ery time data comes in, and the second is called whenever the stream is
at its end. This model is most suited for streaming data that can be im-
mediately processed, even when the whole document isn’t available yet.
A file can be read as a readable stream by using the createReadStream
468
function from fs.
This code creates a server that reads request bodies and streams them
back to the client as all-uppercase text:
const {createServer} = require("http");
createServer((request, response) => {
response.writeHead(200, {"Content-Type": "text/plain"});
request.on("data", chunk =>
response.write(chunk.toString().toUpperCase()));
request.on("end", () => response.end());
}).listen(8000);
The chunk value passed to the data handler will be a binary Buffer
. We can convert this to a string by decoding it as UTF-8 encoded
characters with its toString method.
The following piece of code, when run with the uppercasing server
active, will send a request to that server and write out the response it
gets:
const {request} = require("http");
request({
hostname: "localhost",
port: 8000,
method: "POST"
}, response => {
response.on("data", chunk =>
process.stdout.write(chunk.toString()));
}).end("Hello server");
// → HELLO SERVER
469
The example writes to process.stdout (the process’s standard output,
which is a writable stream) instead of using console.log. We can’t use
console.log because it adds an extra newline character after each piece
of text that it writes, which isn’t appropriate here since the response
may come in as multiple chunks.
A file server
Let’s combine our newfound knowledge about HTTP servers and work-
ing with the file system to create a bridge between the two: an HTTP
server that allows remote access to a file system. Such a server has all
kinds of uses—it allows web applications to store and share data, or it
can give a group of people shared access to a bunch of files.
When we treat files as HTTP resources, the HTTP methods GET, PUT,
and DELETE can be used to read, write, and delete the files, respectively.
We will interpret the path in the request as the path of the file that the
request refers to.
We probably don’t want to share our whole file system, so we’ll in-
terpret these paths as starting in the server’s working directory, which
is the directory in which it was started. If I ran the server from /tmp/
public/ (or C:\tmp\public\ on Windows), then a request for /file.txt
should refer to /tmp/public/file.txt (or C:\tmp\public\file.txt).
We’ll build the program piece by piece, using an object called methods
to store the functions that handle the various HTTP methods. Method
handlers are async functions that get the request object as argument and
470
return a promise that resolves to an object that describes the response.
const {createServer} = require("http");
This starts a server that just returns 405 error responses, which is the
code used to indicate that the server refuses to handle a given method.
471
When a request handler’s promise is rejected, the catch call translates
the error into a response object, if it isn’t one already, so that the server
can send back an error response to inform the client that it failed to
handle the request.
The status field of the response description may be omitted, in which
case it defaults to 200 (OK). The content type, in the type property,
can also be left off, in which case the response is assumed to be plain
text.
When the value of body is a readable stream, it will have a pipe
method that is used to forward all content from a readable stream to
a writable stream. If not, it is assumed to be either null (no body), a
string, or a buffer, and it is passed directly to the response’s end method.
To figure out which file path corresponds to a request URL, the
urlPath function uses Node’s built-in url module to parse the URL. It
takes its pathname, which will be something like "/file.txt", decodes
that to get rid of the %20-style escape codes, and resolves it relative to
the program’s working directory.
const {parse} = require("url");
const {resolve, sep} = require("path");
function urlPath(url) {
let {pathname} = parse(url);
let path = resolve(decodeURIComponent(pathname).slice(1));
if (path != baseDirectory &&
!path.startsWith(baseDirectory + sep)) {
472
throw {status: 403, body: "Forbidden"};
}
return path;
}
473
our server can’t simply return the same content type for all of them.
NPM can help us again here. The mime package (content type indicators
like text/plain are also called MIME types) knows the correct type for
a large number of file extensions.
The following npm command, in the directory where the server script
lives, installs a specific version of mime:
$ npm install mime@2.2.0
When a requested file does not exist, the correct HTTP status code
to return is 404. We’ll use the stat function, which looks up information
about a file, to find out both whether the file exists and whether it is a
directory.
const {createReadStream} = require("fs");
const {stat, readdir} = require("fs").promises;
const mime = require("mime");
474
} else {
return {body: createReadStream(path),
type: mime.getType(path)};
}
};
Because it has to touch the disk and thus might take a while, stat is
asynchronous. Since we’re using promises rather than callback style, it
has to be imported from promises instead of directly from fs.
When the file does not exist, stat will throw an error object with
a code property of "ENOENT". These somewhat obscure, Unix-inspired
codes are how you recognize error types in Node.
The stats object returned by stat tells us a number of things about
a file, such as its size (size property) and its modification date (mtime
property). Here we are interested in the question of whether it is a
directory or a regular file, which the isDirectory method tells us.
We use readdir to read the array of files in a directory and return
it to the client. For normal files, we create a readable stream with
createReadStream and return that as the body, along with the content
type that the mime package gives us for the file’s name.
The code to handle DELETE requests is slightly simpler.
const {rmdir, unlink} = require("fs").promises;
475
stats = await stat(path);
} catch (error) {
if (error.code != "ENOENT") throw error;
else return {status: 204};
}
if (stats.isDirectory()) await rmdir(path);
else await unlink(path);
return {status: 204};
};
When an HTTP response does not contain any data, the status code
204 (“no content”) can be used to indicate this. Since the response to
deletion doesn’t need to transmit any information beyond whether the
operation succeeded, that is a sensible thing to return here.
You may be wondering why trying to delete a nonexistent file returns
a success status code, rather than an error. When the file that is be-
ing deleted is not there, you could say that the request’s objective is
already fulfilled. The HTTP standard encourages us to make requests
idempotent, which means that making the same request multiple times
produces the same result as making it once. In a way, if you try to
delete something that’s already gone, the effect you were trying to do
has been achieved—the thing is no longer there.
This is the handler for PUT requests:
const {createWriteStream} = require("fs");
476
from.on("error", reject);
to.on("error", reject);
to.on("finish", resolve);
from.pipe(to);
});
}
We don’t need to check whether the file exists this time—if it does,
we’ll just overwrite it. We again use pipe to move data from a readable
stream to a writable one, in this case from the request to the file. But
since pipe isn’t written to return a promise, we have to write a wrapper,
pipeStream, that creates a promise around the outcome of calling pipe.
When something goes wrong when opening the file, createWriteStream
will still return a stream, but that stream will fire an "error" event.
The output stream to the request may also fail, for example if the
network goes down. So we wire up both streams’ "error" events to
reject the promise. When pipe is done, it will close the output stream,
which causes it to fire a "finish" event. That’s the point where we can
successfully resolve the promise (returning nothing).
The full script for the server is available at https://eloquentjavascript.net/
code/file_server.js. You can download that and, after installing its de-
477
pendencies, run it with Node to start your own file server. And, of
course, you can modify and extend it to solve this chapter’s exercises
or to experiment.
The command line tool curl, widely available on Unix-like systems
(such as macOS and Linux), can be used to make HTTP requests. The
following session briefly tests our server. The -X option is used to set
the request’s method, and -d is used to include a request body.
$ curl http://localhost:8000/file.txt
File not found
$ curl -X PUT -d hello http://localhost:8000/file.txt
$ curl http://localhost:8000/file.txt
hello
$ curl -X DELETE http://localhost:8000/file.txt
$ curl http://localhost:8000/file.txt
File not found
The first request for file.txt fails since the file does not exist yet. The
PUT request creates the file, and behold, the next request successfully
retrieves it. After deleting it with a DELETE request, the file is again
missing.
Summary
Node is a nice, small system that lets us run JavaScript in a nonbrowser
context. It was originally designed for network tasks to play the role of
a node in a network. But it lends itself to all kinds of scripting tasks,
478
and if writing JavaScript is something you enjoy, automating tasks with
Node works well.
NPM provides packages for everything you can think of (and quite
a few things you’d probably never think of), and it allows you to fetch
and install those packages with the npm program. Node comes with a
number of built-in modules, including the fs module for working with
the file system and the http module for running HTTP servers and
making HTTP requests.
All input and output in Node is done asynchronously, unless you ex-
plicitly use a synchronous variant of a function, such as readFileSync.
When calling such asynchronous functions, you provide callback func-
tions, and Node will call them with an error value and (if available) a
result when it is ready.
Exercises
Search tool
On Unix systems, there is a command line tool called grep that can be
used to quickly search files for a regular expression.
Write a Node script that can be run from the command line and
acts somewhat like grep. It treats its first command line argument as a
regular expression and treats any further arguments as files to search. It
should output the names of any file whose content matches the regular
expression.
When that works, extend it so that when one of the arguments is a
directory, it searches through all files in that directory and its subdi-
479
rectories.
Use asynchronous or synchronous file system functions as you see fit.
Setting things up so that multiple asynchronous actions are requested
at the same time might speed things up a little, but not a huge amount,
since most file systems can read only one thing at a time.
Directory creation
Though the DELETE method in our file server is able to delete directories
(using rmdir), the server currently does not provide any way to create
a directory.
Add support for the MKCOL method (“make collection”), which should
create a directory by calling mkdir from the fs module. MKCOL is not a
widely used HTTP method, but it does exist for this same purpose in
the WebDAV standard, which specifies a set of conventions on top of
HTTP that make it suitable for creating documents.
480
Next, as an advanced exercise or even a weekend project, combine all
the knowledge you gained from this book to build a more user-friendly
interface for modifying the website—from inside the website.
Use an HTML form to edit the content of the files that make up the
website, allowing the user to update them on the server by using HTTP
requests, as described in Chapter ?.
Start by making only a single file editable. Then make it so that
the user can select which file to edit. Use the fact that our file server
returns lists of files when reading a directory.
Don’t work directly in the code exposed by the file server since if you
make a mistake, you are likely to damage the files there. Instead, keep
your work outside of the publicly accessible directory and copy it there
when testing.
481
“Si tienes conocimiento, deja a otros encender sus velas allí.”
—Margaret Fuller
Chapter 17
Proyecto: Sitio web para compartir
habilidades
Una reunión para compartir habilidades es un evento en donde personas
con intereses comunes se juntan para dar pequeñas presentaciones in-
formales acerca de cosas que saben. En un reunión de jardinería alguien
explicaría como cultivar apio. O en un grupo de programación, podrías
presentarte y hablarle a las personas acerca de Node.js.
En estas reuniones, también llamadas grupos de usuarios cuando se
tratan de computadoras, son una forma genial de ampliar tus hori-
zontes, aprender acerca de nuevos desarrollos, o simplemente conoce
gente con intereses similares. Muchas ciudades grandes tienen reuniones
sobre JavaScript. Típicamente la entrada es gratis, y las que he visitado
han sido amistosas y cálidas.
En este capítulo de proyecto final, nuestra meta es construir un sitio
web para administrar las pláticas dadas en una reunión para compartir
habilidades. Imagina un pequeño grupo de gente que se reúne regular-
mente en las oficinas de uno de ellos para hablar de monociclismo. El
organizador previo se fue a otra ciudad, y nadie se postuló para tomar
482
esta tarea. Queremos un sistema que deje a los participantes proponer
y discutir pláticas entre ellos, sin un organizador central.
El proyecto completo puede ser descargado de https://eloquentjavascript.
code/skillsharing.zip (En inglés).
Diseño
El proyecto tiene un parte de servidor, escrita para Node.js, y una
parte cliente, escrita para el navegador. La parte del servidor guarda
la información del sistema y se lo pasa al cliente. Además, sirve los
archivos que implementan la parte cliente.
El servidor mantiene la lista de exposiciones propuestas para la próx-
ima charla, y el cliente muestra la lista. Cada charla tiene el nombre
del presentados, un título, un resumen, y una lista de comentarios aso-
ciados. el cliente permite proponer nuevas charlas, (agregándolas a la
lista), borrar charlas y comentar en las existentes. Cuando un usuario
hace un cambio, el cliente hace la petición HTTP para notificar al servi-
dor.
483
{{index “vista en vivo”, “experiencia de usuario”, “empujar datos”,
conexión}}
La aplicación será construida para mostrar una vista en vivo de las
charlas propuestas y sus comentarios. Cuando alguien en algún lugar
manda una nueva charla o agrega un comentario, todas las personas
que tienen la página abierta en sus navegadores deberían ver inmedi-
atamente el cambio. Esto nos reta un poco: no hay forma de que el
servidor abra una conexión a un cliente, ni una buena forma de saber
484
qué clientes están viendo un sitio web.
Una solución común a este problema es llamada sondeo largo (le
llamaremos long polling), que de casualidad es una de las motivaciones
para el diseño de Node.
Long Polling
{{index firewall, notificación, “long polling”, red, [navegador, seguri-
dad]}}
Para ser capaces de notificar inmediatamente a un cliente que algo
ha cambiado, necesitamos una conexión con ese cliente. Como los nave-
gadores normalmente no aceptan conexiones y los clientes están detrás
de routers que bloquearían la conexión de todas maneras, hacer que el
servidor inicie la conexión no es práctico.
Podemos hacer que el cliente abra la conexión y la mantenga de tal
manera que el servidor pueda usarla para mandar información cunado
lo necesite.
Pero una petición HTTP permite sólamente un flujo simple de infor-
mación: el cliente manda una petición, el servidor responde con una
sola respuesta, y eso es todo. Existe una tecnología moderna llamada
WebSockets, soportada por los principales navegadores, que hace posi-
ble abrir conexiones para intercambio arbitrario de datos. Pero usarla
correctamente es un poco complicado.
En este capítulo usamos una técnica más simple, el long polling en
donde los clientes continuamente le piden al servidor nueva información
usando las peticiones HTTP regulares, y el server detiene su respuesta
485
cuando no hay nada nuevo que reportar.
Mientras el cliente se asegure de tener constantemente abierta una
petición de sondeo, recibirá nueva información del servidor muy poco
tiempo después de que esté disponible. Por ejemplo, si Fatma tiene
nuestra aplicación abierta in su navegador, ese navegador ya habrá
hecho una petición de actualizaciones y estará esperando por una re-
spuesta a esa petición. Cuando Iman manda una charla acerca de Ba-
jada Extrema en Monociclo, el servidor se dará cuenta de que Fatma
está esperando actualizaciones y mandará una respuesta conteniendo
la nueva charla, respondiendo a la petición pendiente. El navegador de
Fatma recibirá los datos y actualizará la pantalla.
Para evitar que las conexiones se venzan (que sean abortadas por falta
de actividad), las técnicas de long polling usualmente ponen el tiempo
máximo para cada petición después de lo cuál el servidor responderá de
todos modos aunque no tenga nada que reportar, después de lo cuál el
cliente iniciará otra petición. Reiniciar periódicamente la petición hace
además la técnica más robusta, permitiendo a los clientes recuperarse
de fallas temporales en la conexión o problemas del servidor.
Un servidor ocupado que esté usando long polling podría tener miles
de peticiones esperando, y por lo tanto miles de conexiones TCP abier-
tas. Node, que hace fácil de manejar muchas conexiones sin crear un
hilo de control para cada una, es un buen elemento para nuestro sis-
tema.
486
HTTP interface
Before we start designing either the server or the client, let’s think
about the point where they touch: the HTTP interface over which they
communicate.
We will use JSON as the format of our request and response body.
Like in the file server from Chapter 17, we’ll try to make good use of
HTTP methods and headers. The interface is centered around the /
talks path. Paths that do not start with /talks will be used for serving
static files—the HTML and JavaScript code for the client-side system.
A GET request to /talks returns a JSON document like this:
[{"title": "Unituning",
"presenter": "Jamal",
"summary": "Modifying your cycle for extra style",
"comments": []}]
487
A request to create a talk about idling might look something like this:
PUT /talks/How%20to%20Idle HTTP/1.1
Content-Type: application/json
Content-Length: 92
{"presenter": "Maureen",
"summary": "Standing still on a unicycle"}
Such URLs also support GET requests to retrieve the JSON represen-
tation of a talk and DELETE requests to delete a talk.
Adding a comment to a talk is done with a POST request to a URL
like /talks/Unituning/comments, with a JSON body that has author and
message properties.
{"author": "Iman",
"message": "Will you talk about raising a cycle?"}
488
Its value is a string that identifies the current version of the resource.
Clients, when they later request that resource again, may make a condi-
tional request by including an If-None-Match header whose value holds
that same string. If the resource hasn’t changed, the server will re-
spond with status code 304, which means “not modified”, telling the
client that its cached version is still current. When the tag does not
match, the server responds as normal.
We need something like this, where the client can tell the server which
version of the list of talks it has, and the server responds only when that
list has changed. But instead of immediately returning a 304 response,
the server should stall the response and return only when something
new is available or a given amount of time has elapsed. To distinguish
long polling requests from normal conditional requests, we give them
another header, Prefer: wait=90, which tells the server that the client
is willing to wait up to 90 seconds for the response.
The server will keep a version number that it updates every time
the talks change and will use that as the ETag value. Clients can make
requests like this to be notified when the talks change:
GET /talks HTTP/1.1
If-None-Match: "4"
Prefer: wait=90
(time passes)
HTTP/1.1 200 OK
Content-Type: application/json
ETag: "5"
489
Content-Length: 295
[....]
The protocol described here does not do any access control. Every-
body can comment, modify talks, and even delete them. (Since the
Internet is full of hooligans, putting such a system online without fur-
ther protection probably wouldn’t end well.)
The server
Let’s start by building the server-side part of the program. The code
in this section runs on Node.js.
Routing
Our server will use createServer to start an HTTP server. In the func-
tion that handles a new request, we must distinguish between the vari-
ous kinds of requests (as determined by the method and the path) that
we support. This can be done with a long chain of if statements, but
there is a nicer way.
A router is a component that helps dispatch a request to the function
that can handle it. You can tell the router, for example, that PUT
requests with a path that matches the regular expression /^\/talks
\/([^\/]+)$/ (/talks/ followed by a talk title) can be handled by a
given function. In addition, it can help extract the meaningful parts
490
of the path (in this case the talk title), wrapped in parentheses in the
regular expression, and pass them to the handler function.
There are a number of good router packages on NPM, but here we’ll
write one ourselves to illustrate the principle.
This is router.js, which we will later require from our server module:
const {parse} = require("url");
The module exports the Router class. A router object allows new
491
handlers to be registered with the add method and can resolve requests
with its resolve method.
The latter will return a response when a handler was found, and null
otherwise. It tries the routes one at a time (in the order in which they
were defined) until a matching one is found.
The handler functions are called with the context value (which will
be the server instance in our case), match strings for any groups they
defined in their regular expression, and the request object. The strings
have to be URL-decoded since the raw URL may contain %20-style
codes.
Serving files
When a request matches none of the request types defined in our router,
the server must interpret it as a request for a file in the public directory.
It would be possible to use the file server defined in Chapter 17 to serve
such files, but we neither need nor want to support PUT and DELETE
requests on files, and we would like to have advanced features such as
support for caching. So let’s use a solid, well-tested static file server
from NPM instead.
I opted for ecstatic. This isn’t the only such server on NPM, but
it works well and fits our purposes. The ecstatic package exports a
function that can be called with a configuration object to produce a
request handler function. We use the root option to tell the server
where it should look for files. The handler function accepts request
and response parameters and can be passed directly to createServer
to create a server that serves only files. We want to first check for re-
492
quests that we should handle specially, though, so we wrap it in another
function.
const {createServer} = require("http");
const Router = require("./router");
const ecstatic = require("ecstatic");
class SkillShareServer {
constructor(talks) {
this.talks = talks;
this.version = 0;
this.waiting = [];
493
fileServer(request, response);
}
});
}
start(port) {
this.server.listen(port);
}
stop() {
this.server.close();
}
}
This uses a similar convention as the file server from the previous
chapter for responses—handlers return promises that resolve to objects
describing the response. It wraps the server in an object that also holds
its state.
Talks as resources
The talks that have been proposed are stored in the talks property of
the server, an object whose property names are the talk titles. These
will be exposed as HTTP resources under /talks/[title], so we need
to add handlers to our router that implement the various methods that
clients can use to work with them.
The handler for requests that GET a single talk must look up the
talk and respond either with the talk’s JSON data or with a 404 error
response.
494
const talkPath = /^\/talks\/([^\/]+)$/;
The updated method, which we will define later, notifies waiting long
polling requests about the change.
To retrieve the content of a request body, we define a function called
readStream, which reads all content from a readable stream and returns
a promise that resolves to a string.
function readStream(stream) {
495
return new Promise((resolve, reject) => {
let data = "";
stream.on("error", reject);
stream.on("data", chunk => data += chunk.toString());
stream.on("end", () => resolve(data));
});
}
One handler that needs to read request bodies is the PUT handler,
which is used to create new talks. It has to check whether the data it
was given has presenter and summary properties, which are strings. Any
data coming from outside the system might be nonsense, and we don’t
want to corrupt our internal data model or crash when bad requests
come in.
If the data looks valid, the handler stores an object that represents
the new talk in the talks object, possibly overwriting an existing talk
with this title, and again calls updated.
router.add("PUT", talkPath,
async (server, title, request) => {
let requestBody = await readStream(request);
let talk;
try { talk = JSON.parse(requestBody); }
catch (_) { return {status: 400, body: "Invalid JSON"}; }
if (!talk ||
typeof talk.presenter != "string" ||
typeof talk.summary != "string") {
496
return {status: 400, body: "Bad talk data"};
}
server.talks[title] = {title,
presenter: talk.presenter,
summary: talk.summary,
comments: []};
server.updated();
return {status: 204};
});
if (!comment ||
typeof comment.author != "string" ||
typeof comment.message != "string") {
return {status: 400, body: "Bad comment data"};
} else if (title in server.talks) {
server.talks[title].comments.push(comment);
server.updated();
return {status: 204};
497
} else {
return {status: 404, body: `No talk '${title}' found`};
}
});
498
The handler itself needs to look at the request headers to see whether
If-None-Match and Prefer headers are present. Node stores headers,
whose names are specified to be case insensitive, under their lowercase
names.
router.add("GET", /^\/talks$/, async (server, request) => {
let tag = /"(.*)"/.exec(request.headers["if-none-match"]);
let wait = /\bwait=(\d+)/.exec(request.headers["prefer"]);
if (!tag || tag[1] != server.version) {
return server.talkResponse();
} else if (!wait) {
return {status: 304};
} else {
return server.waitForChanges(Number(wait[1]));
}
});
If no tag was given or a tag was given that doesn’t match the server’s
current version, the handler responds with the list of talks. If the
request is conditional and the talks did not change, we consult the
Prefer header to see whether we should delay the response or respond
right away.
Callback functions for delayed requests are stored in the server’s
waiting array so that they can be notified when something happens.
The waitForChanges method also immediately sets a timer to respond
with a 304 status when the request has waited long enough.
SkillShareServer.prototype.waitForChanges = function(time) {
499
return new Promise(resolve => {
this.waiting.push(resolve);
setTimeout(() => {
if (!this.waiting.includes(resolve)) return;
this.waiting = this.waiting.filter(r => r != resolve);
resolve({status: 304});
}, time * 1000);
});
};
new SkillShareServer(Object.create(null)).start(8000);
500
The client
The client-side part of the skill-sharing website consists of three files: a
tiny HTML page, a style sheet, and a JavaScript file.
HTML
It is a widely used convention for web servers to try to serve a file named
index.html when a request is made directly to a path that corresponds
to a directory. The file server module we use, ecstatic, supports this
convention. When a request is made to the path /, the server looks for
the file ./public/index.html (./public being the root we gave it) and
returns that file if found.
Thus, if we want a page to show up when a browser is pointed at our
server, we should put it in public/index.html. This is our index file:
<!doctype html>
<meta charset="utf-8">
<title>Skill Sharing</title>
<link rel="stylesheet" href="skillsharing.css">
<h1>Skill Sharing</h1>
<script src="skillsharing_client.js"></script>
It defines the document title and includes a style sheet, which defines
a few styles to, among other things, make sure there is some space
between talks.
501
At the bottom, it adds a heading at the top of the page and loads
the script that contains the client-side application.
Actions
The application state consists of the list of talks and the name of the
user, and we’ll store it in a {talks, user} object. We don’t allow the
user interface to directly manipulate the state or send off HTTP re-
quests. Rather, it may emit actions that describe what the user is
trying to do.
The handleAction function takes such an action and makes it happen.
Because our state updates are so simple, state changes are handled in
the same function.
function handleAction(state, action) {
if (action.type == "setUser") {
localStorage.setItem("userName", action.user);
return Object.assign({}, state, {user: action.user});
} else if (action.type == "setTalks") {
return Object.assign({}, state, {talks: action.talks});
} else if (action.type == "newTalk") {
fetchOK(talkURL(action.title), {
method: "PUT",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({
presenter: state.user,
summary: action.summary
})
}).catch(reportError);
502
} else if (action.type == "deleteTalk") {
fetchOK(talkURL(action.talk), {method: "DELETE"})
.catch(reportError);
} else if (action.type == "newComment") {
fetchOK(talkURL(action.talk) + "/comments", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({
author: state.user,
message: action.message
})
}).catch(reportError);
}
return state;
}
503
This helper function is used to build up a URL for a talk with a given
title.
function talkURL(title) {
return "talks/" + encodeURIComponent(title);
}
When the request fails, we don’t want to have our page just sit there,
doing nothing without explanation. So we define a function called
reportError, which at least shows the user a dialog that tells them
something went wrong.
function reportError(error) {
alert(String(error));
}
Rendering components
We’ll use an approach similar to the one we saw in Chapter ?, splitting
the application into components. But since some of the components
either never need to update or are always fully redrawn when updated,
we’ll define those not as classes but as functions that directly return
a DOM node. For example, here is a component that shows the field
where the user can enter their name:
function renderUserField(name, dispatch) {
504
return elt("label", {}, "Your name: ", elt("input", {
type: "text",
value: name,
onchange(event) {
dispatch({type: "setUser", user: event.target.value});
}
}));
}
The elt function used to construct DOM elements is the one we used
in Chapter ?.
A similar function is used to render talks, which include a list of
comments and a form for adding a new comment.
function renderTalk(talk, dispatch) {
return elt(
"section", {className: "talk"},
elt("h2", null, talk.title, " ", elt("button", {
type: "button",
onclick() {
dispatch({type: "deleteTalk", talk: talk.title});
}
}, "Delete")),
elt("div", null, "by ",
elt("strong", null, talk.presenter)),
elt("p", null, talk.summary),
...talk.comments.map(renderComment),
elt("form", {
onsubmit(event) {
505
event.preventDefault();
let form = event.target;
dispatch({type: "newComment",
talk: talk.title,
message: form.elements.comment.value});
form.reset();
}
}, elt("input", {type: "text", name: "comment"}), " ",
elt("button", {type: "submit"}, "Add comment")));
}
The "submit" event handler calls form.reset to clear the form’s con-
tent after creating a "newComment" action.
When creating moderately complex pieces of DOM, this style of pro-
gramming starts to look rather messy. There’s a widely used (non-
standard) JavaScript extension called JSX that lets you write HTML
directly in your scripts, which can make such code prettier (depending
on what you consider pretty). Before you can actually run such code,
you have to run a program on your script to convert the pseudo-HTML
into JavaScript function calls much like the ones we use here.
Comments are simpler to render.
function renderComment(comment) {
return elt("p", {className: "comment"},
elt("strong", null, comment.author),
": ", comment.message);
}
506
Finally, the form that the user can use to create a new talk is rendered
like this:
function renderTalkForm(dispatch) {
let title = elt("input", {type: "text"});
let summary = elt("input", {type: "text"});
return elt("form", {
onsubmit(event) {
event.preventDefault();
dispatch({type: "newTalk",
title: title.value,
summary: summary.value});
event.target.reset();
}
}, elt("h3", null, "Submit a Talk"),
elt("label", null, "Title: ", title),
elt("label", null, "Summary: ", summary),
elt("button", {type: "submit"}, "Submit"));
}
Polling
To start the app we need the current list of talks. Since the initial load
is closely related to the long polling process—the ETag from the load
must be used when polling—we’ll write a function that keeps polling
the server for /talks and calls a callback function when a new set of
talks is available.
507
async function pollTalks(update) {
let tag = undefined;
for (;;) {
let response;
try {
response = await fetchOK("/talks", {
headers: tag && {"If-None-Match": tag,
"Prefer": "wait=90"}
});
} catch (e) {
console.log("Request failed: " + e);
await new Promise(resolve => setTimeout(resolve, 500));
continue;
}
if (response.status == 304) continue;
tag = response.headers.get("ETag");
update(await response.json());
}
}
This is an async function so that looping and waiting for the request
is easier. It runs an infinite loop that, on each iteration, retrieves the
list of talks—either normally or, if this isn’t the first request, with the
headers included that make it a long polling request.
When a request fails, the function waits a moment and then tries
again. This way, if your network connection goes away for a while and
then comes back, the application can recover and continue updating.
The promise resolved via setTimeout is a way to force the async function
508
to wait.
When the server gives back a 304 response, that means a long polling
request timed out, so the function should just immediately start the
next request. If the response is a normal 200 response, its body is read
as JSON and passed to the callback, and its ETag header value is stored
for the next iteration.
The application
The following component ties the whole user interface together:
class SkillShareApp {
constructor(state, dispatch) {
this.dispatch = dispatch;
this.talkDOM = elt("div", {className: "talks"});
this.dom = elt("div", null,
renderUserField(state.user, dispatch),
this.talkDOM,
renderTalkForm(dispatch));
this.syncState(state);
}
syncState(state) {
if (state.talks != this.talks) {
this.talkDOM.textContent = "";
for (let talk of state.talks) {
this.talkDOM.appendChild(
renderTalk(talk, this.dispatch));
}
this.talks = state.talks;
509
}
}
}
When the talks change, this component redraws all of them. This is
simple but also wasteful. We’ll get back to that in the exercises.
We can start the application like this:
function runApp() {
let user = localStorage.getItem("userName") || "Anon";
let state, app;
function dispatch(action) {
state = handleAction(state, action);
app.syncState(state);
}
pollTalks(talks => {
if (!app) {
state = {user, talks};
app = new SkillShareApp(state, dispatch);
document.body.appendChild(app.dom);
} else {
dispatch({type: "setTalks", talks});
}
}).catch(reportError);
}
runApp();
510
If you run the server and open two browser windows for http://localhost:8
next to each other, you can see that the actions you perform in one win-
dow are immediately visible in the other.
Exercises
The following exercises will involve modifying the system defined in
this chapter. To work on them, make sure you download the code
first (https://eloquentjavascript.net/code/skillsharing.zip), have Node
installed https://nodejs.org, and have installed the project’s dependency
with npm install.
Disk persistence
The skill-sharing server keeps its data purely in memory. This means
that when it crashes or is restarted for any reason, all talks and com-
ments are lost.
Extend the server so that it stores the talk data to disk and auto-
matically reloads the data when it is restarted. Do not worry about
efficiency—do the simplest thing that works.
511
another, add a comment to that talk, the field in the first window will
be redrawn, removing both its content and its focus.
In a heated discussion, where multiple people are adding comments
at the same time, this would be annoying. Can you come up with a
way to solve it?
512
Exercise Hints
The hints below might help when you are stuck with one of the exercises
in this book. They don’t give away the entire solution, but rather try
to help you find it yourself.
Estructura de Programa
Ciclo de un triángulo
Puedes comenzar con un programa que imprima los números del 1 al
7, al que puedes derivar haciendo algunas modificaciones al ejemplo de
impresión de números pares dado anteriormente en el capítulo, donde
se introdujo el ciclo for.
Ahora considera la equivalencia entre números y strings de caracteres
de numeral. Puedes ir de 1 a 2 agregando 1 (+= 1). Puedes ir de "#"
a "##" agregando un caracter (+= "#"). Por lo tanto, tu solución puede
seguir de cerca el programa de impresión de números.
513
FizzBuzz
Ir a traves de los números es claramente el trabajo de un ciclo y selec-
cionar qué imprimir es una cuestión de ejecución condicional. Recuerda
el truco de usar el operador restante (%) para verificar si un número es
divisible por otro número (tiene un residuo de cero).
En la primera versión, hay tres resultados posibles para cada número,
por lo que tendrás que crear una cadena if/else if/else.
La segunda versión del programa tiene una solución directa y una
inteligente. La manera simple es agregar otra “rama” condicional para
probar precisamente la condición dada. Para el método inteligente, crea
un string que contenga la palabra o palabras a imprimir e imprimir
ya sea esta palabra o el número si no hay una palabra, posiblemente
haciendo un buen uso del operador ||.
Tablero de ajedrez
El string se puede construir comenzando con un string vacío ("") y
repetidamente agregando caracteres. Un carácter de nueva línea se
escribe "\n".
Para trabajar con dos dimensiones, necesitarás un ciclo dentro de un
ciclo. Coloca llaves alrededor de los cuerpos de ambos ciclos para hacer
fácil de ver dónde comienzan y terminan. Intenta indentar adecuada-
mente estos cuerpos. El orden de los ciclos debe seguir el orden en el
que construimos el string (línea por línea, izquierda a derecha, arriba
a abajo). Entonces el ciclo externo maneja las líneas y el ciclo interno
maneja los caracteres en una sola linea.
514
Necesitará dos vinculaciones para seguir tu progreso. Para saber si
debes poner un espacio o un signo de numeral en una posición determi-
nada, podrías probar si la suma de los dos contadores es par (% 2).
Terminar una línea al agregar un carácter de nueva línea debe suceder
después de que la línea ha sido creada, entonces haz esto después del
ciclo interno pero dentro del bucle externo.
Funciones
Mínimo
Si tienes problemas para poner llaves y paréntesis en los lugares correc-
tos para obtener una definición válida de función, comienza copiando
uno de los ejemplos en este capítulo y modificándolo.
Una función puede contener múltiples declaraciones de return.
Recursión
Es probable que tu función se vea algo similar a la función interna
encontrar en la función recursiva encontrarSolucion de ejemplo en este
capítulo, con una cadena if/else if/else que prueba cuál de los tres
casos aplica. El else final, correspondiente al tercer caso, hace la lla-
mada recursiva. Cada una de las ramas debe contener una declaración
de return u organizarse de alguna otra manera para que un valor es-
pecífico sea retornado.
Cuando se le dé un número negativo, la función volverá a repetirse
una y otra vez, pasándose a si misma un número cada vez más negativo,
515
quedando así más y más lejos de devolver un resultado. Eventualmente
quedandose sin espacio en la pila y abortando el programa.
Conteo de frijoles
TU función necesitará de un ciclo que examine cada carácter en el string.
Puede correr desde un índice de cero a uno por debajo de su longitud
(< string.length). Si el carácter en la posición actual es el mismo al
que se está buscando en la función, agrega 1 a una variable contador.
Una vez que el ciclo haya terminado, puedes retornat el contador.
Ten cuidado de hacer que todos las vinculaciones utilizadas en la
función sean locales a la función usando la palabra clave let o const.
516
hacia arriba y otro para contar hacia abajo—ya que la comparación que
verifica si el ciclo está terminado necesita ser >= en lugar de <= cuando
se cuenta hacia abajo.
También puede que valga la pena utilizar un paso predeterminado
diferente, es decir -1, cuando el final del rango sea menor que el inicio.
De esa manera, rango(5, 2) retornaria algo significativo, en lugar de
quedarse atascado en un ciclo infinito. Es posible referirse a parámetros
anteriores en el valor predeterminado de un parámetro.
Revirtiendo un array
Hay dos maneras obvias de implementar revertirArray. La primera es
simplemente pasar a traves del array de entrada de adelante hacia atrás
y usar el metodo unshift en el nuevo array para insertar cada elemento
en su inicio. La segundo es hacer un ciclo sobre el array de entrada de
atrás hacia adelante y usar el método push. Iterar sobre un array al
revés requiere de una especificación (algo incómoda) del ciclo for, como
(let i = array.length - 1; i >= 0; i--).
Revertir al array en su lugar es más difícil. Tienes que tener cuidado
de no sobrescribir elementos que necesitarás luego. Usar revertirArray
o de lo contrario, copiar toda el array (array.slice(0) es una buena
forma de copiar un array) funciona pero estás haciendo trampa.
El truco consiste en intercambiar el primer y el último elemento,
luego el segundo y el penúltimo, y así sucesivamente. Puedes hacer
esto haciendo un ciclo basandote en la mitad de la longitud del ar-
ray (use Math.floor para redondear—no necesitas tocar el elemento del
medio en un array con un número impar de elementos) e intercambiar
517
el elemento en la posición i con el de la posición array.length - 1 -
i. Puedes usar una vinculación local para aferrarse brevemente a uno
de los elementos, sobrescribirlo con su imagen espejo, y luego poner
el valor de la vinculación local en el lugar donde solía estar la imagen
espejo.
Una lista
Crear una lista es más fácil cuando se hace de atrás hacia adelante. En-
tonces arrayALista podría iterar sobre el array hacia atrás (ver ejercicio
anterior) y, para cada elemento, agregar un objeto a la lista. Puedes
usar una vinculación local para mantener la parte de la lista que se con-
struyó hasta el momento y usar una asignación como lista = {valor:
X, resto: lista} para agregar un elemento.
Para correr a traves de una lista (en listaAArray y posicion), una
especificación del ciclo for como esta se puede utilizar:
for (let nodo = lista; nodo; nodo = nodo.resto) {}
Puedes ver cómo eso funciona? En cada iteración del ciclo, nodo
apunta a la sublista actual, y el cuerpo puede leer su propiedad valor
para obtener el elemento actual. Al final de una iteración, nodo se mueve
a la siguiente sublista. Cuando eso es nulo, hemos llegado al final de la
lista y el ciclo termina.
La versión recursiva de posición, de manera similar, mirará a una
parte más pequeña de la “cola” de la lista y, al mismo tiempo, contara
atrás el índice hasta que llegue a cero, en cuyo punto puede retornar la
518
propiedad valor del nodo que está mirando. Para obtener el elemento
cero de una lista, simplemente toma la propiedad valor de su nodo
frontal. Para obtener el elemento N + 1, toma el elemento N de la lista
que este en la propiedad resto de esta lista.
Comparación profunda
Tu prueba de si estás tratando con un objeto real se verá algo así
como typeof x == "object" && x != null. Ten cuidado de comparar
propiedades solo cuando ambos argumentos sean objetos. En todo los
otros casos, puede retornar inmediatamente el resultado de aplicar ===.
Usa Object.keys para revisar las propiedades. Necesitas probar si
ambos objetos tienen el mismo conjunto de nombres de propiedad y
si esos propiedades tienen valores idénticos. Una forma de hacerlo es
garantizar que ambos objetos tengan el mismo número de propiedades
(las longitudes de las listas de propiedades son las mismas). Y luego, al
hacer un ciclo sobre una de las propiedades del objeto para compararlos,
siempre asegúrate primero de que el otro realmente tenga una propiedad
con ese mismo nombre. Si tienen el mismo número de propiedades, y
todas las propiedades en uno también existen en el otro, tienen el mismo
conjunto de nombres de propiedad.
Retornar el valor correcto de la función se realiza mejor al inmediata-
mente retornar falso cuando se encuentre una discrepancia y retornar
verdadero al final de la función.
519
Funciones de Orden Superior
Cada
Al igual que el operador &&, el método every puede dejar de evaluar
más elementos tan pronto como haya encontrado uno que no coincida.
Entonces la versión basada en un ciclo puede saltar fuera del ciclo—con
break o return—tan pronto como se encuentre con un elemento para el
cual la función predicado retorne falso. Si el ciclo corre hasta su final
sin encontrar tal elemento, sabemos que todos los elementos coinciden
y debemos retornar verdadero.
Para construir cada usando some, podemos aplicar las leyes De Mor-
gan, que establecen que a && b es igual a !(!a ||! b). Esto puede ser
generalizado a arrays, donde todos los elementos del array coinciden si
no hay elemento en el array que no coincida.
520
La Vida Secreta de los Objetos
Un tipo vector
Mira de nuevo al ejemplo de la clase Conejo si no recuerdas muy bien
como se ven las declaraciones de clases.
Agregar una propiedad getter al constructor se puede hacer al poner
la palabra get antes del nombre del método. Para calcular la distancia
desde (0, 0) a (x, y), puedes usar el teorema de Pitágoras, que dice que
el cuadrado de la distancia que estamos buscando es igual al cuadrado
de
√ la coordenada x más el cuadrado de la coordenada y. Por lo tanto,
x2 + y 2 es el número que quieres, y Math.sqrt es la forma en que
calculas una raíz cuadrada en JavaScript.
Conjuntos
La forma más fácil de hacer esto es almacenar un array con los miembros
del conjunto en una propiedad de instancia. Los métodos includes o
indexOf pueden ser usados para verificar si un valor dado está en el
array.
El constructor de clase puede establecer la colección de miembros
como un array vacio. Cuando se llama a añadir, debes verificar si el
valor dado esta en el conjunto y agregarlo, por ejemplo con push, de lo
contrario.
Eliminar un elemento de un array, en eliminar, es menos sencillo,
pero puedes usar filter para crear un nuevo array sin el valor. No
te olvides de sobrescribir la propiedad que sostiene los miembros del
521
conjunto con la versión recién filtrada del array.
El método desde puede usar un bucle for/of para obtener los valores
de el objeto iterable y llamar a añadir para ponerlos en un conjunto
recien creado.
Conjuntos Iterables
Probablemente valga la pena definir una nueva clase IteradorConjunto.
Las instancias de Iterador deberian tener una propiedad que rastree la
posición actual en el conjunto. Cada vez que se invoque a next, este
comprueba si está hecho, y si no, se mueve más allá del valor actual y
lo retorna.
La clase Conjunto recibe un método llamado por Symbol.iterator que,
cuando se llama, retorna una nueva instancia de la clase de iterador para
ese grupo.
522
Proyecto: Un Robot
Midiendo un robot
Tendrás que escribir una variante de la función correrRobot que, en
lugar de registrar los eventos en la consola, retorne el número de pasos
que le tomó al robot completar la tarea.
Tu función de medición puede, en un ciclo, generar nuevos estados y
contar los pasos que lleva cada uno de los robots. Cuando has generado
suficientes mediciones, puedes usar console.log para mostrar el prome-
dio de cada robot, que es la cantidad total de pasos tomados dividido
por el número de mediciones
Conjunto persistente
La forma más conveniente de representar el conjunto de valores de
miembro sigue siendo un array, ya que son fáciles de copiar.
523
Cuando se agrega un valor al grupo, puedes crear un nuevo grupo con
una copia del array original que tiene el valor agregado (por ejemplo,
usando concat). Cuando se borra un valor, lo filtra afuera del array.
El constructor de la clase puede tomar un array como argumento,
y almacenarlo como la (única) propiedad de la instancia. Este array
nunca es actualizado.
Para agregar una propiedad (vacio) a un constructor que no sea un
método, tienes que agregarlo al constructor después de la definición de
la clase, como una propiedad regular.
Solo necesita una instancia vacio porque todos los conjuntos vacíos
son iguales y las instancias de la clase no cambian. Puedes crear muchos
conjuntos diferentes de ese único conjunto vacío sin afectarlo.
Bugs y Errores
Reintentar
La llamada a multiplicacionPrimitiva definitivamente debería suceder
en un bloquear try. El bloque catch correspondiente debe volver a lan-
zar la excepción cuando no esta no sea una instancia de FalloUnidadMultipli
y asegurar que la llamada sea reintentada cuando lo es.
Para reintentar, puedes usar un ciclo que solo se rompa cuando la
llamada tenga éxito, como en el ejemplo de mirar anteriormente en este
capítulo—o usar recursión y esperar que no obtengas una cadena de
fallas tan largas que desborde la pila (lo cual es una apuesta bastante
segura).
524
La caja bloqueada
Este ejercicio requiere de un bloque finally. Tu función deberia primero
desbloquear la caja y luego llamar a la función argumento desde dentro
de cuerpo try. El bloque finally después de el debería bloquear la caja
nuevamente.
Para asegurarte de que no bloqueemos la caja cuando no estaba ya
bloqueada, comprueba su bloqueo al comienzo de la función y desblo-
quea y bloquea solo cuando la caja comenzó bloqueada.
Expresiones Regulares
Estilo entre comillas
La solución más obvia es solo reemplazar las citas con una palabra no
personaje en al menos un lado. Algo como /\W'|'\W/. Pero también
debes tener en cuenta el inicio y el final de la línea.
Además, debes asegurarte de que el reemplazo también incluya los
caracteres que coincidieron con el patrón \W para que estos no sean
dejados. Esto se puede hacer envolviéndolos en paréntesis e incluyendo
sus grupos en la cadena de reemplazo ($1,$2). Los grupos que no están
emparejados serán reemplazados por nada.
525
La parte más complicada del ejercicio es el problema hacer coincidir
ambos "5." y ".5" sin tambien coincidir coincidir con ".". Para esto,
una buena solución es usar el operador | para separar los dos casos—ya
sea uno o más dígitos seguidos opcionalmente por un punto y cero o
más dígitos o un punto seguido de uno o más dígitos.
Finalmente, para hacer que la e pueda ser mayuscula o minuscula,
agrega una opción i a la expresión regular o usa [eE].
Módulos
Un robot modular
Aqui esta lo que habría hecho (pero, una vez más, no hay una sola
forma correcta de diseñar un módulo dado):
El código usado para construir el camino de grafo vive en el módulo
grafo. Ya que prefiero usar dijkstrajs de NPM en lugar de nuestro
propio código de busqueda de rutas, haremos que este construya el tipo
de datos de grafos que dijkstajs espera. Este módulo exporta una sola
función, construirGrafo. Haria que construirGrafo acepte un array de
arrays de dos elementos, en lugar de strings que contengan guiones, para
hacer que el módulo sea menos dependiente del formato de entrada.
El módulo caminos contiene los datos en bruto del camino (el array
caminos) y la vinculación grafoCamino. Este módulo depende de ./grafo
y exporta el grafo del camino.
La clase EstadoPueblo vive en el módulo estado. Depende del módulo
./caminos, porque necesita poder verificar que un camino dado existe.
También necesita eleccionAleatoria. Dado que eso es una función de
526
tres líneas, podríamos simplemente ponerla en el módulo estado como
una función auxiliar interna. Pero robotAleatorio también la necesita.
Entonces tendriamos que duplicarla o ponerla en su propio módulo.
Dado que esta función existe en NPM en el paquete random-item, una
buena solución es hacer que ambos módulos dependan de el. Podemos
agregar la función correrRobot a este módulo también, ya que es pe-
queña y estrechamente relacionada con la gestión de estado. El módulo
exporta tanto la clase EstadoPueblo como la función correrRobot.
Finalmente, los robots, junto con los valores de los que dependen,
como mailRoute, podrían ir en un módulo robots-ejemplo, que depende
de ./caminos y exporta las funciones de robot. Para que sea posible que
el robotOrientadoAMetas haga busqueda de rutas, este módulo también
depende de dijkstrajs.
Al descargar algo de trabajo a los módulos de NPM, el código se
volvió un poco mas pequeño. Cada módulo individual hace algo bas-
tante simple, y puede ser leído por sí mismo. La división del código
en módulos también sugiere a menudo otras mejoras para el diseño del
programa. En este caso, parece un poco extraño que EstadoPueblo y los
robots dependan de un grafo de caminos. Podría ser una mejor idea
hacer del grafo un argumento para el constructor del estado y hacer que
los robots lo lean del objeto estado—esto reduce las dependencias (lo
que siempre es bueno) y hace posible ejecutar simulaciones en diferentes
mapas (lo cual es aún mejor).
Es una buena idea usar módulos de NPM para cosas que podríamos
haber escrito nosotros mismos? En principio, sí—para cosas no triviales
como la función de busqueda de rutas es probable que cometas errores
527
y pierdas el tiempo escribiendola tú mismo. Para pequeñas funciones
como eleccionAleatoria, escribirla por ti mismo es lo suficiente fácil.
Pero agregarlas donde las necesites tiende a desordenar tus módulos.
Sin embargo, tampoco debes subestimar el trabajo involucrado en
encontrar un paquete apropiado de NPM. E incluso si encuentras uno,
este podría no funcionar bien o faltarle alguna característica que nece-
sitas. Ademas de eso, depender de los paquetes de NPM, significa que
debes asegurarte de que están instalados, tienes que distribuirlos con
tu programa, y podrías tener que actualizarlos periódicamente.
Entonces, de nuevo, esta es una solución con compromisos, y tu
puedes decidir de una u otra manera dependiendo sobre cuánto te ayu-
den los paquetes.
Módulo de Caminos
Como este es un módulo CommonJS, debes usar require para im-
portar el módulo grafo. Eso fue descrito como exportar una función
construirGrafo, que puedes sacar de su objeto de interfaz con una
declaración const de desestructuración.
Para exportar grafoCamino, agrega una propiedad al objeto exports.
Ya que construirGrafo toma una estructura de datos que no empareja
precisamente caminos, la división de los strings de los caminis debe
ocurrir en tu módulo.
528
Dependencias circulares
El truco es que require agrega módulos a su caché antes de comenzar
a cargar el módulo. De esa forma, si se realiza una llamada require
mientras está ejecutando el intento de cargarlo, ya es conocido y la
interfaz actual sera retornada, en lugar de comenzar a cargar el módulo
una vez más (lo que eventualmente desbordaría la pila).
Si un módulo sobrescribe su valor module.exports, cualquier otro mó-
dulo que haya recibido su valor de interfaz antes de que termine de
cargarse ha conseguido el objeto de interfaz predeterminado (que es
probable que este vacío), en lugar del valor de interfaz previsto.
Programación Asincrónica
Siguiendo el bisturí
Esto se puede realizar con un solo ciclo que busca a través de los nidos,
avanzando hacia el siguiente cuando encuentre un valor que no coincida
con el nombre del nido actual, y retornando el nombre cuando esta
encuentra un valor que coincida. En la función async, un ciclo regular
for o while puede ser utilizado.
Para hacer lo mismo con una función simple, tendrás que construir
tu ciclo usando una función recursiva. La manera más fácil de hacer
esto es hacer que esa función retorne una promesa al llamar a then en la
promesa que recupera el valor de almacenamiento. Dependiendo de si
ese valor coincide con el nombre del nido actual, el controlador devuelve
ese valor o una promesa adicional creada llamando a la función de ciclo
529
nuevamente.
No olvides iniciar el ciclo llamando a la función recursiva una vez
desde la función principal.
En la función async, las promesas rechazadas se convierten en excep-
ciones por await Cuando una función async arroja una excepción, su
promesa es rechazada. Entonces eso funciona.
Si implementaste la función no-async como se describe anteriormente,
la forma en que then funciona también provoca automáticamente que
una falla termine en la promesa devuelta. Si una solicitud falla, el
manejador pasado a then no se llama, y la promesa que devuelve se
rechaza con la misma razón.
Construyendo Promise.all
La función pasada al constructor Promise tendrá que llamar then en
cada una de las promesas del array dado. Cuando una de ellas tenga
éxito, dos cosas deben suceder. El valor resultante debe ser almacenado
en la posición correcta de un array de resultados, y debemos verificar
si esta fue la última promesa pendiente y terminar nuestra promesa si
asi fue.
Esto último se puede hacer con un contador que se inicializa con la
longitud del array de entrada y del que restamos 1 cada vez que una
promesa tenga éxito. Cuando llega a 0, hemos terminado. Asegúrate
de tener en cuenta la situación en la que el array de entrada este vacío
(y por lo tanto ninguna promesa nunca se resolverá).
El manejo de la falla requiere pensar un poco, pero resulta ser ex-
tremadamente sencillo. Solo pasa la función reject de la promesa de
530
envoltura a cada una de las promesas en el array como manejador catch
o como segundo argumento a then para que una falla en una de ellos
desencadene el rechazo de la promesa de envoltura completa.
Closure
Again, we are riding along on a JavaScript mechanism to get the equiv-
alent feature in Egg. Special forms are passed the local scope in which
they are evaluated so that they can evaluate their subforms in that
scope. The function returned by fun has access to the scope argument
given to its enclosing function and uses that to create the function’s
local scope when it is called.
This means that the prototype of the local scope will be the scope
in which the function was created, which makes it possible to access
bindings in that scope from the function. This is all there is to imple-
menting closure (though to compile it in a way that is actually efficient,
you’d need to do some more work).
531
Comments
Make sure your solution handles multiple comments in a row, with
potentially whitespace between or after them.
A regular expression is probably the easiest way to solve this. Write
something that matches “whitespace or a comment, zero or more times”.
Use the exec or match method and look at the length of the first element
in the returned array (the whole match) to find out how many characters
to slice off.
Fixing scope
You will have to loop through one scope at a time, using Object.
getPrototypeOf to go the next outer scope. For each scope, use hasOwnProper
to find out whether the binding, indicated by the name property of the
first argument to set, exists in that scope. If it does, set it to the result
of evaluating the second argument to set and then return that value.
If the outermost scope is reached (Object.getPrototypeOf returns null)
and we haven’t found the binding yet, it doesn’t exist, and an error
should be thrown.
532
Querrás recorrer los nombres de las llaves una vez para llenar la
fila superior y luego nuevamente para cada objeto en el arreglo para
construir las filas de datos. Para obtener un arreglo con los nombres de
las llaves proveniente del primer objeto, la función Object.keys será de
utilidad.
Para agregar la tabla al nodo padre correcto, puedes utilizar document
.getElementById o document.querySelector para encontrar el nodo con el
atributo id adecuado.
533
El sombrero del gato
Las funciones Math.cos y Math.sin miden los ángulos en radianes, donde
un círculo completo es 2π . Para un ángulo dado, puedes obtener el
ángulo inverso agregando la mitad de esto, que es Math.PI. Esto puede
ser útil para colocar el sombrero en el lado opuesto de la órbita.
Manejo de Eventos
Globo
Querrás registrar un manejador para el evento "keydown" y mirar event
.key para averiguar si se presionó la teclas de flecha hacia arra o hacia
abajo.
El tamaño actual se puede mantener en una vinculación para que
puedas basar el nuevo tamaño en él. Será útil definir una función que
actualice el tamaño, tanto el enlace como el estilo del globo en el DOM,
para que pueda llamar desde su manejador de eventos, y posiblemente
también una vez al comenzar, para establecer el tamaño inicial.
Puedes cambiar el globo a una explosión reemplazando el texto del
nodo con otro (usando replaceChild) o estableciendo la propiedad textConte
de su no padre a una nueva cadena.
Mouse trail
La creación de los elementos se realiza de mjor manera con un ciclo.
Añadelos al documento para que aparezcan. Para poder acceder a ellos
534
más tarde para cambiar su posición, querrás alamacenar los elementos
en una matriz.
Se puede hacer un ciclo a través de ellos manteniendo una variable
contador y agregando 1 cada vez que se activa el evento "mousemove". El
operador restante (% elements.length) pueden ser usado para obtener
un índice de matriz válido para elegir el elemento que se desaea colocar
durante un evento determinado.
Otro efecto interesante se puede lograr modelando un sistema simple
físico. Utiliza el evento "mousemove" para actualizar un par de enlaces
que rastrean la posición del mouse. Luego usa requestAnimationFrame
para simular que los elementos finales son atraidos a la posición a
la posición del puntero del mouse. En cada paso de la animación,
actualiza la posición en función de su posición relativa al puntero (y,
opcionalmente, una velocidad que se alamacena para cada elemento).
Averiguar una buena forma de hacerlo depende de ti.
Pestañas
Un error con el que puedes encontrarte es que no puede usar directa-
mente la propiedad childNodes del nodo como una colección de nodos de
pestañas. Por un lado, cuando agregas los botones, también se conver-
tirán en nodos secundarios y terminarán en este objeto porque es una
estructura de datos en vivo. Por otro lado, los nodos de texto creados
para el espacio en blanco entre los nodos también están en childNodes
pero no deberían tener sus propias pestañas. Puedes usar children en
lugar dechildNodes para ignorar los nodos con texto.
Puedes comenzar creando una colección de pestañas para que tengas
535
fácil acceso a ellas. Para implementar el estilo de los botones, puedes
almacenar objetos que contengan tanto el panel de pestañas como su
botón.
Yo recomiendo escribir una función aparte para cambiar las pestañas.
Puedes almacenar la pestaña seleccionada anteriormente y cambiar solo
los estilos necesarios para ocultarla y mostrar la nueva, o simplemente
puedes actualizar el estilo de todas las pestañas cada vez que se selec-
ciona una nueva pestaña.
Es posible que desees llamar a esta función inmediatamente para que
la interfaz comience con la primera pestaña visible.
536
You can add a property to the object returned by trackKeys, contain-
ing either that function value or a method that handles the unregistering
directly.
A monster
If you want to implement a type of motion that is stateful, such as
bouncing, make sure you store the necessary state in the actor object—
include it as constructor argument and add it as a property.
Remember that update returns a new object, rather than changing
the old one.
When handling collision, find the player in state.actors and compare
its position to the monster’s position. To get the bottom of the player,
you have to add its vertical size to its vertical position. The creation of
an updated state will resemble either Coin’s collide method (removing
the actor) or Lava’s (changing the status to "lost"), depending on the
player position.
Node.js
Search tool
Your first command line argument, the regular expression, can be found
in process.argv[2]. The input files come after that. You can use the
RegExp constructor to go from a string to a regular expression object.
Doing this synchronously, with readFileSync, is more straightforward,
but if you use fs.promises again to get promise-returning functions and
537
write an async function, the code looks similar.
To figure out whether something is a directory, you can again use
stat (or statSync) and the stats object’s isDirectory method.
Exploring a directory is a branching process. You can do it either
by using a recursive function or by keeping an array of work (files that
still need to be explored). To find the files in a directory, you can call
readdir or readdirSync. The strange capitalization—Node’s file system
function naming is loosely based on standard Unix functions, such as
readdir, that are all lowercase, but then it adds Sync with a capital
letter.
To go from a filename read with readdir to a full path name, you have
to combine it with the name of the directory, putting a slash character
(/) between them.
Directory creation
You can use the function that implements the DELETE method as a
blueprint for the MKCOL method. When no file is found, try to cre-
ate a directory with mkdir. When a directory exists at that path, you
can return a 204 response so that directory creation requests are idem-
potent. If a nondirectory file exists here, return an error code. Code
400 (“bad request”) would be appropriate.
538
content of the file. You can use relative URLs like index.html, instead
of http://localhost:8000/index.html, to refer to files on the same server
as the running script.
Then, when the user clicks a button (you can use a <form> element
and "submit" event), make a PUT request to the same URL, with the
content of the <textarea> as request body, to save the file.
You can then add a <select> element that contains all the files in the
server’s top directory by adding <option> elements containing the lines
returned by a GET request to the URL /. When the user selects another
file (a "change" event on the field), the script must fetch and display
that file. When saving a file, use the currently selected filename.
539
JSON as your file format, you’ll have to copy the properties of the
object returned by JSON.parse into a new, prototype-less object.
540
Index
541
204 (HTTP status code), 476, 477 addEventListener method, 387,
304 (HTTP status code), 488, 499, 388, 445, 468
509 addition, 20, 189
400 (HTTP status code), 538 adoption, 235
403 (HTTP status code), 473 advertencia, 459
404 (HTTP status code), 474, 494, ages example, 172
498 aislamiento, 279
405 (HTTP status code), 471 alcace, 67
500 (HTTP status code), 471 alcance, 68, 280, 281, 284, 285
alcance global, 279, 408, 409
a (HTML tag), 369, 372 alcance léxico, 70
Abelson, Hal, 332 alphanumeric character, 238
absolute positioning, 378, 385, 397, alt attribute, 365
404, 414 alt key, 395
absolute value, 126 altKey property, 395
abstraccion, 359 ambiguity, 353
abstraccione, 136 American English, 240
abstracción, 332 analysis, 210, 218
abstract data type, 159 ancestor element, 434
abstract syntax tree, see syntax Android, 396
tree animaciones, 418
abstraction, 64, 136, 138, 140 animación, 403
acceleration, 444 animation, 380, 414, 426
access control, 160, 234, 490 platform game, 437, 438, 444,
actor, 423, 432, 442 446, 447, 536
add method, 189 spinning cat, 379, 385
addEntry function, 107
542
antirrebote, 410 535
anyStorage function, 327, 330 iteration, 112, 141
apio, 482 length of, 98
aplicación, 1, 484 methods, 116, 131, 141, 144–
appendChild method, 363, 532 146, 151, 156, 157
application (of functions), see func- representation, 128
tion application searching, 112, 117
aprender, 11 traversal, 138
aprendizaje, 12, 482 Array prototype, 164, 171
archivo, 275, 288, 456 array-like object, 359, 362
archivos, 284 Array.from function, 321, 362, 457
argument, 75, 254 arrays, 150
argumento, 42, 83, 122, 333 arrays in egg (exercise), 351, 531
arguments object, 516 arroba, 419
argv property, 455 arrow function, 72, 162
arithmetic, 20, 31, 345 arrow key, 413
arrastrar, 399 artificial intelligence, 192, 350
array, 98, 100, 103, 123, 128, 132, artificial life, 417
138, 154, 157, 178, 201, asignación, 263
232, 242, 291, 351, 521, assert function, 231
523 assertion, 231
as matrix, 421 assignment, 38, 55, 266, 353, 532
as table, 110 assumption, 232
creation, 96, 516 asterisk, 20, 240
filtering, 144 async function, 321–323, 328, 330,
indexing, 96, 112, 118, 516, 508
543
asynchronous programming, 296, barra de progreso, 403
297, 303, 304, 321, 325, bean counting (exercise), 92, 516
327, 449 beforeunload event, 406
in Node.js, 453, 463, 468, 475, behavior, 350
480 benchmark, 371
atributo, 368 best practices, 4
attribute, 368 biblioteca, 458
attributo, 359 big ball of mud, 275
automatic semicolon insertion, 36 binary data, 5, 16
automation, 207, 215 binary number, 16, 18, 217, 250
autómata, 192 binary operator, 20, 26
avatar, 417 binding, 344, 345, 349, 353
average function, 149 assignment, 38, 70
await keyword, 321, 322, 324, 328 definition, 37, 353, 532
axis, 443 from parameter, 66, 79
global, 67, 212, 450, 455
Babbage, Charles, 94 local, 67
background, 430, 439 model of, 38, 105
background (CSS), 414, 418, 432 naming, 40, 57, 85, 124, 213
backslash character, 23, 236, 239, scope of, 67
258, 259, 525 visibility, 68
as path separator, 473 bit, 6, 16, 18, 26
backtick, 22, 25 bitfield, 400
backtracking, 250, 251, 257 bits, 110
balloon (exercise), 413, 534 block, 65, 223, 226, 333
banking example, 224 block comment, 59, 256
Banks, Ian, 416
544
block element, 369, 372 broadcastConnections function, 316
blocking, 297, 380, 410, 464 browser, 10, 388
bloque, 46, 67, 72, 102, 373 window, 387
bloques, 51 bubbling, see event propagation
blur event, 405, 406 Buffer class, 462, 463, 468, 469
body (HTML tag), 356 bug, 210, 261, 271, 276
body (HTTP), 465, 476, 478, 495 building Promise.all (exercise), 331,
body property, 356, 357, 362 530
Book of Programming, 16, 274, button, 386
452 button (HTML tag), 388, 396,
Boolean, 26, 45, 49, 104, 343, 415
345 button property, 389, 400
conversion to, 32, 33, 44, 50 buttons property, 400
Boolean function, 44
Booleano, 237 cache, 284
border (CSS), 369, 373 caché, 301
border-radius (CSS), 397 caja, 234, 354, 417, 418
botón del mouse, 390, 391, 397 caja de arena, 94, 354
bouncing, 420, 425, 438, 442 call method, 162, 171
boundary, 247, 266, 272, 525 call stack, 73, 78, 84, 222, 224,
box shadow (CSS), 434 227
braces, see curly braces callback function, 297, 306, 309,
branching, 247, 251 312, 387, 446, 447, 461,
branching recursion, 82 463, 467, 468, 499, 507
break keyword, 53, 57 calling (of functions), see func-
British English, 240 tion application
camel case, 57, 374
545
campo, 396 catch keyword, 222, 223, 228, 229,
campo de texto, 405 233, 325, 524
cancelAnimationFrame function, catch method, 307
409 CD, 16
canvas, 418 Celsius, 183
capas, 319 center, 436
capitalization, 57, 167, 242, 374, centering, 380
385, 469 change event, 539
capture group, 243, 246, 254, 492 character, 22, 24, 153
capítulo de proyecto, 482 character category, 268
caracteres chinos, 152 character encoding, 463
caracteres de nueva línea, 421 characterCount function, 147
caret character, 239, 247, 266, characterScript function, 156, 158,
460 520
carrera, 416 charCodeAt method, 152
carriage return, 265 chess board (exercise), 62, 514
carácter, 152 chicks function, 327, 329
carácter de tubería, 248 child node, 360, 363
carácteres de punto, 43 childNodes property, 360, 362,
carácteres de tabulación, 51 366, 535
Cascading Style Sheets, see CSS children property, 361
case conversion, 99 Chinese characters, 155
case keyword, 57 ciclo, 48, 61, 138, 150, 310, 357,
case sensitivity, 242, 526 514, 516
casual computing, 2 ciclo infinito, 54, 517
cat’s hat (exercise), 385 cierre, 79
546
circo, 116 codePointAt method, 153
circular dependency, 294, 529 codiciosos, 257
clase, 165, 168, 189, 195, 420 codificación de caracteres, 462
class attribute, 363, 369, 376, 430, coercion de tipo, 31
433, 434 coin, 417, 443
class declaration Coin class, 426, 443
properties, 169 colección, 9, 96
class hierarchy, 186 collection, 100, 103, 132
className property, 369 collision detection, 438, 442, 443,
cleaning up, 224 445, 537
clearInterval function, 409 colon character, 29, 56, 101, 373
clearTimeout function, 409, 410 color (CSS), 373, 374
click event, 387, 388, 391, 397, comentario, 58, 256, 358, 483
401 comillas, 273
client, 466, 501, 502 comma character, 333
cliente, 483 command key, 395
clientHeight property, 369 command line, 277, 452, 455, 479
clientWidth property, 369 comment, 128, 263, 352, 488, 497,
clientX property, 397, 401 505, 532
clientY property, 397, 401 comment field reset (exercise), 511,
closure, 352, 531, 533, 536 540
closure in egg (exercise), 352, 531 COMMENT_NODE code, 358
code comments in egg (exercise), 352,
structure of, 35 532
code golf, 272 CommonJS, 456, 457
code structure, 51, 64, 274, 289 CommonJS modules, 281, 283
547
compareRobots function, 207 conexión, 485, 486
comparison, 26, 32, 49, 57, 133, configuración, 263
345, 516 conjunto, 237, 357
of NaN, 28 conjunto de datos, 111, 142
of numbers, 27, 43 connected graph, 206
of objects, 106 connections binding, 316
of strings, 27 consistencia, 58
of undefined values, 31 consistency, 359
compartir habilidades, 482 consola de JavaScript, 43, 454
compatibilidad, 10 console.log, 9, 15, 26, 42, 74, 77,
compatibility, 460 90, 218, 454, 470
compilation, 287, 349, 531 const keyword, 40, 67, 106, 124,
complejidad, 4, 135 127
complexity, 5, 186, 252, 377, 428 constant, 445
comportamiento, 271 constante, 40
composability, 9, 149, 290 constantes, 124
computadora, 1, 4 constructor, 58, 167, 187, 209,
computed property, 98 224, 244, 258, 521, 524
concat method, 118, 157, 523, constructora, 166, 212
533 constructoras, 168
concatenation, 24, 118, 533 Content-Type header, 465, 472,
conditional execution, 29, 56, 61, 473, 480
342 continuación, 300
conditional operator, 29, 34, 342 continue keyword, 54
conditional request, 489 control flow, 44, 45, 48, 50, 52,
conexiones, 485 74, 222, 297, 322
548
control key, 395 createWriteStream function, 468,
convención, 58 477
coordenadas, 397 crow-tech module, 302
coordinates, 189, 381, 431, 436, crying, 242
439 CSS, 373, 375, 377, 429, 431–
copy-paste programming, 87, 276 434, 437, 501
copyright, 278 ctrlKey property, 395
corchete, 177 cuadrado, 44
corchetes, 96, 97, 127, 239 cuadro de diálogo, 41
corredores de pruebas, 216 cuadrícula, 417, 418
correlaciones, 114 cuervo, 298, 300, 319
correlación, 115 curl program, 478
correlaciónes, 112 curly braces, 8, 65, 72, 101, 107,
correlation, 108, 109 127, 139, 241
cosine, 124, 381 cutting point, 429
countBy function, 154, 158 cwd function, 473
counter variable, 48, 52, 382, 514, código, 12, 256, 417
516, 530 córvidos, 298
CPU, 297
crash, 228, 232, 496, 511 Dark Blue (game), 417
createElement method, 366, 532 dash character, 21, 238
createReadStream function, 468, data, 3, 16
475 data attribute, 368, 415
createServer function, 464, 465, data event, 468
468, 490, 492 data format, 128
createTextNode method, 365 data loss, 511
data structure, 94, 96, 132, 172,
549
198 definirTipoSolicitud function, 302,
tree, 357 311
Date class, 244, 245, 279, 281 DELETE method, 470, 475, 495
date-names package, 281 delete method, 189
Date.now function, 245 delete operator, 102
datos, 94 dependencia, 108
datos binarios, 462 dependency, 275, 276, 279, 285,
dblclick event, 397 294, 459, 460
debugger statement, 219 depuración, 211
debugging, 10, 210, 213, 217, 218, desbordar, 19
224, 229, 231, 271 descarga, 483
decimal number, 16, 217, 250 descargar, 458
declaración, 36, 37, 44, 49, 65, desenrollando la pila, 222
102 deserialization, 129
declaración de clase, 168 design, 276
declaraciónes, 53 destructuring, 246
decodeURIComponent function, destructuring binding, 126, 282,
472, 492 528
deep comparison, 106, 133 developer tools, 13, 42, 227
deep comparison (exercise), 133, diagrama de flujo, 248
519 dialecto, 287
default behavior, 372, 393 diario, 107
default export, 286 Dijkstra’s algorithm, 291
default keyword, 57 Dijkstra, Edsger, 192, 291
default value, 33, 76 dijkstrajs package, 291, 526
defineProperty function, 521 dimensiones, 514
550
dimensions, 189, 369, 416, 419, attributes, 368
438 construction, 359, 363, 366
dinosaur, 350 events, 388, 396
dirección, 128 graphics, 418, 429, 432–434
direct child node, 377 interface, 358
direction (writing), 158 modification, 363
directorio, 456, 461, 463 querying, 362, 377
directory, 456, 461, 470, 473–475, tree, 357
479, 480, 539 domain-specific language, 136, 216,
directory creation (exercise), 480, 235, 351, 377
538 DOMDisplay class, 429, 431
disco duro, 295 dominant direction (exercise), 158,
discretization, 417, 438, 447 520
dispatch, 490 dot character, see period charac-
dispatching, 56 ter
display, 429, 447, 449 double click, 397
display (CSS), 373, 415 double-quote character, 22, 273,
division, 21 333
division by zero, 22 download, 13, 277, 477, 511
do loop, 50, 202 draggable bar example, 399
Document Object Model, see DOM drawing, 354, 369, 371, 380, 429
documentElement property, 356, drawing program example, 397
357 duplication, 276
documento, 354, 406 dígito, 217, 238, 240, 243
dollar sign, 40, 247, 254, 266 dígitos, 241
DOM, 356, 368
ECMAScript, 10, 11, 285
551
ECMAScript 6, 11 encapsulación, 160, 186
ecstatic package, 492 encapsulation, 159, 173, 388, 428,
editores, 52 429
efecto secundario, 36, 43, 66, 364 encodeURIComponent function,
efficiency, 81, 131, 150, 315, 349, 487, 504
371, 433 end event, 468
eficiencia, 418 end method, 465, 467, 468, 472
Egg language, 332, 339, 340, 342, enemies example, 263
345, 347, 350–352, 357 enlace, 360, 362, 393, 396
ejecución condicional, 45 ENOENT (status code), 475
ejemplo de la granja, 89 entorno, 41
ejercicios, 3, 12, 217 entrada, 418
elección, 248 enum (reserved word), 40
electronic life, 417 environment, 342
elegance, 336 equality, 27
elegancia, 81 error, 152, 211–213, 217, 220, 221,
element, 358, 361 227, 230, 306, 308, 319
ELEMENT_NODE code, 358, 533 error de sintaxis, 41
elemento, 357, 366 error event, 477
elipse, 379 error handling, 210, 221, 222, 228,
ellipse, 382 463, 471, 475, 504, 508
else keyword, 46 error message, 340
elt function, 366, 505 error recovery, 220
emoji, 24, 152, 268, 413 error response, 471, 476
empaquetadores, 288 Error type, 224, 228, 230, 475
empty set, 256 errores, 258
552
ES modules, 285 449, 450, 468, 536
escape key, 450 event loop, 325
escaping event object, 397
in regexps, 236, 239, 259 event propagation, 390, 392, 405,
in strings, 23, 333 406
in URLs, 472, 487, 492 event type, 390
escribiendo, 411 every method, 157
espacio en blanco, 333 everything (exercise), 157, 520
espacios en blanco, 337 everywhere function, 315
especificidad, 376 evolución convergente, 299
espías, 403 evolution, 235, 460
estado, 37, 195, 196, 315, 327 exception handling, 223, 224, 227–
estructura, 354 230, 233, 234, 306, 308,
estructura de datos, 100, 292, 334, 322, 325, 331, 530
335, 354 exception safety, 227
estándar, 10, 41, 57, 144, 267, exec method, 242–244, 260, 262
455 execution order, 44, 71, 74
ETag header, 488, 498, 509 exercises, 60
etiqueta, 354, 376 exit method, 455
eval, 280 expectation, 393
evaluación de corto circuito, 34 experiment, 5
evaluate function, 340, 342, 345 experimentar, 13, 271
evaluation, 280, 340, 349 exponent, 19, 273, 526
even number, 92 exponente, 525
event handling, 387–390, 393, 394, exponentiation, 49, 53
403, 405, 407, 418, 445, export keyword, 286
553
exports object, 281, 284, 285, 457, resource, 470, 473, 474
528, 529 stream, 468
exposiciones, 483 file extension, 474
expresion, 70 file format, 263
expresiones regular, 337 file server, 501
expresiones regulare, 235 file server example, 470, 473, 475–
expresiones regulares, 258, 263 477, 480, 538
expresión, 35, 36, 38, 44, 49, 53, file size, 288
333, 334 file system, 462, 470, 473, 539
expresión regular, 237, 272 filter method, 144, 149, 156, 197,
expression, 341 313, 520, 521, 523
expressivity, 351 finally keyword, 226, 234, 525
extension, 456 findIndex method, 155
extiende, 123 findInStorage function, 319, 321
extraction, 244 findRoute function, 205, 317
finish event, 477
factorial function, 15 firstChild property, 360
Fahrenheit, 183 fixed positioning, 404
fallthrough, 57 fixing scope (exercise), 353, 532
false, 26 FizzBuzz (exercise), 61, 514
farm example, 85, 247 flattening (exercise), 157
fecha, 238, 241, 244 flexibility, 11
fetch function, 467, 503, 507, 538 floating-point number, 19, 20
field, 511 flooding, 315, 316
fila, 384 flow diagram, 250
file, 475, 539 flujo de control, 141, 224
access, 461, 462
554
foco, 396, 405 función interior, 533
focus, 512 function, 41, 64, 72, 211, 347
focus event, 405, 406 application, 41–43, 66, 73, 75,
fold, see reduce method 80, 81, 144, 228, 333, 341
fondo, 417 as property, 99
font-family (CSS), 374 as value, 65, 70, 78, 138, 140,
font-size (CSS), 413 144, 389, 446, 536
font-weight (CSS), 375 body, 65, 72
for loop, 52, 54, 112, 138, 157, callback, see callback function
229, 516, 518 declaration, 71
for/of loop, 113, 153, 175, 177, definition, 65, 71, 85
181, 522 higher-order, 70, 138, 140, 144–
forEach method, 141 146, 149, 254, 446
form, 481 model of, 79
form (HTML tag), 506, 539 naming, 85, 87
formatDate module, 281, 286 purity, 89
formato de datos, 359 scope, 69, 279, 352
fractional number, 19, 273, 417 function application, 122
framework, 89 Function constructor, 280, 284,
fs package, 461, 463, 464 345, 349
funcion, 332 function keyword, 65, 71
funciones de flecha, 328 Function prototype, 164, 171
función, 41, 334 futuras, 40
función de devolución de llamada, future, 11, 71
300, 303 físico, 535
función de predicado, 156
game, 445, 449, 450
555
screenshot, 436 getTime method, 245
GAME_LEVELS data set, 449 getYear method, 245
garbage collection, 17 global object, 212
garble example, 456 global scope, 67, 345, 455, 456,
generador, 323 532
GET method, 467, 470, 473, 487, globo, 413
494 goalOrientedRobot function, 207
get method, 173 gossip property, 315
getAttribute method, 368, 369 grafo, 193, 204, 291, 317
getBoundingClientRect method, grammar, 35, 264
370, 371 gramática, 211
getDate function, 246 gran bola de barro, 274
getDate method, 245 graphics, 418, 429, 433
getElementById method, 363, 533 grave accent, see backtick
getElementsByClassName method, gravity, 444, 445
363 greater than, 27
getElementsByTagName method, greed, 256, 257
362, 366, 385, 533 grep, 479
getFullYear method, 245 grid, 431, 439
getHours method, 245 Group class, 189, 190, 208, 324,
getMinutes method, 245 521, 522
getMonth method, 245 groupBy function, 158
getPrototypeOf function, 164, 167, grouping, 20, 46, 241, 243, 254,
353, 532 525
getSeconds method, 245 groups (exercise), 189, 190, 521,
getter, 182, 189, 425 522
556
grupo de usuarios, 482 hooligan, 490
gráficos, 418 hora, 238, 241, 244
href attribute, 362, 368
h1 (HTML tag), 369 HTML, 354, 480
handleAction function, 502 structure, 354, 357
hard disk, 290, 300 html (HTML tag), 356
hard drive, 17, 511 HTTP, 465, 476, 480, 485, 487
hard-coding, 362 client, 466, 478
has method, 173, 189 cliente, 483
hash character, 352 server, 464, 470, 500
hasOwnProperty method, 174, 353, http package, 464, 466
532 HTTPS, 467
head (HTML tag), 356 https package, 467
head property, 356 human language, 35
header, 465, 487 hyphen character, 374
help text example, 405
herencia, 184, 186 id attribute, 363, 376
herramienta, 235 idempotence, 311, 476
herramientas, 235, 271 idempotency, 538
herramientas de desarrollador, 219 identifier, 334
hexadecimal number, 250 identity, 105
hidden element, 373, 415 if keyword, 45, 266
higher-order function, see func- chaining, 47, 56, 514, 515
tion, higher-order If-None-Match header, 488, 499,
hilo, 296, 298, 408 507
historia, 10 imagen, 365
Hières-sur-Amby, 299 imagenes, 406
557
imaginación, 416 inner loop, 253
IME, 396 innerHeight property, 404
img (HTML tag), 365, 372, 406 innerWidth property, 404
immutable, 425 input, 220, 386, 453, 496
implements (reserved word), 40 input (HTML tag), 405
import keyword, 285 insertBefore method, 363, 364
in operator, 103, 174 installation, 277
includes method, 112, 113, 521 instanceof operator, 187, 230
indefinido, 211 instancia, 166
indentación, 51 integer, 20
index property, 243 integration, 235, 359
index.html, 501 interface, 173, 182, 190, 289, 309
index.js, 456 design, 88, 235, 245, 254, 260,
indexOf method, 117, 119, 155, 358, 359, 428
190, 237, 259, 521 HTTP, 487
infinite loop, 75, 229 module, 457
infinity, 22 object, 423
infraestructura, 277 interface (reserved word), 40
inheritance, 164, 184, 187, 230, interfaces, 160, 279
475 interfaz, 175, 235, 275, 279, 281,
INI file, 263 284, 289
ini package, 277, 284, 289, 458 internationalization, 267
inicialización, 406 Internet, 263
inline element, 369, 372 interpolation, 25
inmutables, 104, 198 interpretation, 12, 280, 340, 342,
inner function, 68 349
558
inversion, 239 JOURNAL data set, 111
invoking (of functions), see func- journalEvents function, 113
tion application JSON, 128, 289, 300, 317, 487,
isDirectory method, 475, 538 509, 539
isEven (exercise), 92, 515 JSON.parse function, 129, 539
isolation, 159, 274, 280 JSON.stringify function, 129
iterable interface, 177, 522 JSX, 506
iterador, 323 juego, 416–418, 420
iterator interface, 175, 178, 190 juego de plataforma, 416
jugador, 416, 417, 419, 420
Jacques, 95 jump, 7
jardinería, 482 jump-and-run game, 416
Java, 10 jumping, 417, 444, 445
JavaScript, 10
availability of, 2 Kernighan, Brian, 210
flexibility of, 11 key code, 445
history of, 10 key property, 395, 534
puntos débiles, 10 keyboard, 394, 417, 418, 444, 445,
syntax, 35 450
uses of, 12 keydown event, 394, 410, 446, 534
versions of, 11 keyup event, 394, 446
JavaScript console, 13, 26, 42, kill process, 466
219, 227 Knuth, Donald, 64
JavaScript Object Notation, see
JSON la salida estándar, 454
join method, 156, 171, 457 landscape example, 68
journal, 95, 100, 104, 113 Laozi, 295
559
lastChild property, 360 let keyword, 37, 39, 67, 106, 124,
lastIndex property, 261, 262 127, 212
lastIndex property*, 260 level, 419, 429, 430, 434, 448,
lastIndexOf method, 117 449
latency, 288 Level class, 420
lava, 417, 418, 420, 434, 438, 442, lexical scoping, 68
443 leyes De Morgan, 520
Lava class, 425, 442 library, 359, 458
layout, 369, 373 licencia, 278
laziness, 371 limite, 143
Le Guin, Ursula K., 3 line break, 23, 265
leaf node, 357 line comment, 58, 256
leak, 450 lines of code, 347
learning, 3 link (HTML tag), 437
leerAlmacenamiento function, 301 linked list, 132, 518
left (CSS), 378, 380, 382, 385 linter, 285
legibilidad, 7 Liskov, Barbara, 159
LEGOS, 275 list (exercise), 132, 518
length property lista de trabajo, 205
for array, 98 listen method, 464, 466
for string, 86, 92, 97, 121, listening (TCP), 464
516 literal expression, 35, 236, 337,
lenguaje de programación, 332 341
lenguaje Egg, 334 live data structure, 354, 366, 378,
lenguajes de programación, 2 535
less than, 27 live view, 510, 540
560
lives (exercise), 450 magic, 163
llamada de pila, 325 mailRoute array, 203
llaves, 46, 102, 241, 514 maintenance, 278
load event, 406 manejadores, 387
local binding, 78, 353, 516 manejo de excepciones, 222
local scope, 67, 348 map, 427
localhost, 465 map (data structure), 172
localStorage object, 503 Map class, 173, 182, 321
locked box (exercise), 233, 525 map method, 145, 149, 156, 163,
logging, 218 172, 197, 313, 421
logical and, 28 Marcus Aurelius, 386
logical operators, 28 match method, 243, 262
logical or, 28 matching, 237, 246, 247, 260, 272
long polling, 485, 486, 488, 495, algorithm, 248, 250, 251
498, 499, 507 matemáticas, 140
loop, 7, 8, 52, 61, 62, 80, 112, Math object, 91, 97, 123
138, 148, 262 Math.abs function, 126
termination of, 53 Math.acos function, 124
loop body, 50, 139 Math.asin function, 124
lycanthropy, 95, 107 Math.atan function, 124
límite, 249, 250, 258 Math.ceil function, 126, 439
línea, 37, 265 Math.cos function, 124, 381, 382
línea de comandos, 454, 457 Math.floor function, 125, 201, 439
líneas, 51 Math.max function, 43, 97, 122,
123
machine code, 5, 349 Math.min function, 43, 91, 123
magia, 332
561
Math.PI constant, 124 metaKey property, 395
Math.random function, 125, 201, method, 99, 116, 161, 466
427 HTTP, 466, 478, 487, 490
Math.round function, 126 method call, 161
Math.sin function, 124, 381, 382, methods object, 470
427, 443 mime package, 473
Math.sqrt function, 111, 123, 521 MIME type, 474
Math.tan function, 124 minificadores, 288
mathematics, 80 minimalism, 417
Matrix class, 178 minimum, 43, 91, 123
matrix example, 178, 184 minimum (exercise), 91, 515
MatrixIterator class, 180 minus, 21, 273
max-height (CSS), 434 MKCOL method, 480, 538
max-width (CSS), 434 mkdir function, 480, 538
maximum, 43, 123, 147, 148 modification date, 475
measuring a robot (exercise), 207, modifier key, 395
523 modular robot (exercise), 293, 526
media type, 473 modularity, 159
meetup, 482 module, 293, 429, 491
memoria, 96, 128, 295 design, 289
memory, 16, 17, 37, 74, 105, 132, module loader, 456
349 module object, 284
persistence, 511 modulo operator, 21
menú contextual, 393 modulos CommonJS, 294
message event, 408 monedas, 420
meta key, 395 monociclismo, 482
562
monster (exercise), 451, 537 navegadore, 42, 298
mouse, 41 navegadores, 14, 288
mouse cursor, 397 negation, 26, 29
mouse trail (exercise), 414, 534 negritas, 372
mousedown event, 391, 397, 401 neighbors property, 312
mousemove event, 399, 400, 410, nerd, 259
411, 414, 535 nesting
mouseup event, 397, 400, 401 in regexps, 253
movimiento, 418 of arrays, 110
multiplication, 20, 424, 442 of expressions, 35, 336
multiplier function, 79 of functions, 68
mundo, 416 of loops, 62, 514
mundo virtual, 192 of objects, 357, 361
musical, 416 of scope, 68
mutability, 102, 104, 105, 198 Netscape, 10
método, 160, 166, 212 network, 308
módulo, 275, 456–458 speed, 453
módulo CommonJS, 294, 528 network function, 319
módulos, 279 new operator, 166
módulos CommonJS, 285 newline character, 23, 62, 238,
módulos ES, 285 256, 265
next method, 178, 323, 522
namespace, 123 nextSibling property, 360
namespace pollution, 123 nivel, 417, 420
naming, 40 node program, 454
NaN, 22, 28, 30, 211 node-fetch package, 467
navegador, 2, 41, 418, 483
563
Node.js, 12, 14, 42, 281, 298, 452– notation, 18, 19
454, 456, 458, 461, 463, precision of, 20
464, 466, 467, 470, 473, representation, 18
475–478, 483, 485, 486, 490, special values, 22
511 Number function, 44, 45, 58
node_modules directory, 456, 458 number puzzle example, 82
NodeList type, 359, 377 Number.isNaN function, 46
nodeName property, 385 número, 237, 273, 525
nodeType property, 358, 533, 535 número binario, 110
nodeValue property, 362 números pare, 48
nodo, 357
nodos de texto, 361 object, 43, 94, 103, 104, 123, 127,
nodos hijos, 357 128, 132, 163, 187, 519
nodos hoja, 357 as map, 427
nombrado, 7 creation, 166
nombre, 10 identity, 105
not a number, 22 property, 97
notación, 285 representation, 128
NPM, 277, 278, 281, 285, 287, Object prototype, 163, 164
291, 293, 456, 458, 461, object-oriented programming, 165,
474, 491, 492, 511, 527 174, 175, 184, 290
npm program, 458, 460, 474 Object.create function, 165, 172,
nueces, 115, 116 346
null, 30, 31, 83, 97, 127, 133, 220 Object.keys function, 103, 134,
number, 18, 104 321, 519, 532
conversion to, 31, 44 Object.prototype, 172
objeto, 101, 159, 160, 279
564
objeto de evento, 389 overlay, 375
objeto tipo array, 462 overriding, 169, 174, 184, 529
objeto tipo matriz, 402 overwriting, 477, 481, 496
objetos de evento, 401
obstacle, 438 p (HTML tag), 369
offsetHeight property, 369, 371 package (reserved word), 40
offsetWidth property, 369 package manager, 277
on method, 468 package.json, 459, 460
onclick attribute, 388 padding (CSS), 432
opcional, 240 page reload, 406
operador, 334 pageX property, 397, 401
operador binario, 35 pageXOffset property, 370
operador unario, 35 pageY property, 397, 401
operator, 20, 25, 26, 33, 345 pageYOffset property, 370, 404
application, 20 palabra caracter, 267
optimización, 418 palabra clave, 37, 369
optimization, 81, 90, 371, 410, palabras clave, 40
433, 464 Palef, Thomas, 417
option (HTML tag), 539 paquete, 276, 281, 456, 460
optional argument, 76, 131 parallelism, 296
ordinal package, 281, 284 parameter, 42, 66, 76, 126, 213
organic growth, 274 parametros, 284
organization, 274 parent node, 390
output, 26, 41, 42, 218, 220, 346 parentheses, 20, 248
overflow (CSS), 434 parentNode property, 360
overlap, 439 parse function, 339
parseApply function, 338
565
parseExpression function, 336 persistence, 511, 539
parseINI function, 266, 276 persistencia, 483
parsing, 129, 210, 266, 333–335, persistent data structure, 196, 198,
338, 342, 346, 354, 472, 208, 217, 537
498 persistent group (exercise), 208
parámetro, 65, 67, 72, 76, 122, persistent map (exercise), 523
162 petición, 483
parámetro restante, 122 PGroup class, 208, 523
paréntesis, 35, 42, 46, 49, 53, 72, phase, 426, 427, 443
73, 139, 241, 246, 267, 333, phi coefficient, 108–110
525 phi function, 110, 126
path physics, 437, 444
file system, 456, 470 physics engine, 438
URL, 470, 472, 487, 490 pi, 20, 124, 381, 427
path package, 473 PI constant, 124, 381
pathfinding, 204, 291, 317 pila, 100
patrones, 235, 237 pila de llamadas, 74
patrón, 236, 239, 258 ping request, 312
pattern, 237 pipe character, 525
pausing (exercise), 450, 536 pipe method, 472, 477
pea soup, 136 pipeline, 288
percentage, 404 pixel, 369, 382, 397, 419, 431
performance, 252, 288, 349, 371, pizza, 108, 110
418, 464 plantilla, 282
period character, 97, 98, see max plantillas literales, 25
example, 238, 256, 273 platform game, 450
566
Plauger, P.J., 210 preventDefault method, 393, 403,
player, 434, 438, 443, 448 404, 406, 445
Player class, 424, 443 previousSibling property, 360
plus character, 20, 240, 273 primitiveMultiply (exercise), 233,
Poignant Guide, 35 524
pointer, 360 private (reserved word), 40
pointer event, 391 private properties, 160
polling, 386 private property, 234
pollTalks function, 507 problema de búsqueda, 204
polymorphism, 174 procesador, 295
pop method, 100, 116 process object, 455, 473
Popper, Karl, 366 profiling, 81
porcentaje, 156 program, 36, 44
port, 465, 466 nature of, 3
position, 370 program size, 135, 136, 272, 428
position (CSS), 378, 385, 404, 418, programación, 1
433, 434 programación asincrónica, 300
POST method, 488 programación orientada a obje-
postMessage method, 408 tos, 159, 195
power example, 65, 77, 80 programming
precedence, 20, 29 difficulty of, 3
precedencia, 21, 375 history of, 5
predicate function, 144, 151, 157 joy of, 1, 4
Prefer header, 489, 499, 507 programming language, 5, 350,
pregunta de entrevista, 62 358, 453
premature optimization, 81 power of, 9
567
programming style, 4, 36, 51, 57, prototipo, 163–165, 168–170
428 prototipos, 165
project chapter, 192, 332, 416 prototype, 172, 184, 346, 353, 531,
promesa, 331, 530 539
Promise class, 304, 305, 307, 309, diagram, 170
310, 321, 325, 326, 331, prototype property, 166, 167
463, 467, 470, 508, 530 proyecto para compartir habili-
Promise.all function, 312, 328, 331, dades, 482, 483
530 pseudorandom number, 125
Promise.reject function, 307 public (reserved word), 40
Promise.resolve function, 304, 312 public properties, 160
promises package, 463 public space (exercise), 480, 538
promptDirection function, 228, 230 publishing, 461
promptInteger function, 220 punch card, 5
propagation, see event propaga- punto de interrupción, 219
tion punto y coma, 53, 373
property, 97, 101, 161, 163, 176, pure function, 88, 89, 131, 144,
182 290
assignment, 102 push method, 100, 113, 116, 521
deletion, 102 PUT method, 470, 476, 487, 496,
model of, 102 539
testing for, 103 Pythagoras, 521
propiedad, 43, 97, 98, 107, 166, página web, 288
169, 177, 211 páginas de inicio llamativas, 414
propiedades, 173
protected (reserved word), 40 query string, 488, 498
querySelector method, 378, 533
568
querySelectorAll method, 377 readdir function, 463, 475, 538
question mark, 29, 240 readdirSync function, 538
queue, 326 readFile function, 283, 461, 539
quotation mark, 22 readFileSync function, 464, 537
quoting reading code, 12, 192
in JSON, 128 readStream function, 495, 497
of object properties, 101 real-time, 386
quoting style (exercise), 273, 525 reasoning, 28
recipe analogy, 136
rabbit example, 161, 165, 166, record, 100
168 rectangle, 418, 438
radian, 381 recursion, 75, 80, 82, 92, 133,
radix, 16 310, 321, 336, 339, 342,
raising (exception), 222 361, 385, 515, 518, 529,
random number, 125, 427 533
random-item package, 526 recursión, 524
randomPick function, 200 red, 288, 295, 296
randomRobot function, 200 reduce method, 146, 147, 149,
range function, 130, 516 156, 157, 520
rango, 143, 238, 241 ReferenceError type, 353
rangos, 241 RegExp class, 236, 258, 537
raíz, 357 regexp golf (exercise), 272
read-eval-print loop, 454 regular expression, 237, 239, 253,
readability, 9, 58, 80, 88, 221, 256, 257, 260, 479, 491,
274, 342, 436 492, 532, 537
readable stream, 467, 468, 472, alternatives, 247
495
569
backtracking, 250 repeat method, 121, 404
boundary, 246 repeating key, 394
creation, 236, 258 repetición, 252, 258
escaping, 236, 259, 525 repetition, 85, 240, 241, 409
flags, 242, 253, 258, 526 replace method, 253, 254, 273,
global, 253, 260, 262 525
grouping, 241, 242, 254 replaceChild method, 364, 534
internationalization, 267 reportError function, 504
matching, 248, 260 request, 464, 465, 467, 478
methods, 237, 244, 259 request function, 309, 466–468
repetition, 240 requestAnimationFrame function,
rejecting (a promise), 306, 310, 380, 407, 409, 446, 535
326 requestType function, 311
relación simbiótica, 298 require function, 281, 283, 294,
relative path, 284, 456, 470, 538 456, 458, 474, 491
relative positioning, 378, 380 reserved word, 40
remainder operator, 21, 54, 514, resolution, 284, 456
535 resolve function, 473
remote access, 470 resolving (a promise), 304, 306,
removeChild method, 363 310, 326
removeEventListener method, 389, resource, 470, 494
536 response, 465, 472, 476
rename function, 463 responsiveness, 387, 453
renderTalk function, 505 respuesta, 301, 309
renderTalkForm function, 506 retry, 309
renderUserField function, 504 return keyword, 66, 74, 166, 322,
570
515, 520 runAnimation function, 446, 450
return value, 43, 66, 220, 519 runGame function, 448, 450
reuse, 89, 186, 274, 276, 277, 457 runLevel function, 447, 450
reverse method, 131 running code, 13
reversing (exercise), 131, 517 runRobot function, 199, 523
rgb (CSS), 432
right-aligning, 384 sandbox, 13
rmdir function, 475, 480 scaling, 431
roadGraph object, 194 scalpel (exercise), 330, 529
roads array, 192 scheduling, 325
roads module (exercise), 294, 528 scientific notation, 19, 273
robot, 192, 195, 199, 203, 204, scope, 68, 69, 78, 341, 345, 352,
207, 293 353, 531, 532
robot efficiency (exercise), 208, script, 396
523 script (HTML tag), 406, 407
robustez, 486 SCRIPTS data set, 142, 147, 151,
rounding, 125, 219, 439 154, 158
router, 485, 490 scroll event, 403, 410
Router class, 491 scrolling, 393, 403, 404, 434, 435,
routeRequest function, 318 445
routeRobot function, 203 search method, 259
routing, 315 search problem, 362, 479
rule (CSS), 375, 376 search tool (exercise), 479, 537
run function, 346 searching, 248, 251, 259
run-time error, 215, 217, 220, 232, sección, 264
532 secuencia, 240
security, 473, 490
571
seguimiento de la pila, 224 SICP, 332
selección, 377 side effect, 37, 55, 89, 104, 131,
select (HTML tag), 539 144, 261, 290, 329, 359,
semicolon, 36 363, 371
send method, 302, 308 sign, 19, 273
sendGossip function, 314 sign bit, 19
sep binding, 473 signal, 16
Separador de vocales Mongol, 267 signo, 525
serialization, 128, 129 signo de interrogación, 257
server, 464, 466, 469, 470, 490 simplicity, 349
servidor, 452, 483 simulation, 195, 199, 416, 424,
set, 239 535
set (data structure), 189, 208 sine, 124, 381, 427, 443
Set class, 189, 208, 523 single-quote character, 22, 273
set method, 173 singleton, 209
setAttribute method, 368, 369 sintaxis, 333, 334
setInterval function, 409 sintáctico, 357
setter, 182 sistema de archivos, 461
setTimeout function, 300, 325, sistema de escritura, 142
409, 410, 499, 508 sistema de módulos, 279
shared property, 165, 169, 170 sitio web, 482
shift key, 395 sitios web, 452
shift method, 116 skill-sharing project, 487, 490, 501
shiftKey property, 395 SkillShareApp class, 509
short-circuit evaluation, 83, 343, skipSpace function, 337, 352
520 slash character, 21, 58, 236, 256,
572
473, 538 startsWith method, 472
slice method, 118, 119, 143, 366, stat function, 463, 474, 475, 538
517, 531 state, 49, 52, 55
sloppy programming, 411 in
smooth animation, 381 objects, 422
socket, 485 of application, 433, 511
solicitud, 301, 309 statement, 36
some method, 151, 157, 313, 492 static (reserved word), 40
sondeo, 387 static file, 487, 492
sondeo largo, 485 static method, 183, 190, 421, 524
sorting, 357 Stats type, 475
source property, 260 statSync function, 538
special form, 333, 341, 342 status code, 455
special return value, 220, 221 status property, 503
specialForms object, 342 stdout property, 470
speed, 1 stoicism, 386
split method, 194, 421 stopPropagation method, 391
spread operator, 431 storage function, 305
square brackets, 98, 123, 239, 516 stream, 465, 467, 468, 472, 477,
square example, 65, 71, 72 495
square root, 111, 123, 521 strict mode, 211, 212
stack, see call stack string, 22, 96, 99, 104
stack overflow, 75, 80, 92, 515 indexing, 92, 118, 121, 152,
standard, 453 243
standard environment, 41 length, 61, 152
standard output, 470 methods, 119, 243
573
notation, 22 Symbol function, 176
properties, 119 Symbol.iterator symbol, 177
representation, 24 SymmetricMatrix class, 184
searching, 119 synchronization, 510, 540
String function, 44, 174 synchronous programming, 296,
strings, 151 321, 464, 480
strong (HTML tag), 369, 372 syncState method, 540
structure, 276 syntax, 18, 20, 22, 35–37, 40, 45,
structure sharing, 132 48, 52, 56, 65, 71, 101,
style, 372 211, 222, 228, 273
style (HTML tag), 375 syntax tree, 336, 339, 340, 357
style attribute, 372, 374, 375, 429 SyntaxError type, 337
style sheet, see CSS símbolo, 176
subclase, 185
submit event, 506, 539 tabbed interface (exercise), 415,
substitution, 89 535
subtraction, 21, 189 tabindex attribute, 396
suites de prueba, 216 tabla, 110, 111
sum function, 130 tabla de frecuencias, 108
summing (exercise), 130, 516 table, 432
summing example, 7, 135, 146, table (HTML tag), 383, 418, 431
347 table example, 532
superclase, 185 tableFor function, 111
suposición, 228 talk, 494, 496, 497
Sussman, Gerald, 332 talkResponse method, 498
switch keyword, 56 talksAbout function, 361
talkURL function, 504
574
Tamil, 142 textScripts function, 155, 520
tangent, 124 th (HTML tag), 384
target property, 392 then method, 304, 306, 307, 313,
task management example, 117 530
TCP, 486 this, 99, 161, 162, 166, 212
td (HTML tag), 384, 431 thread, 326
teclado, 41, 386, 393, 394 throw keyword, 222, 223, 230,
teclado virtual, 396 233, 524
teléfono, 396 tiempo, 244, 300, 381, 411
temperature example, 182 Tiempo Unix, 245
template, 511, 540 time, 437, 438, 442, 447
tentacle (analogy), 38, 102, 105 timeline, 297, 325, 380, 386, 407
teoría, 218 timeout, 308, 409, 488, 499
terminal, 454 Timeout class, 309
termitas, 298 times method, 424
ternary operator, 29, 34, 342 tipo, 17
test method, 237 tipo de solicitud, 302
test suite, 215 tipo variable, 214
testing, 207, 215 title, 501
text, 22 toLowerCase method, 99, 385
text node, 358, 361, 365, 535 tool, 288, 460
text-align (CSS), 384 top (CSS), 378, 380, 382, 385
TEXT_NODE code, 358, 535 top-level scope, see global scope
textarea (HTML tag), 410, 538 toString method, 163, 164, 171,
textContent property, 534 172, 174, 469
texto, 354, 358, 462 touch, 401
575
touchend event, 401 unary operator, 26
touches method, 438 uncaught exception, 227, 308
touches property, 401 undefined, 30, 31, 39, 66, 76, 97,
touchmove event, 401 102, 127, 212, 220
touchstart event, 401 underline, 372
toUpperCase method, 99, 216, 385, underscore character, 40, 57, 160,
469 246, 258
tr (HTML tag), 384, 431 Unicode, 24, 27, 142, 152, 238,
trackKeys function, 445, 450 267, 268
transpilation, 349 property, 268
traversal, 249 unidades de código, 152
trial and error, 218, 445 uniformidad, 334
triangle (exercise), 61, 513 uniqueness, 376
trigonometry, 381 unit (CSS), 382, 404
trigonometría, 124 Unix, 475, 478, 479
trim method, 120, 421 unlink function, 463, 475
truco, 285 unshift method, 116
true, 26 upcasing server example, 469
try keyword, 223, 226, 312, 524, updated method, 495, 496, 500,
525 539
type, 25, 187 upgrading, 277
type checking, 215, 287 URL, 466, 487, 504
type coercion, 30–33, 44 url package, 472, 498
type property, 334, 390 urlToPath function, 472
typeof operator, 25, 133, 519 usability, 393
TypeScript, 215 use strict, see strict mode
576
user experience, 387, 504 virtual world, 195, 199
user interface, 228 vista en vivo, 486
UTF16, 24, 152 vocabulario, 64, 136
UTF8, 462, 463 vocabulary, 64, 137
void operator, 40
validation, 220, 232, 333, 436, volatile data storage, 17
496, 497
valor de retorno, 303 waitForChanges method, 499
valores, 17, 304 waiting, 300
var keyword, 39, 67, 127 wave, 427, 443
variable, 7, see binding Web, see World Wide Web
variable contador, 535 web application, 10
Vec class, 189, 422, 423, 442 web browser, see browser
vector (exercise), 189, 521 web worker, 407
velocidad, 4 WebDAV, 480
vencimiento, 486 website, 480
ventana, 391, 400 WebSockets, 485
verbosity, 73, 297 weekDay module, 279
versionado semántico, 460 weekend project, 481
versión, 277, 459, 460 weresquirrel example, 95, 100, 104,
viewport, 434, 436 107, 113, 116
VillageState class, 196 while loop, 8, 49, 52, 86, 263
VillaPradera, 192 whitespace, 51, 57, 120, 238, 267,
vinculaciones, 70, 102, 127, 228, 353, 532
286 in HTML, 362, 535
vinculación, 49, 53, 62, 65, 67, in URLs, 487
106, 263 trimming, 421
577
why, 35 árbol, 164, 335, 339, 357, 358
window, 406 árbol de sintaxis, 335
window object, 387, 388 índice, 97
with statement, 213
wizard (mighty), 6
word boundary, 247
word character, 247
World Wide Web, 10, 128
writable stream, 465, 467, 470,
472
write method, 465, 467, 468
writeFile function, 462, 468, 539
writeHead method, 465
writing code, 12, 192
WWW, see World Wide Web
XML, 359
yield (reserved word), 40
yield keyword, 323
your own loop (example), 157
Yuan-Ma, 16, 274, 452
Zawinski, Jamie, 235
zero-based counting, 92, 97, 245
zeroPad function, 87
zona horaria, 245
ángulo, 381
578