Eloquent JavaScript
Eloquent JavaScript
Illustrations by various artists: Cover and chapter illustrations by Madalina Tantareanu. Pixel art in
Chapters 7 and 16 by Antonio Perdomo Pastor. Regular expression diagrams in Chapter 9 generated
with regexper.com by Jeff Avallone. Village photograph in Chapter 11 by Fabrice Creuzot. Game
concept for Chapter 15 by Thomas Palef.
The third edition was made possible by 325 financial backers, most notably and
. The second edition was supported by 454 backers, with significant contributions
from , , and .
Introduction
1. Values, Types, and Operators (Part 1: Language)
2. Program Structure
3. Functions
4. Data Structures: Objects and Arrays
5. Higher-order Functions
6. The Secret Life of Objects
7. Project: A Robot
8. Bugs and Errors
9. Regular Expressions
10. Modules
11. Asynchronous Programming
12. Project: A Programming Language
13. JavaScript and the Browser (Part 2: Browser)
“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
Este libro intentará familiarizarte lo suficiente con este lenguaje para poder
hacer cosas útiles y divertidas con él.
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.
P o r q u é e l l e n g u a j e i m p o r ta
Ese es un programa que suma los números del 1 al 10 entre ellos e imprime
el resultado: 1 + 2 + ... + 10 = 55 . Podría ser ejecutado en una simple
máquina hipotética. Para programar las primeras computadoras, era
necesario colocar grandes arreglos de interruptores en la posición correcta o
perforar agujeros en tarjetas de cartón y dárselos a la computadora.
Probablemente puedas imaginarte lo tedioso y propenso a errores que era
este procedimiento. Incluso escribir programas 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. Podría ser
escrito en español así:
Aunque eso ya es más legible que la sopa de bits, es aún difícil de entender.
Usar nombres en lugar de números para las instrucciones y ubicaciones de
memoria ayuda:
console.log(suma(rango(1, 10)));
// → 55
¿ Q u é e s J ava S c r i p t ?
JavaScript se introdujo en 1995 como una forma de agregar programas 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. Alguien pensó
que era una buena idea intentar cabalgar sobre este éxito. Ahora estamos
atrapados con el nombre.
Hay quienes dirán cosas terribles sobre JavaScript. Muchas de estas cosas
son verdaderas. Cuando estaba comenzando a escribir algo en JavaScript
por primera vez, rápidamente comencé a despreciarlo. El lenguaje aceptaba
casi cualquier cosa que escribiera, pero la interpretaba 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 espacio 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 superar 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.
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 plataformas
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.
C ódi g o, y q u é h ac e r c on é l
La parte del lenguaje del libro comienza con cuatro capítulos para presentar
la estructura básica del lenguaje de JavaScript. Estos introducen estructuras
de control (como la palabra while que ya viste en esta introducción),
funciones (escribir tus propios bloques de construcció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.
function factorial(numero) {
if (numero == 0) {
return 1;
} else {
return factorial(numero - 1) * numero;
}
}
console.log(factorial(8));
// → 40320
¡Buena suerte!
Chapter 1
Va l o r e s , T i p o s , y O p e r a d o r e s
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, usualmente
descritos como ceros y unos. Dentro de la computadora, estos 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 secuencia de ceros y unos y,
de esa manera ser representada en bits.
0 0 0 0 1 1 0 1
128 64 32 16 8 4 2 1
Va l o r e s
Para poder trabajar con tales cantidades de bits sin perdernos, debemos
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 sucesivamente.
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, 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.
Números
13
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 almacenan 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 representados 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.
9.81
Aritmética
100 + 4 * 11
(100 + 4) * 11
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'
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.
console.log(typeof 4.5)
// → number
console.log(typeof "x")
// → string
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.
Va l o r e s B o o l e a n o s
C o m pa r a c i ó n
console.log(3 > 2)
// → true
console.log(3 < 2)
// → false
Los signos > y < son tradicionalmente símbolos para “mayor que” y
“menor que”, respectivamente. Ambos son operadores binarios. Aplicarlos
resulta en un valor Boolean que indica si la condición que indican se
cumple.
Otros operadores similares son >= (mayor o igual que), <= (menor o igual
que), == (igual a), y != (no igual a).
console.log("Itchy" != "Scratchy")
// → true
console.log("Manzana" == "Naranja")
// → false
console.log(NaN == NaN)
// → false
console.log(false || true)
// → true
console.log(false || false)
// → false
1 + 1 == 2 && 10 * 10 > 50
console.log(true ? 1 : 2);
// → 1
console.log(false ? 1 : 2);
// → 2
Va l o r e s va c í o s
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.
C o n v e r s i ó n d e t i p o a u t o m át i c a
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
Cuando algo que no se traduce a un número en una manera obvia (tal como
"cinco" o undefined ) es convertido a un número, obtenemos el valor NaN .
Operaciones aritméticas subsecuentes con NaN , continúan produciendo NaN ,
asi que si te encuentras obteniendo uno de estos valores en algun lugar
inesperado, busca por coerciones de tipo accidentales.
console.log(null == undefined);
// → true
console.log(null == 0);
// → false
Pero que pasa si queremos probar que algo se refiere precisamente al valor
false ? Las reglas para convertir strings y números a valores Booleanos,
dice que 0 , NaN , y el string vació ( "" ) cuentan como false , mientras que
todos los otros valores cuentan como true . Debido a esto, expresiones
como 0 == false , y "" == false son también verdaderas. Cuando no
queremos ninguna conversion de tipo automática, existen otros dos
operadores adicionales: === y !== . El primero prueba si un valor es
precisamente igual al otro, y el segundo prueba si un valor no es
precisamente igual. Entonces "" === false es falso, como es de esperarse.
console.log(null || "usuario")
// → usuario
console.log("Agnes" || "usuario")
// → Agnes
Podemos utilizar esta funcionalidad como una forma de recurrir a un valor
por defecto. Si tenemos un valor que puede estar vacío, podemos usar ||
después de este para remplazarlo con otro valor. Si el valor inicial puede ser
convertido a falso, obtendra el reemplazo en su lugar.
Resumen
E st ruc t u r a de Pro gr a m a
“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!”
Expresiones y de cl ar aciones
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.
1;
!false;
Sin embargo, es un programa inútil. Una expresión puede estar feliz solo
con producir un valor, que luego pueda ser utilizado por el código
circundante. Una declaración es independiente por si misma, por lo que
equivale a algo solo si afecta al mundo. Podría mostrar algo en la pantalla—
eso cuenta como cambiar el mundo—o podría cambiar el estado interno de
la máquina en una manera que afectará a las declaraciones que vengan
después de ella. Estos cambios se llaman efecto secundarios. Las
declaraciones en el ejemplo anterior solo producen los valores 1 y true y
luego inmediatamente los tira a la basura. Esto no deja ninguna huella en el
mundo. Cuando ejecutes este programa, nada observable ocurre.
Vincul aciones
let atrapado = 5 * 5;
Cuando una vinculación señala a un valor, eso no significa que esté atada a
ese valor para siempre. El operador = puede usarse en cualquier momento
en vinculaciones existentes para desconectarlas de su valor actual y hacer
que ellas apuntan a uno nuevo:
Las palabras var y const también pueden ser usadas para crear
vinculaciones, en una manera similar a let .
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:
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
prompt("Introducir contraseña");
La función c onsole.lo g
Va l o r e s d e r e t o r n o
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. 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 expresiones 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
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.
if (1 + 1 == 2) console.log("Es verdad");
// → Es verdad
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);
let numero = 0;
while (numero <= 12) {
console.log(numero);
numero = numero + 2;
}
// → 0
// → 2
// … etcetera
Una declaración que comienza con la palabra clave while crea un ciclo. La
palabra while es seguida por una expresión en paréntesis y luego por una
declaración, muy similar a if . El bucle sigue ingresando a esta declaración
siempre que la expresión produzca un valor que dé true cuando sea
convertida a Boolean.
Como un ejemplo que realmente hace algo útil, ahora podemos escribir un
programa que calcula y muestra el valor de 210 (2 a la 10). Usamos dos
vinculaciones: una para realizar un seguimiento de nuestro resultado y una
para contar cuántas veces hemos multiplicado este resultado 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
let tuNombre;
do {
tuNombre = prompt("Quien eres?");
} while (!tuNombre);
console.log(tuNombre);
I n d e n ta n d o C ó d i g o
if (false != true) {
console.log("Esto tiene sentido.");
if (1 < 2) {
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. Entonces
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.
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 .
let resultado = 1;
for (let contador = 0; contador < 10; contador = contador + 1) {
resultado = resultado * 2;
}
console.log(resultado);
// → 1024
Ro m p i e n d o u n c i c l o
Ac t ua li z a n d o v i nc u l ac ion e s de m a n e r a
s u c i n ta
contador = contador + 1;
contador += 1;
Atajos similares funcionan para muchos otros operadores, como resultado
*= 2 para duplicar resultado o contador -= 1 para contar hacia abajo.
D e s pa c h a r e n u n va l o r c o n s w i t c h
if (x == "valor1") accion1();
else if (x == "valor2") accion2();
else if (x == "valor3") accion3();
else accionPorDefault();
C a p i ta l i z a c i ó n
pequeñatortugaverde
pequeña_tortuga_verde
PequeñaTortugaVerde
pequeñaTortugaVerde
El primer estilo puede ser difícil de leer. Me gusta mucho el aspecto del
estilo con los guiones bajos, aunque ese estilo es algo fastidioso de escribir.
Las funciones estándar de JavaScript, y la mayoría de los 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.
C o m e n ta r i o s
/*
Primero encontré este número garabateado en la parte posterior
de
un viejo cuaderno. Desde entonces, a menudo lo he visto,
apareciendo en números de teléfono y en los números de serie de
productos que he comprado. Obviamente me gusta, así que
decidí quedármelo
*/
const miNumero = 11213;
Resumen
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
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 un
largo tiempo y tengas un ligero dolor de cabeza.
Ciclo de un triángulo
#
##
###
####
#####
######
#######
Puede ser útil saber que puedes encontrar la longitud de un string
escribiendo .length después de él:
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
"Fizz" en lugar del número, y para los números divisibles por 5 (y no 3),
imprime "Buzz" en su lugar.
Ta b l e r o d e a j e d r e z
# # # #
# # # #
# # # #
# # # #
# # # #
# # # #
# # # #
# # # #
Cuando tengas un programa que genere este patrón, define una vinculación
tamaño = 8 y cambia el programa para que funcione con cualquier tamaño ,
dando como salida una cuadrícula con el alto y ancho dados.
Chapter 3
Funciones
“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
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 general tenemos que
introducir nuevos conceptos para evitar repetirnos demasiado.
console.log(cuadrado(12));
// → 144
Una función es creada con una expresión que comienza con la palabra clave
function (“función”). Las funciones tienen un conjunto de parámetros (en
este caso, solo x ) y un cuerpo, que contiene las declaraciones 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.
hacerSonido();
// → Pling!
console.log(potencia(2, 10));
// → 1024
let x = 10;
if (true) {
let y = 20;
var z = 30;
console.log(x + y + z);
// → 60
}
// 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.
Alcance anidado
En resumen, cada alcance local puede ver también todos los alcances
locales que lo contengan. El conjunto de vinculaciones visibles dentro de un
bloque está determinado por el lugar de ese bloque en el texto del
programa. Cada alcance local puede tambien ver todos los alcances locales
que lo contengan, y todos los alcances pueden ver el alcance global. Este
enfoque para la visibilidad de vinculaciones es llamado alcance léxico.
F u n c i o n e s c o m o va l o r e s
Pero los dos son diferentes. Un valor de función puede hacer todas las cosas
que otros valores pueden hacer—puedes usarlo en expresiones arbitrarias,
no solo llamarlo. Es posible almacenar un valor de función en una nueva
vinculación, pasarla como argumento a una función, y así sucesivamente.
Del mismo modo, una vinculación que contenga una función sigue siendo
solo una vinculación regular y se le puede asignar un nuevo valor, asi:
N o ta c i ó n d e d e c l a r a c i ó n
Hay una forma ligeramente más corta de crear una vinculación de funció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";
}
Este código funciona, aunque la función esté definida debajo del código
que lo usa. Las declaraciones de funciones no son parte del flujo de control
regular de arriba hacia abajo. Estas son conceptualmente trasladadas a la
cima de su alcance y pueden ser utilizadas por todo el código en ese
alcance. Esto es a veces útil porque nos da la libertad de ordenar el código
en una forma que nos parezca significativa, sin preocuparnos por tener que
definir todas las funciones antes de que sean utilizadas.
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 >= ).
No hay una buena razón para tener ambas funciones de flecha y expresiones
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 escribir pequeñas expresiones
de funciones de una manera menos verbosa. Las usaremos mucho en el
Capitulo 5.
La pil a de ll amada s
function saludar(quien) {
console.log("Hola " + quien);
}
saludar("Harry");
console.log("Adios");
no en una función
en saludar
en console.log
en saludar
no en una función
en console.log
no en una función
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.
function gallina() {
return huevo();
}
function huevo() {
return gallina();
}
console.log(gallina() + " vino primero.");
// → ??
Argumentos Op cionales
La ventaja es que este comportamiento se puede usar para permitir que una
función sea llamada con diferentes cantidades de argumentos. Por ejemplo,
esta función menos intenta imitar al operador - actuando ya sea en uno o
dos argumentos
function menos(a, b) {
if (b === undefined) return -a;
else return a - b;
}
console.log(menos(10));
// → -10
console.log(menos(10, 5));
// → 5
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 .
console.log(potencia(4));
// → 16
console.log(potencia(2, 6));
// → 64
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?
function envolverValor(n) {
let local = n;
return () => local;
}
function multiplicador(factor) {
return numero => numero * factor;
}
Re cursió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
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 se ejecuta con la
suficiente frecuencia como para tomar cantidades significativas de tiempo
—puedes medir luego y mejorar si es necesario.
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)`);
}
}
return encontrar(1, "1");
}
console.log(encontrarSolucion(24));
// → (((1 * 3) + 5) * 3)
Para hacer esto, la función realiza una de tres acciones. Si el número actual
es el número objetivo, la historia actual es una forma de llegar a ese
objetivo, por lo que es retornada. Si el número actual es mayor que el
objetivo, no tiene sentido seguir explorando esta rama ya que tanto agregar
como multiplicar solo hara que el número sea mas grande, por lo que
retorna null . Y finalmente, si aún estamos por debajo del número objetivo,
la función intenta ambos caminos posibles que comienzan desde el número
actual llamandose a sí misma dos veces, una para agregar y otra para
multiplicar. Si la primera llamada devuelve algo que no es null , esta es
retornada. De lo contrario, se retorna 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 estamos
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!
Hay dos formas más o menos naturales para que las funciones sean
introducidas en los programas.
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.
007 Vacas
011 Pollos
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
Ejercicios
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.
Pruébalo con 50 y 75. Observa cómo se comporta con -1. Por qué? Puedes
pensar en una forma de arreglar esto?
Conteo de frijoles
Escribe una función contarFs que tome un string como su único argumento
y devuelva un número que indica cuántos caracteres “F” en mayúsculas
haya en el string.
E s t r u c t u r a s d e Dat o s : O b j e t o s y
A r r ay s
“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)
Los números, los booleanos y los strings son los átomos que constituyen las
estructuras de datos. Sin embargo, muchos tipos de información requieren
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 limitados 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 Hombre Ardill a
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.
C o n j u n t o s d e d at o s
Para trabajar con una porción de datos digitales, primero debemos encontrar
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.
Propiedades
Métod os
Este ejemplo demuestra dos métodos que puedes usar para manipular
arrays:
Estos nombres algo tontos son los términos tradicionales para las
operaciones en una pila. Una pila, en programación, es una estructura de
datos que te permite agregar valores a ella y volverlos a sacar en el orden
opuesto, de modo que lo que se agregó de último se elimine primero. Estas
son comunes en la programación—es posible que recuerdes la pila de
llamadas en el capítulo anterior, que es una instancia de la misma idea.
Objetos
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 comas.
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"
};
console.log(Object.keys({x: 0, y: 0, z: 2}));
// → ["x", "y", "z"]
Hay una función Object.assign que copia todas las propiedades de un
objeto a otro.
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... */
];
M u ta b i l i d a d
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
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" .
console.log(objeto1 == objeto2);
// → true
console.log(objeto1 == objeto3);
// → false
objeto1.valor = 15;
console.log(objeto2.valor);
// → 15
console.log(objeto3.valor);
// → 10
Una vez que tiene suficientes puntos de datos, tiene la intención de utilizar
estadísticas para encontrar cuál de estos eventos puede estar relacionado
con la transformación a ardilla.
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]));
}
console.log(phi([76, 9, 4, 1]));
// → 0.068599434
Para extraer una tabla de dos por dos para un evento en específico del
diario, debemos hacer un ciclo a traves de todas las entradas y contar
cuántas veces ocurre el evento en relación a las transformaciones de ardilla.
console.log(tablaPara("pizza", JOURNAL));
// → [76, 9, 4, 1]
El cuerpo del ciclo en tablaPara determina en cual caja de la tabla cae cada
entrada del diario al verificar si la entrada contiene el evento específico que
nos interesa y si el evento ocurre junto con un incidente de ardilla. El ciclo
luego agrega uno a la caja correcta en la tabla.
C i c l o s d e a r r ay
Hay una forma más simple de escribir tales ciclos en JavaScript moderno.
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 = [];
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", …]
Yendo a traves de todos los eventos, y agregando aquellos que aún no están
en allí en el array eventos , la función recolecta cada tipo de evento.
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.
Durante algunos años, las cosas van bien para Jacques. Pero en algún
momento él pierde su trabajo. Porque vive en un país desagradable donde
no tener trabajo significa que no tiene servicios médicos, se ve obligado a
trabajar con a circo donde actua como El Increible Hombre-Ardilla,
llenando su boca con mantequilla de maní antes de cada presentación.
Un día, harto de esta existencia lamentable, Jacques no puede cambiar de
vuelta a su forma humana, salta a través de una grieta en la carpa del circo,
y se desvanece en el bosque. Nunca se le ve de nuevo.
A r r ay o l o g í a ava n z a d a
console.log([1, 2, 3, 2, 1].indexOf(2));
// → 1
console.log([1, 2, 3, 2, 1].lastIndexOf(2));
// → 3
El método concat (“concatenar”) se puede usar para unir arrays y asi crear
un nuevo array, similar a lo que hace el operador + para los strings.
Pero estos tipos tienen propiedades integradas. Cada valor de string tiene un
numero de metodos. Algunos muy útiles son slice e indexOf , que se
parecen a los métodos de array de los mismos nombres.
console.log("panaderia".slice(0, 3));
// → pan
console.log("panaderia".indexOf("a"));
// → 1
console.log(String(6).padStart(3, "0"));
// → 006
console.log("LA".repeat(3));
// → LALALA
Ya hemos visto la propiedad length en los valores de tipo string. Acceder a
los caracteres individuales en un string es similar a acceder a los elementos
de un array (con una diferencia que discutiremos en el Capítulo 6).
Pa r á m e t r o s r e s ta n t e s
Puede ser útil para una función aceptar cualquier cantidad de argumentos.
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
E l o b j e t o M at h
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));
// → {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 (exclusivo) cada
vez que la llamas.
console.log(Math.random());
// → 0.36993729369714856
console.log(Math.random());
// → 0.727367032552138
console.log(Math.random());
// → 0.40180766698904335
console.log(Math.floor(Math.random() * 10));
// → 2
Desest ruct ur ar
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]));
}
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.
Esto también funciona para vinculaciones creadas con let , var , o const .
Si sabes que el valor que estas vinculando es un array, puedes usar
corchetes para “mirar dentro” del valor, y asi vincular sus contenidos.
Un truco similar funciona para objetos, utilizando llaves en lugar de
corchetes.
JSON
Si deseas guardar datos en un archivo para más tarde, o para enviarlo a otra
computadora a través de la red, tienes que convertir de alguna manera estos
enredos de direcciones de memoria a una descripció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 popular
llamado JSON (pronunciado “Jason”), que significa JavaScript Object
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.
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
Puedes iterar sobre los arrays utilizando un tipo especial de ciclo for — for
(let elemento of array) .
Ejercicios
La suma de un r ango
console.log(suma(rango(1, 10)));
Escribe una función rango que tome dos argumentos, inicio y final , 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] .
R e v i r t i e n d o u n a r r ay
U n a l i s ta
let lista = {
valor: 1,
resto: {
valor: 2,
resto: {
valor: 3,
resto: null
}
}
};
value: 1
value: 2
rest: value: 3
rest:
rest: null
C o m pa r a c i ó n p r o f u n d a
Escribe una función igualdadProfunda que toma dos valores y retorne 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 .
“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.”
console.log(suma(rango(1, 10)));
Abst r ac ción
“Coloque 1 taza de guisantes secos por persona en un recipiente. 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
persona. 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:
“Por persona: 1 taza de guisantes secos, media cebolla picada, un tallo de apio y una
zanahoria.
Remoje los guisantes durante 12 horas. Cocine a fuego lento durante 2 horas en 4 tazas de
agua (por persona). Picar y agregar verduras. Cocine por 10 minutos más.”
A b s t r ay e n d o l a r e p e t i c i ó n
Las funciones simples, como las hemos visto hasta ahora, son una buena
forma de construir abstracciones. Pero a veces se quedan cortas.
Podemos abstraer “hacer algo N veces” como una función? Bueno, es fácil
escribir una función que llame a console.log N cantidad de veces.
function repetirLog(n) {
for (let i = 0; i < n; i++) {
console.log(i);
}
}
Pero, y si queremos hacer algo más que loggear los números? Ya que “hacer
algo” se puede representar como una función y que las funciones solo son
valores, podemos pasar nuestra acción como un valor de función.
repetir(3, console.log);
// → 0
// → 1
// → 2
Esto está estructurado un poco como un ciclo for —primero describe el tipo
de ciclo, y luego provee un cuerpo. Sin embargo, el cuerpo ahora está
escrito como un valor de función, que está envuelto en el paréntesis de la
llamada a repetir . Por eso es que tiene que cerrarse con el corchete 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.
function mayorQue(n) {
return m => m > n;
}
let mayorQue10 = mayorQue(10);
console.log(mayorQue10(11));
// → true
Y puedes tener funciones que cambien otras funciones.
function ruidosa(funcion) {
return (...argumentos) => {
console.log("llamando con", argumentos);
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
C o n j u n t o d e d at o s d e c ó d i g o s
Un área donde brillan las funciones de orden superior es en el
procesamiento de datos. Para procesar datos, necesitaremos algunos datos
reales. Este capítulo usara un conjunto de datos acerca de códigos—sistema
de escrituras como Latin, Cirílico, o Arábico.
Aunque solo puedo leer con fluidez los caracteres en Latin, aprecio el hecho
de que las personas estan escribiendo textos en al menos 80 diferentes
sistemas de escritura, muchos de los cuales ni siquiera reconocería. Por
ejemplo, aquí está una muestra de escritura a mano en Tamil.
{
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 asignados 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).
F i lt r a n d o a r r ay s
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:
Tr ansformand o c on map
Resumiend o c on reduce
Otra cosa común que hacer con arrays es calcular un valor único a partir de
ellos. Nuestro ejemplo recurrente, sumar una colección de números, es una
instancia de esto. Otro ejemplo sería encontrar el codigo con la mayor
cantidad de caracteres.
Los parámetros para reduce son, además del array, una función de
combinación y un valor de inicio. Esta función es un poco menos sencilla
que filter y map , así que mira atentamente:
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", …}
Composabilidad
Hay algunos vinculaciones más, y el programa tiene cuatro líneas más. Pero
todavía es bastante legible.
function promedio(array) {
return array.reduce((a, b) => a + b) / array.length;
}
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
Pero es más difícil de ver qué se está calculando y cómo. Y ya que los
resultados intermedios no se representan como valores coherentes, sería
mucho más trabajo extraer algo así como promedio en una función aparte.
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 correspondiente (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", …}
Usando contarPor , podemos escribir la función que nos dice qué codigos
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
Resumen
Ejercicios
Aplanamiento
Tu propio cicl o
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
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 .
L a V i d a S e c r e ta d e l o s O b j e t o s
“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
De esta forma, los conocimientos acerca de como funciona una parte del
programa pueden mantenerse locales a esa pieza. Alguien trabajando 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 actualizado.
Métod os
Los métodos no son más que propiedades que tienen valores de función.
Este es un método simple:
conejo.hablar("Estoy vivo.");
// → El conejo dice 'Estoy vivo.'
Por lo general, un método debe hacer algo en el objeto con que se llamó.
Cuando una función es llamada como un método—buscada como una
propiedad y llamada inmediatamente, como en objeto.metodo() —la
vinculación llamada this (“este”) en su cuerpo apunta automáticamente al
objeto en la cual fue llamada.
function hablar(linea) {
console.log(`El conejo ${this.tipo} dice '${linea}'`);
}
let conejoBlanco = {tipo: "blanco", hablar};
let conejoHambriento = {tipo: "hambriento", hablar};
hablar.call(conejoHambriento, "Burp!");
// → El conejo hambriento dice 'Burp!'
Como cada función tiene su propia vinculación this , cuyo valor depende
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 .
function normalizar() {
console.log(this.coordinadas.map(n => n / this.length));
}
normalizar.call({coordinadas: [0, 2, 3], length: 5});
// → [0, 0.4, 0.6]
Protot ip os
Observa atentamente.
let vacio = {};
console.log(vacio.toString);
// → function toString(){…}
console.log(vacio.toString());
// → [object Object]
console.log(Object.getPrototypeOf({}) ==
Object.prototype);
// → true
console.log(Object.getPrototypeOf(Object.prototype));
// → null
console.log(Object.getPrototypeOf(Math.max) ==
Function.prototype);
// → true
console.log(Object.getPrototypeOf([]) ==
Array.prototype);
// → true
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
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 asegurarte 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;
}
function Conejo(tipo) {
this.tipo = tipo;
}
Conejo.prototype.hablar = function(linea) {
console.log(`El conejo ${this.tipo} dice '${linea}'`);
};
N o ta c i ó n d e c l a s e
class Conejo {
constructor(tipo) {
this.tipo = tipo;
}
hablar(linea) {
console.log(`El conejo ${this.tipo} dice '${linea}'`);
}
}
S o b r e e s c r i b i e n d o p r o p i e d a d e s d e r i va d a s
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
El siguiente diagrama esboza la situación después de que este código ha
sido ejecutado. Los prototipos de Conejo y Object se encuentran detrás de
conejoAsesino como una especie de telón de fondo, donde las propiedades
que no se encuentren en el objeto en sí mismo puedan ser buscadas.
Rabbit
prototype
Object
killerRabbit
create: <function>
teeth: "long, sharp, ..."
prototype
type: "killer"
...
teeth: "small"
speak: <function>
toString: <function>
...
console.log(Array.prototype.toString ==
Object.prototype.toString);
// → false
console.log([1, 2].toString());
// → 1,2
console.log(Object.prototype.toString.call([1, 2]));
// → [object Array]
M a pa s
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.
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í.
Como tal, usar objetos simples como mapas es peligroso. Hay varias formas
posibles de evitar este problema. Primero, es posible crear objetos sin
ningun prototipo. Si pasas null a Object.create , el objeto resultante no se
derivará de Object.prototype y podra ser usado de forma segura como un
mapa.
console.log("toString" in Object.create(null));
// → false
Afortunadamente, JavaScript viene con una clase llamada Map que esta
escrita para este propósito exacto. Esta almacena un mapeo y permite
cualquier tipo de llaves.
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 alguna
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 operador in ,
puedes usar el método hasOwnProperty (“tienePropiaPropiedad”), el cual
ignora el prototipo del objeto.
console.log({x: 1}.hasOwnProperty("x"));
// → true
console.log({x: 1}.hasOwnProperty("toString"));
// → false
Polimorfismo
Conejo.prototype.toString = function() {
return `un conejo ${this.tipo}`;
};
console.log(String(conejoNegro));
// → un conejo negro
Símb olos
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
console.log([1, 2].toString());
// → 1,2
console.log([1, 2][simboloToString]());
// → 2 cm de hilo azul
let objetoString = {
[simboloToString]() { return "una cuerda de cañamo"; }
};
console.log(objetoString[simboloToString]());
// → una cuerda de cañamo
L a i n t e r fa z d e i t e r a d o r
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 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 proporcione
una segunda interfaz, iteradora. Esta es la cosa real que realiza la iteración.
Tiene un método next (“siguiente”) que retorna el siguiente 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 probablemente sea
agregado a un monton de objetos diferentes, es un símbolo real.
obtener(x, y) {
return this.contenido[y * this.ancho + x];
}
establecer(x, y, valor) {
this.contenido[y * this.ancho + x] = valor;
}
}
class IteradorMatriz {
constructor(matriz) {
this.x = 0;
this.y = 0;
this.matriz = matriz;
}
next() {
if (this.y == this.matriz.altura) return {done: true};
La clase hace un seguimiento del progreso de iterar sobre una matriz en sus
propiedades x y y . El método next (“siguiente”) comienza comprobando si
la parte inferior de la matriz ha sido alcanzada. Si no es 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 individuales de código
permanezcan pequeñas y autónomas. En un programa regular, donde no hay
necesidad de dividir el código en pedazos pequeños, declararias estos
métodos directamente en la clase.
Matriz.prototype[Symbol.iterator] = function() {
return new IteradorMatriz(this);
};
Ahora podemos recorrer una matriz con for / of .
G e t t e r s , s e t t e r s y e s tát i c o s
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
Cuando alguien lee desde la propiedad tamaño de este objeto, el método
asociado es llamado. Puedes hacer algo similar cuando se escribe en una
propiedad, usando un setter.
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);
}
}
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.
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.
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étodo set ,
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.
E l o p e r a d o r i n s ta n c e o f
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
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.
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 escrito para
utilizar una interfaz automáticamente sabe cómo trabajar con cualquier
cantidad de objetos diferentes que proporcionen la interfaz. Esto se llama
polimorfismo.
Ejercicios
Un t ip o ve ctor
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).
Conjuntos
Escribe una clase llamada Conjunto . Como Set , debe tener los métodos
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 (si era un miembro)
y tiene retorna un valor booleano que indica si su argumento es un
miembro del conjunto.
T o m a n d o u n m é t o d o p r e s ta d o
Anteriormente en el capítulo mencioné que el metodo hasOwnProperty de
un objeto puede usarse como una alternativa más robusta al operador in
cuando quieras ignorar las propiedades del prototipo. Pero, ¿y si tu mapa
necesita incluir la palabra "hasOwnProperty" ? Ya no podrás llamar a ese
método ya que la propiedad del objeto oculta el valor del método.
P r oy e c t o : U n Ro b o t
“[...] 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
const caminos = [
"Casa de Alicia-Casa de Bob", "Casa de Alicia-Cabaña",
"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"
];
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;
}
L a ta r e a
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);
}
}
}
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.
console.log(siguiente.lugar);
// → Casa de Alicia
console.log(siguiente.parcels);
// → []
console.log(primero.lugar);
// → Oficina de Correos
Dat o s p e r s i s t e n t e s
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. Podrí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 alguien 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.
Simul ación
Considera lo que un robot tiene que hacer para “resolver” un estado dado.
Debe recoger todos los paquetes visitando cada ubicación que tenga un
paquete, y entregarlos visitando cada lugar al que se dirige un paquete, pero
solo después de recoger el paquete.
function eleccionAleatoria(array) {
let eleccion = Math.floor(Math.random() * array.length);
return array[eleccion];
}
function robotAleatorio(estado) {
return {direccion:
eleccionAleatoria(grafoCamino[estado.lugar])};
}
EstadoPueblo.aleatorio = function(numeroDePaquetes = 5) {
let paquetes = [];
for (let i = 0; i < numeroDePaquetes; i++) {
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);
};
No queremos paquetes que sean enviados desde el mismo lugar al que están
dirigidos. Por esta razón, el bucle do sigue seleccionando nuevos lugares
cuando obtenga uno que sea igual a la dirección.
correrRobot(EstadoPueblo.aleatorio(), robotAleatorio);
// → Moverse a Mercado
// → Moverse a Ayuntamiento
// → …
// → Listo en 63 turnos
Le toma muchas vueltas al robot para entregar los paquetes, porque este no
está planeando muy bien. Nos ocuparemos de eso pronto.
L a r u ta d e l c a m i ó n d e c o r r e o s
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"
];
B ú s q u e d a d e r u ta s
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.
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.
Este robot usa su valor de memoria como una lista de instrucciones para
moverse, como el robot que sigue la ruta. Siempre que esa lista esté vacía,
este tiene que descubrir qué hacer a continuación. Toma el primer paquete
no entregado en el conjunto y, si ese paquete no se ha recogido aún, traza
una ruta hacia el. Si el paquete ha sido recogido, todavía debe ser
entregado, por lo que el robot crea una ruta hacia la dirección de entrega en
su lugar.
Ejercicios
Midiend o un rob ot
Puedes escribir un robot que termine la tarea de entrega más rápido que
robotOrientadoAMetas ? Si observas el comportamiento de ese robot, qué
obviamente cosas estúpidas este hace? Cómo podrían mejorarse?
Conjunto persistente
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 .
B ug s y Er ror e s
“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.”
L e ng ua j e
Hay algunas cosas de las que JavaScript se queja. Escribir un programa 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.
Mod o 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";
for (contador = 0; contador < 10; contador++) {
console.log("Feliz feliz");
}
}
puedesDetectarElProblema();
// → ReferenceError: contador is not defined
Así que la llamada fraudulenta a Persona tuvo éxito pero retorno un valor
indefinido y creó la vinculación nombre global. En el modo estricto, el
resultado es diferente.
"use strict";
function Persona(nombre) { this.nombre = nombre; }
let ferdinand = Persona("Ferdinand"); // olvide new
// → TypeError: Cannot set property 'nombre' of undefined
El modo estricto hace algunas cosas más. No permite darle a una función
múltiples parámetros con el mismo nombre y elimina ciertas características
problemáticas del lenguaje por completo (como la declaración with
(“con”), la cual esta tan mal, que no se discute en este libro).
Tipos
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.
Proband o
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 pruebas 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 correctamente en todas las situaciones
para las que escribiste tu prueba. Cuando rompas algo, lo notarás
inmediatamente, en lugar aleatoriomente encontrarte con el problema en
algún momento posterior.
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 persistentes auto-
contenidos en lugar de cambiar objetos, tiende a ser fácil de probar.
Depur ación
Una vez que notes que hay algo mal con tu programa porque se comporta
mal o produce errores, el siguiente paso es descubir cual es el problema.
13
1.3
0.13
0.013
…
1.5e-323
P r o pa g a c i ó n d e e r r o r e s
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;
}
function ultimoElemento(array) {
if (array.length == 0) {
return {fallo: true};
} else {
return {elemento: array[array.length - 1]};
}
}
E xc e p c i o n e s
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.
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);
}
Bueno, casi...
L i m p i a n d o d e s p u é s d e e xc e p c i o n e s
El efecto de una excepción es otro tipo de flujo de control. Cada acción que
podría causar una excepción, que es prácticamente cualquier llamada de
función y acceso a propiedades, puede causar al control dejar tu codigo
repentinamente.
Eso significa que cuando el código tiene varios efectos secundarios, incluso
si parece que el flujo de control “regular” siempre sucederá, una excepción
puede evitar que algunos de ellos sucedan.
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;
}
Ese código podría haber sido escrito de una manera un poco más
inteligente, 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.
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 .”
C a p t u r a s e l e c t i va
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 generalmente 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 cuidadoso con la
corrupción de datos. Aborta todo el proceso cuando ocurre una excepción
no manejada.
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 (;;) {
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");
}
}
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);
}
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 excepciones no
relacionadas pasen a través. Si reintroduce el error tipográfico, el error de la
vinculación indefinida será reportado correctamente.
Afirmaciones
Si, por ejemplo, primerElemento se describe como una función que nunca
se debería invocar en arrays vacíos, podríamos escribirla así:
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
importante 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 control del
programa deberían ser manejados con gracia. A veces, cuando el problema
pueda ser manejado localmente, los valores de devolución especiales son
una buena forma de rastrearlos. De lo contrario, las excepciones pueden ser
preferibles.
Ejercicios
R e i n t e n ta r
La caja bloqueada
const caja = {
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 directamente a
la propiedad privada _contenido está prohibido.
“Algunas personas, cuando confrontadas con un problema, piensan ‘Ya sé, usaré expresiones
regulares.’ Ahora tienen dos problemas.”
—Jamie Zawinski
Proband o p or c oincidencia s
console.log(/abc/.test("abcde"));
// → true
console.log(/abc/.test("abxde"));
// → false
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.
console.log(/[0123456789]/.test("en 1992"));
// → true
console.log(/[0-9]/.test("en 1992"));
// → true
Por lo que podrías coincidir con un formato de fecha y hora como 30-01-
2003 15:20 con la siguiente expresión:
R e p i t i e n d o pa r t e s d e u n pat r ó n
Para indicar que un patrón deberia ocurrir un número preciso de veces, usa
llaves. Por ejemplo, al poner {4} después de un elemento, hace que
requiera que este ocurra exactamente cuatro veces. También es posible
especificar un rango de esta manera: {2,4} significa que el elemento debe
ocurrir al menos dos veces y como máximo cuatro veces.
Aquí hay otra versión del patrón fecha y hora que permite días tanto en
dígitos individuales como dobles, meses y horas. Es también un poco más
fácil de descifrar.
let fechaHora = /\d{1,2}-\d{1,2}-\d{4} \d{1,2}:\d{2}/;
console.log(fechaHora.test("30-1-2003 8:45"));
// → true
A g r u pa n d o s u b e x p r e s i o n e s
Coincidencia s y grup os
Un objeto retornado por exec tiene una propiedad index (“indice”) que nos
dice donde en el string comienza la coincidencia exitosa. Aparte de eso, el
objeto parece (y de hecho es) un array de strings, cuyo primer elemento es
el string que coincidio—en el ejemplo anterior, esta es la secuencia de
dígitos que estábamos buscando.
console.log(/mal(isimo)?/.exec("mal"));
// → ["mal", undefined]
console.log(/(\d)+/.exec("123"));
// → ["123", "3"]
Los grupos pueden ser útiles para extraer partes de un string. Si no solo
queremos verificar si un string contiene una fecha pero también extraerla y
construir un objeto que la represente, podemos envolver paréntesis
alrededor de los patrones de dígitos y tomar directamente la fecha del
resultado de exec .
L a c l a s e Dat e ( “ F e c h a” )
JavaScript tiene una clase estándar para representar fechas—o mejor dicho,
puntos en el tiempo. Se llama Date . Si simplemente creas un objeto fecha
usando new , obtienes la fecha y hora actual.
console.log(new Date());
// → Mon Nov 13 2017 16:19:11 GMT+0100 (CET)
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)
Pa l a b r a y l í m i t e s d e s t r i n g
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
Pat r o n e s d e e l e c c i ó 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 pollo,
o cualquiera de sus formas plurales.
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.
L a s m e c á n i c a s d e l e m pa r e j a m i e n t o
Conceptualmente, cuando usas exec o test el motor de la expresión
regular busca una coincidencia en tu string al tratar de hacer coincidir la
expresión primero desde el comienzo del string, luego desde el segundo
caracter, y así sucesivamente hasta que encuentra una coincidencia o llega
al final del string. Retornara la primera coincidencia que se puede encontrar
o fallara en encontrar cualquier coincidencia.
"pig"
"chicken"
Ret ro cediend o
One of:
“0”
“b”
“1”
One of:
word boundary word boundary
digit
“h”
“a” - “f”
digit
One of:
"0"
"b"
"1"
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.
console.log("papa".replace("p", "m"));
// → mapa
El primer argumento también puede ser una expresión regular, en cuyo caso
ña primera coincidencia de la expresión regular es reemplazada. Cuando
una opción g (para global) se agrega a la expresión regular, todas las
coincidencias en el string será reemplazadas, no solo la primera.
console.log("Borobudur".replace(/[ou]/, "a"));
// → Barobudur
console.log("Borobudur".replace(/[ou]/g, "a"));
// → Barabadar
console.log(
"Liskov, Barbara\nMcCarthy, John\nWadler, Philip"
.replace(/(\w+), (\w+)/g, "$2 $1"));
// → Barbara Liskov
// John McCarthy
// Philip Wadler
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
Pero la salida de la última línea parece haber salido mal. Por qué?
function removerComentarios(codigo) {
return codigo.replace(/\/\/.*|\/\*[^]*?\*\//g, "");
}
console.log(removerComentarios("1 /* a */+/* b */ 1"));
// → 1 + 1
Hay casos en los que quizás no sepas el patrón exacto necesario para
coincidir cuando estes escribiendo tu código. Imagina que quieres buscar el
nombre del usuario en un texto y encerrarlo en caracteres de subrayado para
que se destaque. Como solo sabrás el nombrar una vez que el programa se
está ejecutando realmente, no puedes usar la notación basada en barras.
Pero puedes construir un string y usar el constructor RegExp en el. Aquí hay
un ejemplo:
El métod o search
console.log(" palabra".search(/\S/));
// → 2
console.log(" ".search(/\S/));
// → -1
La propiedad l a stIndex
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.
console.log("Banana".match(/an/g));
// → ["an", "an"]
Por lo tanto, ten cuidado con las expresiones regulares globales. Los casos
donde son necesarias—llamadas a replace y lugares donde deseas
explícitamente usar lastIndex —son generalmente los únicos lugares donde
querras usarlas.
Esto hace uso del hecho de que el valor de una expresión de asignació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.
A n á li si s de un a rc hi vo I NI
motordebusqueda=https://duckduckgo.com/?q=$1
malevolencia=9.7
[davaeorn]
nombrecompleto=Davaeorn
tipo=hechizero malvado
directoriosalida=/home/marijn/enemies/davaeorn
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;
}
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 objeto, 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.
🍎
console.log(/ {3}/.test(" 🍎🍎🍎
"));
// → false
🌹
console.log(/<.>/.test("< >"));
// → false
🌹
console.log(/<.>/u.test("< >"));
// → true
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
Una expresión regular tiene un método test para probar si una determinada
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 expresió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.
Las expresiones regulares pueden tener opciones, que se escriben después
de la barra que cierra la expresión. La opción i hace que la coincidencia 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.
Ejercicios
Golf Re gexp
Para cada uno de los siguientes elementos, escribe una expresión regular
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)
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.
Números ot r a vez
Módul os
Módulos
Las interfaces de los módulos tienen mucho en común con las interfaces 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 interactúan a través de
conectores bien definidos, y menos como barro, donde todo se mezcla con
todo.
Para separar módulos de esa manera, cada uno necesita su propio alcance
privado.
Pa q u e t e s
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.
El software es barato de copiar, por lo que una vez lo haya escrito alguien,
distribuirlo a otras personas es un proceso eficiente. Pero escribirlo 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 personas son
simplemente agradables, y porque la publicación de un buen software puede
ayudarte a hacerte un poco famoso entre los programadores, se publican
muchos paquetes bajo una licencia que explícitamente permite a otras
personas usarlos.
Así que diseñaron sus propios sistema de módulos arriba del lenguaje.
Puedes usar funciones de JavaScript para crear alcances locales, y objetos
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 consiste en
diaDeLaSemana.nombre y diaDeLaSemana.numero , y oculta su vinculación
local nombres dentro del alcance de una expresión de función que se invoca
inmediatamente.
console.log(diaDeLaSemana.nombre(diaDeLaSemana.numero("Domingo")))
;
// → Domingo
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.
E va l u a n d o d at o s c o m o c ó d i g o
Hay varias maneras de tomar datos (un string de código) y ejecutarlos como
una parte del programa actual.
La forma más obvia es usar el operador especial eval , que ejecuta un string
en el alcance actual. Esto usualmente es una mala idea porque rompe
algunas de las propiedades que normalmente tienen los alcances, tal como
fácilmente predecir a qué vinculación se refiere un nombre dado.
const x = 1;
function evaluarYRetornarX(codigo) {
eval(codigo);
return x;
}
console.log(evaluarYRetornarX("var x = 2"));
// → 2
CommonJS
Debido a que el cargador envuelve el código del módulo en una funció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 (“exportaciones”).
require.cache = Object.create(null);
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;
}
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 sobrescribir
module.exports . Esto lo hacen muchos módulos para exportar un valor
único en lugar de un objeto de interfaz.
Módulos ECMAScript
Para crear una exportación por default, escribe export default antes de
una expresión, una declaración de función o una declaración de clase.
console.log(nombresDias.length);
// → 7
C o n s t r u y e n d o y e m pa q u e ta n d o
De hecho, muchos proyectos de JavaScript ni siquiera están, técnicamente,
escritos en JavaScript. Hay extensiones, como el dialecto de comprobación
de tipos mencionado en el Capítulo 7, que son ampliamente usados. Las
personas también suelen comenzar a usar extensiones planificadas para el
lenguaje mucho antes de que estas hayan sido 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.
Diseño de módul os
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.
Resumen
Ejercicios
Un rob ot modul ar
caminos
construirGrafo
grafoCamino
EstadoPueblo
correrRobot
eleccionAleatoria
robotAleatorio
rutaCorreo
robotRuta
encontrarRuta
robotOrientadoAMetas
Módulo de Caminos
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 velocidad 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 computadoras 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.
Asincronicidad
asynchronous
Otra forma de describir la diferencia es que esperar que las acciones
terminen es implicito en el modelo síncrono, mientras que es explicito, bajo
nuestro control, en el asincrónico.
T e c n o l o gí a c u e rvo
La mayoría de las personas son conscientes del hecho de que los cuervos
son pájaros muy inteligentes. Pueden usar herramientas, planear con
anticipación, recordar cosas e incluso comunicarse estas cosas entre ellos.
De voluc ión de ll a m a da s
(Todos los nombres de las vinculaciones y los strings se han traducido del
lenguaje cuervo a Español.)
Pero para hacer nidos capaces de recibir esa solicitud, primero tenemos 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. Asumiremos que un
cuervo sobrevuela e instala nuestro código controlador en todos los nidos.
Promesa s
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.
almacenamiento(granRoble, "enemigos")
.then(valor => console.log("Obtuve", valor));
Fr ac a s o
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.
Las promesas hacen esto más fácil. Estas pueden ser resueltas (la acción
termino con éxito) o rechazadas (esta falló). Los controladores de
resolución (registrados con then ) solo se llaman cuando la acción es
exitosa, y los rechazos se propagan automáticamente a la nueva promesa
que es retornada por then . Y cuando un controlador arroje una excepció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ó.
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 rechazada no
es manejada, y reportará esto como un error.
Tal y como es, eso solo causará que la devolución de llamada dada a send
nunca sea llamada, lo que probablemente hará que el programa se detenga
sin siquiera notar que hay un problema. Sería bueno si, después de un
determinado período de no obtener una respuesta, una solicitud expirará e
informara de un fracaso.
Y, como hemos establecido que las promesas son algo bueno, tambien
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.
Estos pueden ser traducidos para prometer resolución y rechazo por parte de
nuestra envoltura.
Debido a que las promesas solo se pueden resolver (o rechazar) una vez,
esto funcionara. La primera vez que se llame a resolve o reject se
determinara el resultado de la promesa y cualquier llamada subsecuente,
como el tiempo de espera que llega después de que finaliza la solicitud, o
una solicitud que regresa después de que otra solicitud es finalizada, es
ignorada.
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]);
});
}
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.
todosLados(nido => {
nido.estado.chismorreo = [];
});
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.
E n r u ta m i e n t o d e m e n s a j e s
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.
todosLados(nido => {
nido.estado.conexiones = new Map;
nido.estado.conexiones.set(nido.nombre, nido.vecinos);
difundirConexiones(nido, nido.nombre);
});
Una cosa que puedes hacer con grafos es encontrar rutas en ellos, como
vimos en el Capítulo 7. Si tenemos una ruta hacia el destino de un mensaje,
sabemos en qué dirección enviarlo.
Esta función encontrarRuta , que se parece mucho a encontrarRuta del
Capítulo 7, busca por una forma de llegar a un determinado nodo en la red.
Pero en lugar de devolver toda la ruta, simplemente retorna el siguiente
paso. Ese próximo nido en si mismo, usando su información actual sobre la
red, decididira hacia dónde enviar el mensaje.
Ahora podemos construir una función que pueda enviar mensajes de larga
distancia. Si el mensaje está dirigido a un vecino directo, se entrega
normalmente. Si no, se empaqueta en un objeto y se envía a un vecino que
este más cerca del objetivo, usando el tipo de solicitud "ruta" , que hace
que ese vecino repita el mismo comportamiento.
Funciones a síncrona s
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.
function red(nido) {
return Array.from(nido.estado.conexiones.keys());
}
Una función async está marcada por la palabra async antes de la palabra
clave function . Los métodos también pueden hacerse async al escribir
async antes de su nombre. Cuando se llame a dicha función o método, este
retorna una promesa. Tan pronto como el cuerpo retorne algo, esa promesa
es resuelta Si arroja una excepción, la promesa es rechazada.
Para código asincrónico no-trivial, esta notación suele ser más conveniente
que usar promesas directamente. Incluso si necesitas hacer algo que no se
ajuste al modelo síncrono, como realizar múltiples acciones al mismo
tiempo, es fácil combinar await con el uso directo de promesas.
Gener ad ores
function* potenciacion(n) {
for (let actual = n;; actual *= n) {
yield actual;
}
}
Conjunto.prototype[Symbol.iterator] = function*() {
for (let i = 0; i < this.miembros.length; i++) {
yield this.miembros[i];
}
};
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 permanece
inactivo, esperando por la siguiente acción.
try {
setTimeout(() => {
throw new Error("Woosh");
}, 20);
} catch (_) {
// Esto no se va a ejecutar
console.log("Atrapado!");
}
Promise.resolve("Listo").then(console.log);
console.log("Yo primero!");
// → Yo primero!
// → Listo
Errores a sincrónic os
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.
La parte async nombre => muestra que las funciones de flecha también
pueden ser async al poner la palabra async delante de ellas.
Pero está seriamente roto. Siempre devolverá solo una línea de salida,
enumerando al nido que fue más lento en responder.
Errores como este son fáciles de hacer, especialmente cuando se usa await ,
y debes tener en cuenta dónde se producen las brechas en tu código. Una
ventaja de la asincronicidad explicita de JavaScript (ya sea a través de
devoluciones de llamada, promesas, o await ) es que detectar estas brechas
es relativamente fácil.
Resumen
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.
Implemente algo como esto tu mismo como una función regular llamada
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.
Chapter 12
P r oy e c t o : U n L e n g ua j e d e
Pro gr a m ac ión
Análisis
hacer(definir(x, 10),
si(>(x, 5),
imprimir("grande"),
imprimir("pequeño")))
La uniformidad del lenguaje Egg significa que las cosas que son operadores
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.
{
tipo: "aplicar",
operador: {tipo: "palabra", nombre: ">"},
argumentos: [
{tipo: "palabra", nombre: "x"},
{tipo: "valor", valor: 5}
]
}
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]};
} else {
throw new SyntaxError("Sintaxis inesperada: " + programa);
}
return aplicarAnalisis(expresion,
programa.slice(emparejamiento[0].length));
}
function saltarEspacio(string) {
let primero = string.search(/\S/);
if (primero == -1) return "";
return string.slice(primero);
}
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.
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));
}
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: "+"},
// argumentos: [{tipo: "palabra", nombre: "a"},
// {tipo: "valor", valor: 10}]}
T h e e va l u at o r
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.
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.
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.
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 .
Another basic building block is hacer , which executes all its arguments
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;
};
To be able to create bindings and give them new values, we also create a
form called definir . It expects a word as its first argument and an
expression producing the value to assign to that word as its second
argument. Since definir , like everything, is an expression, it must return a
value. We’ll make it return the value that was assigned (just like
JavaScript’s = operator).
topScope.true = true;
topScope.false = false;
To supply basic arithmetic and comparison operators, we will also add some
function values to the scope. In the interest of keeping the code short, we’ll
use Function to synthesize a bunch of operator functions in a loop, instead
of defining them individually.
function run(programa) {
return evaluate(parse(programa), Object.create(topScope));
}
We’ll use object prototype chains to represent nested scopes, so that the
program can add bindings to its local scope without changing the top-level
scope.
run(`
hacer(definir(total, 0),
definir(count, 1),
while(<(count, 11),
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
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.
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),
1,
*(base, pow(base, -(exp, 1)))))),
imprimir(pow(2, 10)))
`);
// → 1024
C o m p i l at i o n
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.
C h e at i n g
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.
Or imagine you are building a giant robotic dinosaur and need to program
its behavior. JavaScript might not be the most effective way 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
Ex ercises
A r r ay s
Add support for arrays to Egg by adding the following three functions to the
top scope: array(...values) to construct an array containing the 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.
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 mechanism
causes this to work.
Comments
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 sc ope
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).
Object.prototype.hasOwnProperty.call(scope, name);
Chapter 13
J ava S c r i p t y e l N av e g a d o r
Los próximos capítulos del libro hablarán del navegador web. Sin
navegadores web, no existiría JavaScript. Incluso, si existieran, nadie le
habría puesto atención.
Redes e Internet
Las redes de computadoras han existido de los 50s. Si pones cables entre
dos o más computadoras y les permites enviar y recibir información a través
de esos cables, puedes hacer todo tipo de cosas increíbles.
Una computadora puede usar esta red para mandar bits a otra computadora.
Para cualquier comunicación efectiva, las computadoras en ambos lados
deben saber qué se supone que representan los bits. El significado de
cualquier secuencia de bits depende enteramente del tipo de cosas que se
está tratando de expresar y del mecanismo de codificación utilizado.
Esa conexión actua como un una tubería de dos direcciones por la cual los
bits pueden fluir, las computadoras en ambos extremos pueden poner
información en esa tubería. Una vez que los bits son transmitidos de forma
exitosa, pueden ser leidos nuevamente por la computadora en el otro lado.
Este es un modelo conveniente. Podrías decir que TCP provee una
abstracción de la red.
La Web
La Red Mundial (en inglés World Wide Web, no confundir con Internet) es
un conjunto de protocolos y formatos que nos permiten visitar páginas web
en un navegador. La parte “Web” del nombre se refiere al hecho de que esas
páginas pueden enlazarse fácilmente unas con otras, conectandose a una
gran red a entre la cual los usuarios pueden moverse.
Para llegar a ser parte de la Web, todo lo que tienes que hacer es conectar
una computadora a Internet y ponerla a escuchar en el puerto 80 con el
protocolo HTTP para que otras computadoras puedan pedirle documentos.
La primera sección nos dice que ésta URL utiliza el protocolo HTTP (La
versión ecriptada sería https://). Posteriormente sigue la sección que
identifica a qué servidor estamos solicitado el documento. La última
sección es una cadena de ruta que identifica el documento especifico (o
recurso) que nos interesa.
HTML
HTML, que significa Lenguaje de Marcado de Hipertexto (en inglés
Hypertext Markup Language), es el formato de documento utilizado para
páginas web. Un documento HTML contiene texto, así como etiquetas que
dan estructura al texto, describiendo elementos como enlaces, párrafos y
encabezados.
<!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>
Las etiquetas, encerradas entre paréntesis angulares ( < y > , los símbolos
para Menor qué y Mayor qué), proveen información acerca de la estructura
del documento. El otro texto es sólo texto plano.
El documento inicia con <!doctype html> , eso le indica al navegador que
interprete la página como HTML moderno, no como distintos lenguajes que
fueron utilizados en el pasado.
<!doctype html>
<meta charset=utf-8>
<title>Mi página de inicios</title>
El Model o de Ob je to del
D o cumento
“¡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
<!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>
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í .
Árb oles
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.
Lo mismo sucede para el DOM, los nodos para los elementos, los cuales
representan etiquetas HTML, determinan la estructura del documento. Estos
pueden tener nodos hijos. Un ejemplo de estos nodos es document.body .
Algunos de estos hijos pueden ser nodos hoja, como los fragmentos de texto
o los nodos comentario.
Cada objeto nodo DOM tiene una propiedad nodeType , la cual contiene 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 comentarios
obtienen el código 8 ( Node.COMMENT_NODE ).
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.
E l e s tá n d a r
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 secundarios. 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
destinadas a la programación del navegador vienen con esas herramientas.
M o v i é n d o s e a t r av é s d e l á r b o l
Los nodos del DOM contienen una amplia cantidad de enlaces a otros
nodos cercanos. El siguiente diagrama los ilustra:
childNodes firstChild
body
0 h1
Mi página de inicio
previousSibling
1 p
Hola, mi nombre es... parentNode
nextSibling
2 p
También escribí un...
lastChild
A pesar de que el diagrama muestra un solo enlace por cada tipo, cada nodo
tiene una propiedad parentNode que apunta al nodo al que pertenece, si es
que hay alguno. Igualmente, cada nodo elemento (nodo tipo 1) tiene una
propiedad childNodes que apunta a un objeto similar a un arreglo que
almacena a sus hijos.
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:
console.log(hablaSobre(document.body, "libro"));
// → 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, alcanzarlo
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.
<script>
let avestruz = document.getElementById("gertrudiz");
console.log(avestruz.src);
</script>
Un tercer método similar es getElementsByClassName , el cual, de manera
similar a getElementsByTagName busca a través de los contenidos de un
nodo elemento y obtiene todos los elementos que tienen una cadena dada en
su attributo class .
Ac t ua li z a r e l d o c um e n to
<p>Uno</p>
<p>Dos</p>
<p>Tres</p>
<script>
let parrafos = document.body.getElementsByTagName("p");
document.body.insertBefore(parrafos[2], parrafos[0]);
</script>
Crear nod os
Digamos que queremos escribir un script que reemplace todas las imagenes
(etiquetas <img> ) en el documento con el texto contenido en sus atributos
alt , los cuales especifican una representación textual alternativa de la
imagen.
<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>
El siguiente ejemplo define una utilidad elt , la cual crea un elemento nodo
y trata el resto de sus argumentos como hijos de ese nodo. Luego, esta
función es utilizada para agregar una atribución a una cita.
<blockquote id="cita">
Ningún libro puede terminarse jamás. Mientras se trabaja en
él aprendemos solo lo suficiente para encontrar inmaduro
el momento en el que nos alejamos de él.
</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>
At r i b u t o s
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.
<script>
let parrafos = document.body.getElementsByTagName("p");
for (let parrafo of Array.from(parrafos)) {
if (parrafo.getAttribute("data-classified") == "secreto") {
parrafo.remove();
}
}
</script>
L ay o u t
<script>
let parrafo = document.body.getElementsByTagName("p")[0];
console.log("clientHeight:", parrafo.clientHeight);
console.log("offsetHeight:", parrafo.offsetHeight);
</script>
<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");
}
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>
Est ilización
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. Este es un
ejemplo que utiliza la propiedad style :
<script>
let parrafo = document.getElementById("parrafo");
console.log(parrafo.style.color);
parrafo.style.color = "magenta";
</script>
Estilos en Ca scada
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>
Cuando varias reglas definen un valor para una misma propiedad, la regla
leída más recientemente obtiene una mayor precedencia y gana. Por lo que
si la regla en la etiqueta <style> incluyera font-weight: normal ,
contradiciendo la regla por defecto de font-weight , el texto se vería
normal, no en negritas. Los estilos en un atributo style aplicados
directamente al nodo tienen la mayor precedencia y siempre ganan.
.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;
}
La regla de precedencia que favorece a las reglas más recientemente
definidas aplican solamente cuando las reglas tienen la misma
especificidad. La especificidad de una regla es una medida de que tan
precisamente describe a los elementos que coinciden con la regla,
determinado por el número y tipo (etiqueta, clase o ID) de los aspectos del
elemento que requiera. Por ejemplo, una regla que apunta a p.a es más
específica que las reglas que apuntan a p o solamente a .a y tendrá
precedencia 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.
S e l e c t o r e s d e c o n s u lta
<script>
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
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.
cos(-⅔π)
sin(-⅔π)
sin(¼π)
cos(¼π)
El código de animación del gato mantiene un contador, angulo , para el
ángulo actual de la animación y lo incrementa cada vez que la función
animar es llamada. Luego, se puede utilizar este ángulo para calcular la
posición actual del elemento imagen. El estilo top es calculado con
Math.sin y multiplicado por 20, que es el radio vertical de nuestra elipse.
El estilo left se basa en Math.cos multiplicado por 200 por lo que la elipse
es mucho más ancha que su altura.
Nótese que los estilos usualmente necesitan unidades. En este caso, tuvimos
que agregar "px" al número para informarle al navegador que estábamos
contando en pixeles (al contrario de centímetros, “ems”, u otras unidades).
Esto es sencillo de olvidar. Usar números sin unidades resultará en estilos
que son ignorados—a menos que el número sea 0, que siempre indica la
misma cosa, independientemente de su unidad.
Resumen
Ejercicios
C o n s t r u y e u n a ta b l a
<table>
<tr>
<th>nombre</th>
<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> ).
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" .
E l e m e n t o s p o r n o m b r e d e ta g
E l s o m b r e r o d e l g at o
Manejo de Ev entos
“Tienes poder sobre tu mente, no sobre los acontecimientos. Date cuenta de esto, y
encontrarás la fuerza.”
—Marco Aurelio, Meditaciones
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.
Manejad or 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.
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.
<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.
Objetos de ev ento
P r o pa g a c i ó n
También es posible utilizar la propiedad target para lanzar una red amplia
para un evento específico. Por ejemplo, si tienes un nodo que contiene una
gran cantidad de botones, puede ser más conveniente el registrar un
manejador en un solo clic en el nodo externo y hacer que use la propiedad
target para averiguar si se hizo clic en un botón, en lugar de registrar
manejadores individuales en todos los botones.
<button>A</button>
<button>B</button>
<button>C</button>
<script>
document.body.addEventListener("click", evento => {
if (evento.target.nodeName == "BUTTON") {
console.log("Presionado", evento.target.textContent);
}
});
</script>
<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 cuando el
comportamiento esperado no funcione.
Eventos de te cl ad o
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.
Para notar cuando se escribió algo, los elementos en los que se puede
escribir, como las etiquetas <input> y <textarea> , lanzan eventos "input"
cada vez que el usuario cambia su contenido. Para obtener el contenido real
que fue escrito, es mejor leerlo directamente desde el campo seleccionado.
El Chapter ? mostrará cómo.
Actualmente, hay dos formas muy utilizadas para señalar en una pantalla:
mouse (incluyendo dispositivos que actuan como mouse, como páneles
táctiles y trackballs) y pantallas táctiles. Estos producen diferentes 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.
<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>
function movido(evento) {
if (evento.buttons == 0) {
window.removeEventListener("mousemove", movido);
} else {
let distancia = event.clientX - lastX;
let nuevaAnchura = Math.max(10, barra.offsetWidth +
distancia);
barra.style.width = nuevaAnchura + "px";
ultimoX = evento.clientX;
}
}
</script>
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.
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>
<style>
#progreso {
border-bottom: 2px solid blue;
width: 0;
position: fixed;
top: 0; left: 0;
}
</style>
<div id="progreso"></div>
<script>
// Crear algo de contenido
document.body.appendChild(document.createTextNode(
"supercalifragilisticoespialidoso ".repeat(1000)));
Eventos de fo c o
<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>
Ev ento de c arga
El hecho de que los eventos puedan ser procesados solo cuando no se está
ejecutando nada más que, si el ciclo de eventos está vinculado con otro
trabajo, cualquier interacción con la página (que ocurre a través de eventos)
se retrasará hasta que haya tiempo para procesarlo. Por lo tanto, si
programas demasiado trabajo, ya sea con manejadores de eventos de
ejecución prolongada o con muchos de breve ejecución, la página se
volverá lenta y engorrosa de usar.
Para casos en donde realmente se quiera hacer algo que requiera mucho
tiempo en segundo plano sin congelar la página, los navegadores proveeen
algo llamado web worker. Un worker es un proceso de JavaScript que se
jecuta junto con el guión principal, en su propia línea de tiempo.
Para evitar el problema de tener multiples hilos tocando los mismos datos,
los workers no comparten su alcance global o cualquier otro tipo de dato
con el entorno del guión principal. En cambio, tienes que comunicarte con
ellos enviando mensajes de un lado a otro.
Este código genera un worker que ejecuta ese guión, le envía algunos
mensajes y genera las respuestas.
let tictac = 0;
let reloj = setInterval(() => {
console.log("tictac", tictac++);
if (tictac == 10) {
clearInterval(reloj);
console.log("Detener.");
}
}, 200);
Antirreb ote
<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
Ejercicios
Gl ob o
Puedes controlar el tamaño del texto (los emojis son texto) configurando la
propiedad CSS font-size ( style.fontSize ) en su elemento padre.
Recuerda incluir una unidad en el valor, por ejemplo pixeles ( 10px ).
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
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.
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" .
P e s ta ñ a s
P r oy e c t o : U n J u e g o d e P l ata f o r m a
El juego
El jugador puede caminar alrededor con las teclas de las flechas izquierda y
derecha y pueden brincar con la flecha arriba. Brincar es una especilidad de
este personaje de juego. Puede alcanzar varias veces su propia altura y
puede cambiar de dirección en el aire. Esto puede no ser completamente
realista, pero ayuda a darle al jugador el sentimiento de estar en control
directo del avatar.
La t e cnol o gía
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.
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 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.
Leyendo un 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 = [];
Para crear estos arreglos, mapearemos sobre las filas y luego sobre su
contenido. Recuerda que map pasa el índice del arreglo como un segundo
argumento a la función de mapeo, que nos dice las coordenadas x y las
coordenadas y de un caracter dado. Las posiciones en el juego serán
guardadas como pares de coordenadas, con la izquierda superior siendo 0,0
y cada cuadrado del fondo siendo 1 unidad de alto y ancho.
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");
}
}
Actor s
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.
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);
}
}
The times method scales a vector by a given number. It will be useful when
we need to multiply a speed vector by a time interval to get the distance
traveled during that time.
The different types of actors get their own classes since their behavior is
very different. Let’s define these classes. We’ll get to their update methods
later.
The player class has a property speed that stores its current speed to
simulate momentum and gravity.
class Player {
constructor(pos, speed) {
this.pos = pos;
this.speed = speed;
}
static create(pos) {
return new Player(pos.plus(new Vector(0, -0.5)),
new Vector(0, 0));
}
}
The size property is the same for all instances of Player , so we store it on
the prototype rather than on the instances themselves. We could have used a
getter like type , but that would create and return a new Vector object every
time the property is read, which would be wasteful. (Strings, being
immutable, don’t have to be re-created every time they are evaluated.)
The create method looks at the character that the Level constructor passes
and creates the appropriate lava actor.
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).
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);
}
}
To avoid a situation where all coins move up and down synchronously, the
starting phase of each coin is randomized. The phase of Math.sin ’s wave,
the width of a wave it produces, is 2π. We multiply the value returned by
Math.random by that number to give the coin a random starting position on
the wave.
We can now define the levelChars object that maps plan characters to
either background grid types or actor classes.
const levelChars = {
".": "empty", "#": "wall", "+": "lava",
"@": Player, "o": Coin,
"=": Lava, "|": Lava, "v": Lava
};
That gives us all the parts needed to create a Level instance.
The task ahead is to display such levels on the screen and to model time and
motion inside them.
E n c a p s u l at i o n a s a b u r d e n
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 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.
D r aw i n g
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 create them, but
that would produce more verbose programs.
clear() { this.dom.remove(); }
}
The level’s background grid, which never changes, is drawn once. 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.
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})))
));
}
The following CSS makes the table look like the background we want:
The background rule sets the background color. CSS allows colors to be
specified both as words ( white ) or with a format such as rgb(R, G, B) ,
where the red, green, and blue components of the color are separated into
three numbers from 0 to 255. So, in rgb(52, 166, 251) , the red component
is 52, green is 166, and blue is 251. Since the blue component is the largest,
the resulting color will be bluish. You can see that in the .lava rule, the
first number (red) is the largest.
We draw each actor by creating a DOM element for it and setting that
element’s position and size based on the actor’s properties. The values have
to be multiplied by scale to go from game units to pixels.
function drawActors(actors) {
return elt("div", {}, ...actors.map(actor => {
let rect = elt("div", {class: `actor ${actor.type}`});
rect.style.width = `${actor.size.x * scale}px`;
rect.style.height = `${actor.size.y * scale}px`;
rect.style.left = `${actor.pos.x * scale}px`;
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.
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);
this.dom.appendChild(this.actorLayer);
this.dom.className = `game ${state.status}`;
this.scrollPlayerIntoView(state);
};
By adding the level’s current status as a class name to the wrapper, we can
style the player actor slightly differently when the game is won or lost by
adding a CSS rule that takes effect only when the player has an ancestor
element with a given class.
.lost .player {
background: rgb(160, 64, 64);
}
.won .player {
box-shadow: -4px -7px 8px white, 4px -7px 8px white;
}
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 element 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 element’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;
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 outside 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.
<script>
let simpleLevel = new Level(simpleLevelPlan);
let display = new DOMDisplay(document.body, simpleLevel);
display.syncState(State.start(simpleLevel));
</script>
The <link> tag, when used with rel="stylesheet" , is a way to load a CSS
file into a page. The file game.css contains the styles necessary for our
game.
Now we’re at the point where we can start adding motion—the most
interesting aspect of the game. The basic approach, taken by most games
like this, is to split time into small steps and, for each step, move the actors
by a distance corresponding to their speed multiplied by the size of the time
step. We’ll measure time in seconds, so speeds are expressed in units per
second.
Moving things is easy. The difficult part is dealing with the interactions
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 physical 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 collision 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.
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 coordinates
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.
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 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.
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");
};
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);
};
A c t o r u p d at e s
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.
This update method computes a new position by adding the product of the
time step and the current speed to its old position. If no obstacle 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.
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);
};
That leaves the player itself. Player motion is handled separately per axis
because hitting the floor should not prevent horizontal motion, and hitting a
wall should not stop falling or jumping motion.
const playerXSpeed = 7;
const gravity = 30;
const jumpSpeed = 17;
The horizontal motion is computed based on the state of the left and right
arrow keys. When there’s no wall blocking the new position created by this
motion, it is used. Otherwise, the old position is kept.
Vertical motion works in a similar way but has to simulate jumping and
gravity. The player’s vertical speed ( ySpeed ) is first accelerated to 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 constants in
this game have been set by trial and error. I tested values until I found a
combination I liked.
Tr acking keys
For a game like this, we do not want keys to take effect once per keypress.
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);
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" ).
Ru n n i n g t h e g a m e
Let’s define a helper function that wraps those boring parts in a convenient
interface and allows us to simply call runAnimation , giving 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);
}
I have set a maximum frame step of 100 milliseconds (one-tenth of a
second). When the browser tab or window with our page is hidden,
requestAnimationFrame calls will be suspended until the tab or window is
shown again. In this case, the difference between lastTime and time will
be the entire time in which the page was hidden. Advancing the game by
that much in a single step would look silly and might cause weird side
effects, such as the player falling through the floor.
The function also converts the time steps to seconds, which are an easier
quantity to think about than milliseconds.
The runLevel function takes a Level object and a display constructor and
returns a promise. It displays the level (in document.body ) and 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.
A game is a sequence of levels. Whenever the player dies, the current level
is restarted. When a level is completed, we move on to the next level. This
can be expressed by the following function, which takes an array of level
plans (strings) and a display constructor:
<body>
<script>
runGame(GAME_LEVELS, DOMDisplay);
</script>
</body>
Ex ercises
Game ov er
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.
Pa u s i n g t h e g a m e
Make it possible to pause (suspend) and unpause the game by pressing the
Esc key.
The runAnimation interface may not look like it is suitable for this at first
glance, but it is if you rearrange the way runLevel calls it.
When you have that working, there is something else you could try. The
way we have been registering keyboard event handlers is somewhat
problematic. The arrowKeys object is currently a global binding, and its
event handlers are kept around even when no game is running. You could
say they leak out of our system. Extend trackKeys to provide a way to
unregister its handlers and then change runLevel to register its handlers
when it starts and unregister them again when it is finished.
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.
Chapter 16
Node . j s
“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
Ante cedentes
El c omand o Node
$ node hello.js
Hello world
$ node
> 1 + 1
2
> [-1, -2, -3].map(Math.abs)
[1, 2, 3]
> process.exit(0)
$
La referencia process , al igual que la referencia console , está disponible
globalmente en Node. Esta proporciona varias formas de inspeccionar y
manipular el programa actual. El método exit finaliza el proceso y se le
puede pasar un código de estado de salida, esto le dice al programa que
inició node (en este caso, el shell de la línea de comandos) si el programa
finalizó con éxito (código cero) o si se encontró con un error (cualquier otro
código).
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.
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.
console.log(reverse(argument));
El archivo reverse.js define una biblioteca para invertir cadenas, el cual
puede ser utilizado tanto por la herramienta de línea de comandos como por
otros scripts que necesiten acceso directo a una función de inversión de
cadenas.
exports.reverse = function(string) {
return Array.from(string).reverse().join("");
};
I n s ta l a c i ó n c o n N P M
$ node
> const {parse} = require("ini");
> parse("x = 1\ny = 2");
{ x: '1', y: '2' }
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.
A r c h i v o s d e pa q u e t e s
{
"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"
},
"license": "ISC"
}
Versiones
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 buscar
paquetes.
El módu l o del si st e m a de a rc hi vo s
Aunque las promesas han sido parte de JavaScript por un tiempo, hasta el
momento de escribir este documento su integración en Node.js es aun un
trabajo en progreso. Hay un objeto promises exportado del paquete fs
desde la versión 10.1 que contiene la mayoría de las mismas funciones que
fs pero que utiliza promesas en lugar de funciones de devolución de
llamada.
The HT TP module
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.
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
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 connections
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.
The first argument to request configures the request, telling Node what
server to talk to, what path to request from that server, which method to use,
and so on. The second argument is the function that should be called when a
response comes in. It is given an object that allows us to inspect the
response, for example to find out its status code.
Just like the response object we saw in the server, the object returned by
request allows us to stream data into the request with the write method
and finish the request with the end method. The example does not use
write because GET requests should not contain data in their request body.
There’s a similar request function in the https module that can be used to
make requests to https: URLs.
Making requests with Node’s raw functionality is rather verbose. There are
much more convenient wrapper packages available on NPM. For example,
node-fetch provides the promise-based fetch interface that we know from
the browser.
Streams
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.
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 every
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 immediately
processed, even when the whole document isn’t available yet. A file can be
read as a readable stream by using the createReadStream function from fs .
This code creates a server that reads request bodies and streams them back
to the client as all-uppercase text:
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:
A f i l e s e rv e r
Let’s combine our newfound knowledge about HTTP servers and working
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 interpret
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 return a
promise that resolves to an object that describes the response.
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.
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.
function urlPath(url) {
let {pathname} = parse(url);
let path = resolve(decodeURIComponent(pathname).slice(1));
if (path != baseDirectory &&
!path.startsWith(baseDirectory + sep)) {
throw {status: 403, body: "Forbidden"};
}
return path;
}
To avoid such problems, urlPath uses the resolve function from the path
module, which resolves relative paths. It then verifies that the result is
below the working directory. The process.cwd function (where cwd stands
for “current working directory”) can be used to find this working directory.
The sep binding from the path package is the system’s path separator—a
backslash on Windows and a forward slash on most other systems. When
the path doesn’t start with the base directory, the function throws an error
response object, using the HTTP status code indicating that access to the
resource is forbidden.
We’ll set up the GET method to return a list of files when reading a directory
and to return the file’s content when reading a regular file.
The following npm command, in the directory where the server script lives,
installs a specific version of mime :
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.
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.
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 .
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.
S u m m a ry
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 explicitly
use a synchronous variant of a function, such as readFileSync . When
calling such asynchronous functions, you provide callback functions, and
Node will call them with an error value and (if available) a result when it is
ready.
Ex ercises
Search to ol
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 subdirectories.
D i r e c t o r y c r e at i o n
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.
A p u b l i c s pa c e o n t h e w e b
Since the file server serves up any kind of file and even includes the right
Content-Type header, you can use it to serve a website. Since it allows
everybody to delete and replace files, it would be an interesting kind of
website: one that can be modified, improved, and vandalized by everybody
who takes the time to create the right HTTP request.
Write a basic HTML page that includes a simple JavaScript file. Put the
files in a directory served by the file server and open them in your browser.
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.
Chapter 18
P r oy e c t o : S i t i o w e b pa r a
c o m pa r t i r h a b i l i d a d e s
Diseño
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 inmediatamente 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 qué clientes están viendo un sitio
web.
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 cuando no
hay nada nuevo que reportar.
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 abiertas.
Node, que hace fácil de manejar muchas conexiones sin crear un hilo de
control para cada una, es un buen elemento para nuestro sistema.
H T T P i n t e r fa c e
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.
Since talk titles may contain spaces and other characters that may not
appear normally in a URL, title strings must be encoded with the
encodeURIComponent function when building up such a URL.
A request to create a talk about idling might look something like this:
{"presenter": "Maureen",
"summary": "Standing still on a unicycle"}
Such URLs also support GET requests to retrieve the JSON representation of
a talk and DELETE requests to delete a talk.
{"author": "Iman",
"message": "Will you talk about raising a cycle?"}
To support long polling, GET requests to /talks may include extra headers
that inform the server to delay the response if no new information is
available. We’ll use a pair of headers normally intended to manage caching:
ETag and If-None-Match .
Servers may include an ETag (“entity tag”) header in a response. Its value is
a string that identifies the current version of the resource. Clients, when
they later request that resource again, may make a conditional request by
including an If-None-Match header whose value holds that same string. If
the resource hasn’t changed, the server will respond 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"
Content-Length: 295
[....]
The protocol described here does not do any access control. Everybody can
comment, modify talks, and even delete them. (Since the Internet is full of
hooligans, putting such a system online without further protection probably
wouldn’t end well.)
T h e s e rv e r
Let’s start by building the server-side part of the program. The code in this
section runs on Node.js.
Ro u t i ng
Our server will use createServer to start an HTTP server. In the function
that handles a new request, we must distinguish between the various 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.
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:
The module exports the Router class. A router object allows new 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.
S e rv i n g f i l e s
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 requests that we should handle specially,
though, so we wrap it in another function.
class SkillShareServer {
constructor(talks) {
this.talks = talks;
this.version = 0;
this.waiting = [];
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.
Ta l k s a s r e s o u r c e s
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.
The updated method, which we will define later, notifies waiting long
polling requests about the change.
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") {
return {status: 400, body: "Bad talk data"};
}
server.talks[title] = {title,
presenter: talk.presenter,
summary: talk.summary,
comments: []};
server.updated();
return {status: 204};
});
Adding a comment to a talk works similarly. We use readStream to get the
content of the request, validate the resulting data, and store it as a comment
when it looks valid.
router.add("POST", /^\/talks\/([^\/]+)\/comments$/,
async (server, title, request) => {
let requestBody = await readStream(request);
let comment;
try { comment = JSON.parse(requestBody); }
catch (_) { return {status: 400, body: "Invalid JSON"}; }
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};
} else {
return {status: 404, body: `No talk '${title}' found`};
}
});
The most interesting aspect of the server is the part that handles long
polling. When a GET request comes in for /talks , it may be either a regular
request or a long polling request.
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.
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) {
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);
});
};
SkillShareServer.prototype.updated = function() {
this.version++;
let response = this.talkResponse();
this.waiting.forEach(resolve => resolve(response));
this.waiting = [];
};
new SkillShareServer(Object.create(null)).start(8000);
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.
<!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.
At the bottom, it adds a heading at the top of the page and loads the script
that contains the client-side application.
Act ions
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 requests. 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.
We’ll store the user’s name in localStorage so that it can be restored when
the page is loaded.
The actions that need to involve the server make network requests, using
fetch , to the HTTP interface described earlier. We use a wrapper function,
fetchOK , which makes sure the returned promise is rejected when the server
returns an error code.
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 c omponents
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:
The elt function used to construct DOM elements is the one we used in
Chapter ?.
The "submit" event handler calls form.reset to clear the form’s content
after creating a "newComment" action.
function renderComment(comment) {
return elt("p", {className: "comment"},
elt("strong", null, comment.author),
": ", comment.message);
}
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.
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 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.
T h e a p p l i c at i o n
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;
}
}
}
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.
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();
If you run the server and open two browser windows for
http://localhost:8000 next to each other, you can see that the actions you
perform in one window are immediately visible in the other.
Ex ercises
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 .
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 comments are
lost.
Extend the server so that it stores the talk data to disk and automatically
reloads the data when it is restarted. Do not worry about efficiency—do the
simplest thing that works.
The wholesale redrawing of talks works pretty well because you usually
can’t tell the difference between a DOM node and its identical replacement.
But there are exceptions. If you start typing something in the comment field
for a talk in one browser window and then, in another, add a comment to
that talk, the field in the first window will be redrawn, removing both its
content and its focus.
E x erc i se Hi n t s
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.
Ciclo de un triángulo
FizzBuzz
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 || .
Ta b l e r o d e a j e d r e z
Funciones
Mínimo
Recursión
Conteo de frijoles
E s t r u c t u r a s d e Dat o s : O b j e t o s y A r r ay s
La suma de un r ango
Dado que el límite final es inclusivo, deberias usar el operador <= en lugar
de < para verificar el final de tu ciclo.
R e v i r t i e n d o u n a r r ay
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--) .
U n a l i s ta
Crear una lista es más fácil cuando se hace de atrás hacia adelante.
Entonces 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 construyó 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:
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.
C o m pa r a c i ó n p r o f u n d a
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 === .
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 Morgan,
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.
L a V i d a S e c r e ta d e l o s O b j e t o s
Un t ip o ve ctor
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 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.
T o m a n d o u n m é t o d o p r e s ta d o
Y que puedes llamar a una función con una vinculación this específica al
usar su método call .
P roy e c t o : Un Ro b o t
Midiend o un rob ot
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.
Una posible solución sería calcular rutas para todos los paquetes, y luego
tomar la más corta. Se pueden obtener incluso mejores resultados, si hay
múltiples rutas más cortas, al ir prefiriendo las que van a recoger un paquete
en lugar de entregar un paquete.
Conjunto persistente
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.
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
R e i n t e n ta r
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).
La caja bloqueada
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.
Números ot r a vez
Módulos
Un rob ot modul ar
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 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.
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 .
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 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.
Módulo de Caminos
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 nuevamente.
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á).
P roy e c t o : Un L e n g ua j e d e P ro gr a m ac i ó n
A r r ay s
The easiest way to do this is to represent Egg arrays with JavaScript arrays.
The values added to the top scope must be functions. By using a rest
argument (with triple-dot notation), the definition of array can be very
simple.
Closure
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 implementing closure (though
to compile it in a way that is actually efficient, you’d need to do some more
work).
Comments
Fixing sc ope
You will have to loop through one scope at a time, using Object.
getPrototypeOf to go the next outer scope. For each scope, use
hasOwnProperty 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.
C o n s t r u y e u n a ta b l a
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.
E l e m e n t o s p o r n o m b r e d e ta g
E l s o m b r e r o d e l g at o
Manejo de Eventos
Gl ob o
Mouse trail
P e s ta ñ a s
Puedes comenzar creando una colección de pestañas para que tengas 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.
P r o y e c t o : U n J u e g o d e P l ata f o r m a
Pa u s i n g t h e g a m e
So we need to communicate the fact that we are pausing the game to the
function given to runAnimation . For that, you can use a binding that both
the event handler and that function have access to.
A monster
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 to ol
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 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.
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.
D i r e c t o r y c r e at i o n
You can use the function that implements the DELETE method as a blueprint
for the MKCOL method. When no file is found, try to create a directory with
mkdir . When a directory exists at that path, you can return a 204 response
so that directory creation requests are idempotent. If a nondirectory file
exists here, return an error code. Code 400 (“bad request”) would be
appropriate.
A p u b l i c s pa c e o n t h e w e b
You can create a <textarea> element to hold the content of the file that is
being edited. A GET request, using fetch , can retrieve the current 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.
P r o y e c t o : S i t i o w e b pa r a c o m pa r t i r
habilidades
The simplest solution I can come up with is to encode the whole talks
object as JSON and dump it to a file with writeFile . There is already a
method ( updated ) that is called every time the server’s data changes. It can
be extended to write the new data to disk.
Pick a filename, for example ./talks.json . When the server starts, it can
try to read that file with readFile , and if that succeeds, the server can use
the file’s contents as its starting data.
The best way to do this is probably to make talks component objects, with a
syncState method, so that they can be updated to show a modified version
of the talk. During normal operation, the only way a talk can be changed is
by adding more comments, so the syncState method can be relatively
simple.
The difficult part is that, when a changed list of talks comes in, we have to
reconcile the existing list of DOM components with the talks on the new list
—deleting components whose talk was deleted and updating components
whose talk changed.
To do this, it might be helpful to keep a data structure that stores the talk
components under the talk titles so that you can easily figure out whether a
component exists for a given talk. You can then loop over the new array of
talks, and for each of them, either synchronize an existing component or
create a new one. To delete components for deleted talks, you’ll have to
also loop over the components and check whether the corresponding talks
still exist.
This le was downloaded from Z-Library project
Z-Access
https://wikipedia.org/wiki/Z-Library
ffi
fi