Javascript (JS) : La Etiqueta "Script"
Javascript (JS) : La Etiqueta "Script"
Fundamentos de JavaScript
¡Hola, mundo!
¡Hola, mundo!
Esta parte del tutorial trata sobre el núcleo de JavaScript, el lenguaje en sí.
Pero necesitamos un entorno de trabajo para ejecutar nuestros scripts y, dado que este libro
está en línea, el navegador es una buena opción. Mantendremos la cantidad de comandos
específicos del navegador (como alert ) al mínimo para que no pases tiempo en ellos si planeas
concentrarte en otro entorno (como Node.js). Nos centraremos en JavaScript en el navegador en
la siguiente parte del tutorial.
Primero, veamos cómo adjuntamos un script a una página web. Para entornos del lado del
servidor (como Node.js), puedes ejecutar el script con un comando como "node my.js" .
La etiqueta “script”
Los programas de JavaScript se pueden insertar en casi cualquier parte de un documento HTML
con el uso de la etiqueta <script> .
Por ejemplo:
<!DOCTYPE HTML>
<html>
<body>
<script>
alert( '¡Hola, mundo!' );
</script>
</body>
</html>
Puedes ejecutar el ejemplo haciendo clic en el botón “Play” en la esquina superior derecha
del cuadro de arriba.
Marcado moderno
JavaScript (JS) 1
La etiqueta <script> tiene algunos atributos que rara vez se usan en la actualidad, pero aún se
pueden encontrar en código antiguo:
El atributo type : <script type =…> El antiguo estándar HTML, HTML4, requería que un script tuviera
un type . Por lo general, era type="text/javascript" . Ya no es necesario. Además, el estándar HTML
moderno cambió totalmente el significado de este atributo. Ahora, se puede utilizar para
módulos de JavaScript. Pero eso es un tema avanzado, hablaremos sobre módulos en otra parte
del tutorial. El atributo language : <script language =…> Este atributo estaba destinado a mostrar el
lenguaje del script. Este atributo ya no tiene sentido porque JavaScript es el lenguaje
predeterminado. No hay necesidad de usarlo. Comentarios antes y después de los scripts. En
libros y guías muy antiguos, puedes encontrar comentarios dentro de las etiquetas <script> ,
como el siguiente:
<script type="text/javascript"><!--
...
//--></script>
Scripts externos
Si tenemos un montón de código JavaScript, podemos ponerlo en un archivo separado.
<script src="/path/to/script.js"></script>
Aquí, /path/to/script.js es una ruta absoluta al archivo de script desde la raíz del sitio.
También se puede proporcionar una ruta relativa desde la página actual. Por
ejemplo, src="script.js" significaría un archivo "script.js" en la carpeta actual.
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js"></script>
<script src="/js/script1.js"></script>
<script src="/js/script2.js"></script>
Podemos usar una etiqueta <script> para agregar código JavaScript a una página.
Hay mucho más que aprender sobre los scripts del navegador y su interacción con la página
web. Pero tengamos en cuenta que esta parte del tutorial está dedicada al lenguaje
JavaScript, por lo que no debemos distraernos con implementaciones específicas del navegador.
Usaremos el navegador como una forma de ejecutar JavaScript, lo cual es muy conveniente para
la lectura en línea, pero es solo una de muchas.
JavaScript (JS) 2
Sentencias
Las sentencias son construcciones sintácticas y comandos que realizan acciones.
Ya hemos visto una sentencia, alert('¡Hola mundo!') , que muestra el mensaje “¡Hola mundo!”.
Podemos tener tantas sentencias en nuestro código como queramos, las cuales se pueden separar
con un punto y coma.
alert('Hola'); alert('Mundo');
Generalmente, las sentencias se escriben en líneas separadas para hacer que el código sea más
legible:
alert('Hola');
alert('Mundo');
Punto y coma
Se puede omitir un punto y coma en la mayoría de los casos cuando existe un salto de línea.
Esto también funcionaría:
alert('Hola')
alert('Mundo')
Aquí, JavaScript interpreta el salto de línea como un punto y coma “implícito”. Esto se
denomina inserción automática de punto y coma.
En la mayoría de los casos, una nueva línea implica un punto y coma. Pero “en la mayoría de
los casos” no significa “siempre”!
Hay casos en que una nueva línea no significa un punto y coma. Por ejemplo:
alert(3 +
1
+ 2);
Pero hay situaciones en las que JavaScript “falla” al asumir un punto y coma donde realmente
se necesita.
Los errores que ocurren en tales casos son bastante difíciles de encontrar y corregir.
Un ejemplo de error
Si tienes curiosidad por ver un ejemplo concreto de tal error, mira este código:
alert("Hello");
[1, 2].forEach(alert);
JavaScript (JS) 3
alert("Hello")
[1, 2].forEach(alert);
Esta vez, si ejecutamos el código, solo se ve el primer Hello (y un error pero necesitas abrir
la consola para verlo). Los números no aparecen más.
Esto ocurre porque JavaScript no asume un punto y coma antes de los corchetes [...] , entonces
el código del primer ejemplo se trata como una sola sentencia.
alert("Hello")[1, 2].forEach(alert);
Se ve extraño, ¿verdad? Tal unión en este caso es simplemente incorrecta. Necesitamos poner
un punto y coma después del alert para que el código funcione bien.
Comentarios
A medida que pasa el tiempo, los programas se vuelven cada vez más complejos. Se hace
necesario agregar comentarios que describan lo que hace el código y por qué.
El resto de la línea es un comentario. Puede ocupar una línea completa propia o seguir una
sentencia.
Como aquí:
Los comentarios de varias líneas comienzan con una barra inclinada y un asterisco /* y
terminan con un asterisco y una barra inclinada */ .
Como aquí:
El contenido de los comentarios se ignora, por lo que si colocamos el código dentro de /* … */ ,
no se ejecutará.
A veces puede ser útil deshabilitar temporalmente una parte del código:
/* Comentando el código
alert('Hola');
JavaScript (JS) 4
*/
alert('Mundo');
/*
/* comentario anidado ?!? */
*/
alert( 'Mundo' );
Los comentarios aumentan el tamaño general del código, pero eso no es un problema en
absoluto. Hay muchas herramientas que minimizan el código antes de publicarlo en un servidor
de producción. Eliminan los comentarios, por lo que no aparecen en los scripts de trabajo.
Por lo tanto, los comentarios no tienen ningún efecto negativo en la producción.
“use strict”
La directiva se asemeja a un string: "use strict" . Cuando se sitúa al principio de un script, el
script entero funciona de la manera “moderna”.
Por ejemplo:
"use strict";
Aprenderemos funciones (una manera de agrupar comandos) en breve, pero adelantemos que "use
strict" se puede poner al inicio de una función. De esta manera, se activa el modo estricto
JavaScript (JS) 5
El modo estricto no está activado aquí:
alert("algo de código");
// la directiva "use strict" de abajo es ignorada, tiene que estar al principio
"use strict";
No hay ninguna directiva del tipo "no use strict" que haga al motor volver al comportamiento
anterior.
Una vez entramos en modo estricto, no hay vuelta atrás.
Si esto no funciona, como en los viejos navegadores, hay una fea pero confiable manera de
asegurar use strict . Ponlo dentro de esta especie de envoltura:
(function() {
'use strict';
// ...tu código...
})()
Entonces, por ahora "use strict"; es un invitado bienvenido al tope de tus scripts. Luego,
cuando tu código sea todo clases y módulos, puedes omitirlo.
A partir de ahora tenemos que saber acerca de use strict en general.
En los siguientes capítulos, a medida que aprendamos características del lenguaje, veremos
las diferencias entre el modo estricto y el antiguo. Afortunadamente no hay muchas y
realmente hacen nuestra vida mejor.
Todos los ejemplos en este tutorial asumen modo estricto salvo que (muy raramente) se
especifique lo contrario.
JavaScript (JS) 6
Variables
La mayoría del tiempo, una aplicación de JavaScript necesita trabajar con información. Aquí
hay 2 ejemplos:
1. Una tienda en línea – La información puede incluir los bienes a la venta y un “carrito de
compras”.
2. Una aplicación de chat – La información puede incluir los usuarios, mensajes, y mucho más.
Una variable
Una variable es un “almacén con un nombre” para guardar datos. Podemos usar variables para
almacenar golosinas, visitantes, y otros datos.
let message;
let message;
La cadena ahora está almacenada en el área de la memoria asociada con la variable. La podemos
acceder utilizando el nombre de la variable:
let message;
message = 'Hola!';
Para ser concisos, podemos combinar la declaración de la variable y su asignación en una sola
línea:
alert(message); // Hola!
Esto puede parecer más corto, pero no lo recomendamos. Por el bien de la legibilidad, por
favor utiliza una línea por variable.
La versión de líneas múltiples es un poco más larga, pero se lee más fácil:
JavaScript (JS) 7
let user = 'John',
age = 25,
message = 'Hola';
Técnicamente, todas estas variantes hacen lo mismo. Así que, es cuestión de gusto personal y
preferencia estética.
var en vez de let
En scripts más viejos, a veces se encuentra otra palabra clave: var en lugar de let :
La palabra clave var es casi lo mismo que let . También hace la declaración de una variable,
aunque de un modo ligeramente distinto, y más antiguo.
Existen sutiles diferencias entre let y var , pero no nos interesan en este momento.
Cubriremos el tema a detalle en el capítulo La vieja "var".
Por ejemplo, podemos imaginar la variable message como una caja etiquetada "message" con el
valor "Hola!" adentro:
Podemos introducir cualquier valor a la caja.
También la podemos cambiar cuantas veces queramos:
let message;
message = 'Hola!';
alert(message);
Cuando el valor ha sido alterado, los datos antiguos serán removidos de la variable:
También podemos declarar dos variables y copiar datos de una a la otra.
let message;
JavaScript (JS) 8
// 'let' repetidos lleva a un error
let message = "That"; // SyntaxError: 'message' ya fue declarado
Debemos declarar una variable una sola vez y desde entonces referirnos a ella sin let .
Lenguajes funcionales
Aunque a primera vista puede parecer un poco extraño, estos lenguajes son muy capaces de
desarrollo serio. Más aún: existen áreas, como la computación en paralelo, en las cuales esta
limitación otorga ciertas ventajas.
Nombramiento de variables
Existen dos limitaciones de nombre de variables en JavaScript:
let userName;
let test123;
Es interesante notar que el símbolo del dólar '$' y el guion bajo '_' también se utilizan en
nombres. Son símbolos comunes, tal como las letras, sin ningún significado especial.
Los siguientes nombres son válidos:
alert($ + _); // 3
La Capitalización es Importante
Dos variables con nombres manzana y MANZANA son variables distintas.
Las letras que no son del alfabeto inglés están permitidas, pero no se recomiendan
Es posible utilizar letras de cualquier alfabeto, incluyendo letras del cirílico, logogramas
chinos, etc.:
Técnicamente, no existe ningún error aquí. Tales nombres están permitidos, pero existe una
tradición internacional de utilizar inglés en el nombramiento de variables. Incluso si
JavaScript (JS) 9
estamos escribiendo un script pequeño, este puede tener una larga vida por delante. Puede ser
necesario que gente de otros países deba leerlo en algún momento.
Nombres reservados
Hay una lista de palabras reservadas, las cuales no pueden ser utilizadas como nombre de
variable porque el lenguaje en sí las utiliza.
Normalmente, debemos definir una variable antes de utilizarla. Pero, en los viejos tiempos,
era técnicamente posible crear una variable simplemente asignando un valor sin utilizar
‘let’. Esto aún funciona si no ponemos ‘use strict’ en nuestros scripts para mantener la
compatibilidad con scripts antiguos.
alert(num); // 5
"use strict";
Constantes
Para declarar una variable constante (inmutable) use const en vez de let :
Las variables declaradas utilizando const se llaman “constantes”. No pueden ser alteradas. Al
intentarlo causaría un error:
Cuando un programador está seguro de que una variable nunca cambiará, puede declarar la
variable con const para garantizar y comunicar claramente este hecho a todos.
Constantes mayúsculas
Existe una práctica utilizada ampliamente de utilizar constantes como aliases de valores
difíciles-de-recordar y que se conocen previo a la ejecución.
Tales constantes se nombran utilizando letras mayúsculas y guiones bajos.
Por ejemplo, creemos constantes para los colores en el formato “web” (hexadecimal):
JavaScript (JS) 10
let color = COLOR_ORANGE;
alert(color); // #FF7F00
Ventajas:
¿Cuándo se deben utilizar letras mayúsculas para una constante, y cuando se debe nombrarla de
manera normal? Dejémoslo claro.
Ser una “constante” solo significa que el valor de la variable nunca cambia. Pero hay
constantes que son conocidas previo a la ejecución (como el valor hexadecimal del color rojo)
y hay constantes que son calculadas en el tiempo de ejecución, pero no cambian después de su
asignación inicial.
Por ejemplo:
const pageLoadTime = /* el tiempo que tardó la página web para cargar */;
El valor de pageLoadTime no se conoce antes de cargar la página, así que la nombramos
normalmente. No obstante, es una constante porque no cambia después de su asignación inicial.
En otras palabras, las constantes con nombres en mayúscula son utilizadas solamente como
alias para valores invariables y preestablecidos (“hard-coded”).
En un proyecto real, la mayor parte de el tiempo se pasa modificando y extendiendo una base
de código en vez de empezar a escribir algo desde cero. Cuando regresamos a algún código
después de hacer algo distinto por un rato, es mucho más fácil encontrar información que está
bien etiquetada. O, en otras palabras, cuando las variables tienen nombres adecuados.
Por favor pasa tiempo pensando en el nombre adecuado para una variable antes de declararla.
Hacer esto te da un retorno muy sustancial.
Evite abreviaciones o nombres cortos a , b , c , al menos que en serio sepa lo que está
haciendo.
Cree nombres que describen al máximo lo que son y sean concisos. Ejemplos que no son
adecuados son data y value . Estos nombres no nos dicen nada. Estos solo está bien usarlos
en el contexto de un código que deje excepcionalmente obvio cuál valor o cuales datos está
referenciando la variable.
¿Suena simple? De hecho lo es, pero no es tan fácil crear nombres de variables descriptivos y
concisos a la hora de practicar. Inténtelo.
¿Reusar o crear?
JavaScript (JS) 11
Una última nota. Existen programadores haraganes que, en vez de declarar una variable nueva,
tienden a reusar las existentes.
El resultado de esto es que sus variables son como cajas en las cuales la gente introduce
cosas distintas sin cambiar sus etiquetas. ¿Que existe dentro de la caja? ¿Quién sabe?
Necesitamos acercarnos y revisar.
Dichos programadores se ahorran un poco durante la declaración de la variable, pero pierden
diez veces más a la hora de depuración.
Resumen
Podemos declarar variables para almacenar datos al utilizar las palabra clave var , let ,
o const .
absoluto. Cubriremos sus sutiles diferencias con let en el capítulo La vieja "var", por si
lo necesitaras.
Las variables deben ser nombradas de tal manera que entendamos fácilmente lo que está en su
interior.
Tipos de datos
Hay ocho tipos de datos básicos en JavaScript. En este capítulo los cubriremos en general y
en los próximos hablaremos de cada uno de ellos en detalle.
Podemos almacenar un valor de cualquier tipo dentro de una variable. Por ejemplo, una
variable puede contener en un momento un string y luego almacenar un número:
// no hay error
let message = "hola";
message = 123456;
Los lenguajes de programación que permiten estas cosas, como JavaScript, se denominan
“dinámicamente tipados”, lo que significa que allí hay tipos de datos, pero las variables no
están vinculadas rígidamente a ninguno de ellos.
Number
let n = 123;
n = 12.345;
Hay muchas operaciones para números. Por ejemplo, multiplicación * , división / , suma + ,
resta - , y demás.
Además de los números comunes, existen los llamados “valores numéricos especiales” que
también pertenecen a este tipo de datos: Infinity , -Infinity y NaN .
JavaScript (JS) 12
Infinity representa el Infinito matemático ∞. Es un valor especial que es mayor que
cualquier número.
alert( 1 / 0 ); // Infinity
NaN es “pegajoso”. Cualquier otra operación sobre NaN devuelve NaN :
Por lo tanto, si hay un NaN en alguna parte de una expresión matemática, se propaga a todo el
resultado (con una única excepción: NaN ** 0 es 1 ).
Las operaciones matemáticas son seguras
Hacer matemáticas es “seguro” en JavaScript. Podemos hacer cualquier cosa: dividir por cero,
tratar las cadenas no numéricas como números, etc.
El script nunca se detendrá con un error fatal (“morir”). En el peor de los casos,
obtendremos NaN como resultado.
Los valores numéricos especiales pertenecen formalmente al tipo “número”. Por supuesto que no
son números en el sentido estricto de la palabra.
BigInt
En JavaScript, el tipo “number” no puede representar de forma segura valores enteros mayores
que (2 53 -1) (eso es 9007199254740991 ), o menor que -(2 53 -1) para negativos.
Para ser realmente precisos, el tipo de dato “number” puede almacenar enteros muy grandes
(hasta 1.7976931348623157 * 10 308 ), pero fuera del rango de enteros seguros ±(2 53 -1) habrá un error
de precisión, porque no todos los dígitos caben en el almacén fijo de 64-bit. Así que es
posible que se almacene un valor “aproximado”.
Por ejemplo, estos dos números (justo por encima del rango seguro) son iguales:
Podemos decir que ningún entero impar mayor que (2 53 -1) puede almacenarse en el tipo de dato
“number”.
Para la mayoría de los propósitos, el rango ±(2 53 -1) es suficiente, pero a veces necesitamos
números realmente grandes; por ejemplo, para criptografía o marcas de tiempo de precisión de
microsegundos.
BigInt se agregó recientemente al lenguaje para representar enteros de longitud arbitraria.
JavaScript (JS) 13
// la "n" al final significa que es un BigInt
const bigInt = 1234567890123456789012345678901234567890n;
Como los números BigInt rara vez se necesitan, no los cubrimos aquí sino que les dedicamos un
capítulo separado <info: bigint>. Léelo cuando necesites números tan grandes.
Problemas de compatibilidad
En este momento, BigInt está soportado por Firefox/Chrome/Edge/Safari, pero no por IE.
String
Un string en JavaScript es una cadena de caracteres y debe colocarse entre comillas.
Las comillas dobles y simples son comillas “sencillas” (es decir, funcionan igual). No hay
diferencia entre ellas en JavaScript.
Los backticks son comillas de “funcionalidad extendida”. Nos permiten incrustar variables y
expresiones en una cadena de caracteres encerrándolas en ${...} , por ejemplo:
La expresión dentro de ${...} se evalúa y el resultado pasa a formar parte de la cadena.
Podemos poner cualquier cosa ahí dentro: una variable como name , una expresión aritmética
como 1 + 2 , o algo más complejo.
Toma en cuenta que esto sólo se puede hacer con los backticks. ¡Las otras comillas no tienen
esta capacidad de incrustación!
alert( "el resultado es ${1 + 2}" ); // el resultado es ${1 + 2} (las comillas dobles no hacen nada)
No existe el tipo carácter
En algunos lenguajes, hay un tipo especial “carácter” para un solo carácter. Por ejemplo, en
el lenguaje C y en Java es char .
En JavaScript no existe tal tipo. Sólo hay un tipo: string . Un string puede estar formado por
un solo carácter, por ninguno, o por varios de ellos.
JavaScript (JS) 14
Este tipo se utiliza comúnmente para almacenar valores de sí/no: true significa “sí, correcto,
verdadero”, y false significa “no, incorrecto, falso”.
Por ejemplo:
En JavaScript, null no es una “referencia a un objeto inexistente” o un “puntero nulo” como en
otros lenguajes.
Es sólo un valor especial que representa “nada”, “vacío” o “valor desconocido”.
El código anterior indica que el valor de age es desconocido o está vacío por alguna razón.
let age;
alert(age); // "undefined"
…Pero no recomendamos hacer eso. Normalmente, usamos null para asignar un valor “vacío” o
“desconocido” a una variable, mientras undefined es un valor inicial reservado para cosas que
no han sido asignadas.
Object y Symbol
El tipo object (objeto) es especial.
Todos los demás tipos se llaman “primitivos” porque sus valores pueden contener una sola cosa
(ya sea una cadena, un número o lo que sea). Por el contrario, los objetos se utilizan para
almacenar colecciones de datos y entidades más complejas.
JavaScript (JS) 15
Siendo así de importantes, los objetos merecen un trato especial. Nos ocuparemos de ellos más
adelante en el capítulo Objetos después de aprender más sobre los primitivos.
El tipo symbol (símbolo) se utiliza para crear identificadores únicos para los objetos.
Tenemos que mencionarlo aquí para una mayor integridad, pero es mejor estudiar este tipo
después de los objetos.
El operador typeof
El operador typeof devuelve el tipo de dato del operando. Es útil cuando queremos procesar
valores de diferentes tipos de forma diferente o simplemente queremos hacer una comprobación
rápida.
typeof 0 // "number"
typeof Math // "object" (1)typeof null // "object" (2)typeof alert // "function" (3)
2. El resultado de typeof null es "object" . Esto está oficialmente reconocido como un error de
comportamiento de typeof que proviene de los primeros días de JavaScript y se mantiene por
compatibilidad. Definitivamente null no es un objeto. Es un valor especial con un tipo
propio separado.
3. El resultado de typeof alert es "function" porque alert es una función. Estudiaremos las
funciones en los próximos capítulos donde veremos que no hay ningún tipo especial
“function” en JavaScript. Las funciones pertenecen al tipo objeto. Pero typeof las trata de
manera diferente, devolviendo function . Además proviene de los primeros días de JavaScript.
Técnicamente dicho comportamiento es incorrecto, pero puede ser conveniente en la
práctica.
Se puede encontrar otra sintaxis en algún código: typeof(x) . Es lo mismo que typeof x .
Para ponerlo en claro: typeof es un operador, no una función. Los paréntesis aquí no son parte
del operador typeof . Son del tipo usado en agrupamiento matemático.
Usualmente, tales paréntesis contienen expresiones matemáticas tales como (2 + 2) , pero aquí
solo tienen un argumento (x) . Sintácticamente, permiten evitar el espacio entre el
operador typeof y su argumento, y a algunas personas les gusta así.
Algunos prefieren typeof(x) , aunque la sintaxis typeof x es mucho más común.
Resumen
Hay 8 tipos básicos en JavaScript.
number para números de cualquier tipo: enteros o de punto flotante, los enteros están
limitados por ±(2 53 1) .
JavaScript (JS) 16
string para cadenas. Una cadena puede tener cero o más caracteres, no hay un tipo
especial para un único carácter.
null para valores desconocidos – un tipo independiente que tiene un solo valor
nulo: null .
undefined para valores no asignados – un tipo independiente que tiene un único valor
“indefinido”: undefined .
El operador typeof nos permite ver qué tipo está almacenado en una variable.
Devuelve una cadena con el nombre del tipo. Por ejemplo "string" .
Como usaremos el navegador como nuestro entorno de demostración, veamos un par de funciones
para interactuar con el usuario: alert , prompt , y confirm .
alert
Ya la hemos visto. Muestra un mensaje y espera a que el usuario presione “Aceptar”.
Por ejemplo:
alert("Hello");
La mini ventana con el mensaje se llama * ventana modal *. La palabra “modal” significa que
el visitante no puede interactuar con el resto de la página, presionar otros botones, etc.,
hasta que se haya ocupado de la ventana. En este caso, hasta que presionen “OK”.
prompt
La función prompt acepta dos argumentos:
Muestra una ventana modal con un mensaje de texto, un campo de entrada para el visitante y
los botones OK/CANCELAR.
title El texto a mostrar al usuario. default Un segundo parámetro opcional, es el valor inicial
El usuario puede escribir algo en el campo de entrada de solicitud y presionar OK, así
obtenemos ese texto en result . O puede cancelar la entrada, con el botón “Cancelar” o
presionando la tecla Esc, de este modo se obtiene null en result .
La llamada a prompt retorna el texto del campo de entrada o null si la entrada fue cancelada.
Por ejemplo:
JavaScript (JS) 17
let age = prompt ('¿Cuántos años tienes?', 100);
Por lo tanto, para que las indicaciones se vean bien en IE, recomendamos siempre proporcionar
el segundo argumento:
confirm
La sintaxis:
result = confirm(pregunta);
La función confirm muestra una ventana modal con una pregunta y dos botones: OK y CANCELAR.
Resumen
Cubrimos 3 funciones específicas del navegador para interactuar con los usuarios:
alert muestra un mensaje. prompt muestra un mensaje pidiendo al usuario que introduzca un texto.
Retorna el texto o, si se hace clic en CANCELAR o se presiona Esc, retorna null . confirm muestra
un mensaje y espera a que el usuario pulse “OK” o “CANCELAR”. Retorna true si se presiona OK
y false si se presiona CANCEL/Esc.
Todos estos métodos son modales: detienen la ejecución del script y no permiten que el
usuario interactúe con el resto de la página hasta que la ventana se haya cerrado.
Ese es el precio de la simplicidad. Existen otras formas de mostrar ventanas más atractivas e
interactivas para el usuario, pero si la apariencia no importa mucho, estos métodos funcionan
bien.
Conversiones de Tipos
La mayoría de las veces, los operadores y funciones convierten automáticamente los valores
que se les pasan al tipo correcto. Esto es llamado “conversión de tipo”.
JavaScript (JS) 18
Por ejemplo, alert convierte automáticamente cualquier valor a string para mostrarlo. Las
operaciones matemáticas convierten los valores a números.
También hay casos donde necesitamos convertir de manera explícita un valor al tipo esperado.
Aún no hablamos de objetos
En este capítulo no hablamos de objetos. Por ahora, solamente veremos los valores primitivos.
ToString
La conversión a string ocurre cuando necesitamos la representación en forma de texto de un
valor.
Por ejemplo, alert(value) lo hace para mostrar el valor como texto.
También podemos llamar a la función String(value) para convertir un valor a string:
ToNumber
La conversión numérica ocurre automáticamente en funciones matemáticas y expresiones.
Podemos usar la función Number(value) para convertir de forma explícita un valor a un número:
La conversión explícita es requerida usualmente cuando leemos un valor desde una fuente
basada en texto, como lo son los campos de texto en los formularios, pero que esperamos que
contengan un valor numérico.
null 0
true and false 1 y 0
string
Se eliminan los espacios (incluye espacios, tabs \t , saltos de línea \n , etc.) al inicio y
JavaScript (JS) 19
final del texto. Si el string resultante es vacío, el resultado es 0 , en caso contrario el
número es “leído” del string. Un error devuelve NaN .
Ejemplos:
Ten en cuenta que null y undefined se comportan de distinta manera aquí: null se convierte
en 0 mientras que undefined se convierte en NaN .
Adición ‘+’ concatena strings
Casi todas las operaciones matemáticas convierten valores a números. Una excepción notable es
la suma + . Si uno de los valores sumados es un string, el otro valor es convertido a string.
Esto ocurre solo si al menos uno de los argumentos es un string, en caso contrario los
valores son convertidos a número.
ToBoolean
La conversión a boolean es la más simple.
Ocurre en operaciones lógicas (más adelante veremos test condicionales y otras cosas
similares), pero también puede realizarse de forma explícita llamando a la
función Boolean(value) .
Las reglas de conversión:
Los valores que son intuitivamente “vacíos”, como 0 , "" , null , undefined , y NaN , se
convierten en false .
Por ejemplo:
Algunos lenguajes (como PHP) tratan "0" como false . Pero en JavaScript, un string no vacío es
siempre true .
Resumen
Las tres conversiones de tipo más usadas son a string, a número y a boolean.
ToString – Ocurre cuando se muestra algo. Se puede realizar con String(value) . La conversión a
JavaScript (JS) 20
Valor Se convierte en…
undefined NaN
null 0
true / false 1 / 0
El string es leído “como es”, los espacios en blanco (incluye espacios, tabs \t , saltos de
string línea \n , etc.) tanto al inicio como al final son ignorados. Un string vacío se convierte
en 0 . Un error entrega NaN .
La mayoría de estas reglas son fáciles de entender y recordar. Las excepciones más notables
donde la gente suele cometer errores son:
"0" y textos que solo contienen espacios como " " son true como boolean.
Los objetos no son cubiertos aquí. Volveremos a ellos más tarde en el capítulo Conversión de
objeto a valor primitivo que está dedicado exclusivamente a objetos después de que aprendamos
más cosas básicas sobre JavaScript.
Conocemos varios operadores matemáticos porque nos los enseñaron en la escuela. Son cosas
como la suma + , multiplicación * , resta - , etcétera.
En este capítulo, nos vamos a concentrar en los aspectos de los operadores que no están
cubiertos en la aritmética escolar.
let x = 1;
Un operador es binario si tiene dos operandos. El mismo negativo también existe en forma
binaria:
let x = 1, y = 3;
alert( y - x ); // 2, binario negativo resta valores
Matemáticas
JavaScript (JS) 21
Están soportadas las siguientes operaciones:
Suma + ,
Resta ,
Multiplicación ,
División / ,
Resto % ,
Exponenciación * .
Los primeros cuatro son conocidos mientras que % y ** deben ser explicados más ampliamente.
Resto %
El operador resto % , a pesar de su apariencia, no está relacionado con porcentajes.
Exponenciación **
El operador exponenciación a ** b eleva a a la potencia de b .
En matemáticas de la escuela, lo escribimos como ab.
Por ejemplo:
alert( 2 ** 2 ); // 2² = 4
alert( 2 ** 3 ); // 2³ = 8
alert( 2 ** 4 ); // 2⁴ = 16
Tenga presente que si uno de los operandos es una cadena, el otro es convertido a una cadena
también.
Por ejemplo:
JavaScript (JS) 22
Vieron, no importa si el primer operando es una cadena o el segundo.
Aquí, los operadores trabajan uno después de otro. El primer + suma dos números entonces
devuelve 4 , luego el siguiente + le agrega la cadena 1 , así que se evalúa como 4 + '1' = 41 .
Aquí el primer operando es una cadena, el compilador trata los otros dos operandos como
cadenas también. El 2 es concatenado a '1' , entonces es como '1' + 2 = "12" y "12" + 2 = "122" .
El binario + es el único operador que soporta cadenas en esa forma. Otros operadores
matemáticos trabajan solamente con números y siempre convierten sus operandos a números.
alert( 2 - '1' ); // 1
alert( '6' / '2' ); // 3
Por ejemplo:
let y = -2;
alert( +y ); // -2
JavaScript (JS) 23
Desde el punto de vista de un matemático, la abundancia de signos más puede parecer extraña.
Pero desde el punto de vista de un programador no hay nada especial: primero se aplican los
signos más unarios que convierten las cadenas en números, y luego el signo más binario los
suma.
¿Por qué se aplican los signos más unarios a los valores antes que los binarios? Como
veremos, eso se debe a su mayor precedencia.
Los paréntesis anulan cualquier precedencia, por lo que si no estamos satisfechos con el
orden predeterminado, podemos usarlos para cambiarlo. Por ejemplo, escriba (1 + 2) * 2 .
Hay muchos operadores en JavaScript. Cada operador tiene un número de precedencia
correspondiente. El que tiene el número más grande se ejecuta primero. Si la precedencia es
la misma, el orden de ejecución es de izquierda a derecha.
Aquí hay un extracto de la tabla de precedencia (no necesita recordar esto, pero tenga en
cuenta que los operadores unarios son más altos que el operador binario correspondiente):
… … …
14 suma unaria +
14 negación unaria -
13 exponenciación **
12 multiplicación *
12 división /
11 suma +
11 resta -
… … …
2 asignación =
… … …
Como podemos ver, la “suma unaria” tiene una prioridad de 14 , que es mayor que el 11 de
“suma” (suma binaria). Es por eso que, en la expresión "+apples + +oranges" , las sumas unarias se
hacen antes de la adición.
Asignación
Tengamos en cuenta que una asignación = también es un operador. Está listado en la tabla de
precedencia con la prioridad muy baja de 2 .
Es por eso que, cuando asignamos una variable, como x = 2 * 2 + 1 , los cálculos se realizan
primero y luego se evalúa el = , almacenando el resultado en x .
let x = 2 * 2 + 1;
alert( x ); // 5
JavaScript (JS) 24
Todos los operadores en JavaScript devuelven un valor. Esto es obvio para + y - , pero
también es cierto para = .
let a = 1;
let b = 2;
let c = 3 - (a = b + 1);alert( a ); // 3
alert( c ); // 0
Código gracioso, ¿no? Deberíamos entender cómo funciona, porque a veces lo vemos en las
bibliotecas de JavaScript.
Pero no deberíamos escribir algo así. Tales trucos definitivamente no hacen que el código sea
más claro o legible.
Asignaciones encadenadas
Otra característica interesante es la habilidad para encadenar asignaciones:
let a, b, c;
a = b = c = 2 + 2;alert( a ); // 4
alert( b ); // 4
alert( c ); // 4
c = 2 + 2;
b = c;
a = c;
Modificar en el lugar
A menudo necesitamos aplicar un operador a una variable y guardar el nuevo resultado en esa
misma variable.
Por ejemplo:
let n = 2;
n = n + 5;
n = n * 2;
let n = 2;
n += 5; // ahora n = 7 (es lo mismo que n = n + 5)
n *= 2; // ahora n = 14 (es lo mismo que n = n * 2)
alert( n ); // 14
JavaScript (JS) 25
Los operadores cortos “modifica y asigna” existen para todos los operadores aritméticos y de
nivel bit: /= , -= , etcétera.
Tales operadores tienen la misma precedencia que la asignación normal, por lo tanto se
ejecutan después de otros cálculos:
let n = 2;
alert( n ); // 16
Incremento/decremento
Aumentar o disminuir un número en uno es una de las operaciones numéricas más comunes.
let counter = 2;
counter++; // funciona igual que counter = counter + 1, pero es más corto
alert( counter ); // 3
let counter = 2;
counter--; // funciona igual que counter = counter - 1, pero es más corto
alert( counter ); // 1
Importante:
Incremento/decremento sólo puede ser aplicado a variables. Intentar utilizarlo en un valor
como 5++ dará un error.
Los operadores ++ y -- pueden ser colocados antes o después de una variable.
¿Existe alguna diferencia? Sí, pero solamente la podemos ver si utilizamos el valor devuelto
de ++/-- .
Aclaremos. Tal como conocemos, todos los operadores devuelven un valor. Incremento/decremento
no es una excepción. La forma prefijo devuelve el nuevo valor mientras que la forma sufijo
devuelve el valor anterior (antes del incremento/decremento).
let counter = 1;
let a = ++counter; // (*)
alert(a); // 2
En la línea (*) , la forma prefijo ++counter incrementa counter y devuelve el nuevo valor, 2 . Por
lo tanto, el alert muestra 2 .
Ahora usemos la forma sufijo:
let counter = 1;
let a = counter++; // (*) cambiado ++counter a counter++
alert(a); // 1
JavaScript (JS) 26
En la línea (*) , la forma sufijo counter++ también incrementa counter , pero devuelve
el antiguo valor (antes de incrementar). Por lo tanto, el alert muestra 1 .
Para resumir:
let counter = 0;
counter++;
++counter;
alert( counter ); // 2, las líneas de arriba realizan lo mismo
let counter = 0;
alert( ++counter ); // 1
let counter = 0;
alert( counter++ ); // 0
let counter = 1;
alert( 2 * ++counter ); // 4
Compara con:
let counter = 1;
alert( 2 * counter++ ); // 2, porque counter++ devuelve el valor "antiguo"
Aunque técnicamente está bien, tal notación generalmente hace que el código sea menos
legible. Una línea hace varias cosas, no es bueno.
Mientras lee el código, un rápido escaneo ocular “vertical” puede pasar por alto fácilmente
algo como ‘counter++’ y no será obvio que la variable aumentó.
let counter = 1;
alert( 2 * counter );
counter++;
AND ( & )
JavaScript (JS) 27
OR ( | )
XOR ( ^ )
NOT ( ~ )
Coma
El operador coma , es uno de los operadores más raros e inusuales. A veces, es utilizado para
escribir código más corto, entonces tenemos que saberlo para poder entender qué está pasando.
El operador coma nos permite evaluar varias expresiones, dividiéndolas con una coma , . Cada
una de ellas es evaluada, pero sólo el resultado de la última es devuelto.
Por ejemplo:
A veces, las personas lo usan en construcciones más complejas para poner varias acciones en
una línea.
Por ejemplo:
Tales trucos se usan en muchos frameworks de JavaScript. Por eso los estamos mencionando.
Pero generalmente no mejoran la legibilidad del código, por lo que debemos pensar bien antes
de usarlos.
Comparaciones
JavaScript (JS) 28
Igual: a == b (ten en cuenta que el doble signo == significa comparación, mientras que un
solo símbolo a = b significaría una asignación).
En este artículo, aprenderemos más sobre los diferentes tipos de comparaciones y de cómo las
realiza JavaScript, incluidas las peculiaridades importantes.
Al final, encontrará una buena receta para evitar problemas relacionadas con las
“peculiaridades” de JavaScript.
Booleano es el resultado
Como todos los demás operadores, una comparación retorna un valor. En este caso, el valor es
un booleano.
Por ejemplo:
El resultado de una comparación puede asignarse a una variable, igual que cualquier valor:
Comparación de cadenas
Para ver si una cadena es “mayor” que otra, JavaScript utiliza el llamado orden “de
diccionario” o “lexicográfico”.
3. De lo contrario, si los primeros caracteres de ambas cadenas son los mismos, compare los
segundos caracteres de la misma manera.
En los ejemplos anteriores, la comparación 'Z' > 'A' llega a un resultado en el primer paso.
La segunda comparación "Glow" y "Glee" necesitan más pasos, se comparan carácter por carácter:
JavaScript (JS) 29
3. o es mayor que e . Detente aquí. La primera cadena es mayor.
Para valores booleanos, true se convierte en 1 y false en 0 .
Por ejemplo:
Uno de ellos sea true como booleano y el otro sea false como booleano.
Por ejemplo:
let a = 0;
alert( Boolean(a) ); // false
let b = "0";
alert( Boolean(b) ); // true
alert( a == b ); // true!
Desde el punto de vista de JavaScript, este resultado es bastante normal. Una comparación de
igualdad convierte valores utilizando la conversión numérica (de ahí que "0" se convierta
en 0 ), mientras que la conversión explícita Boolean utiliza otro conjunto de reglas.
Igualdad estricta
Una comparación regular de igualdad == tiene un problema. No puede diferenciar 0 de `falso’:
Esto sucede porque los operandos de diferentes tipos son convertidos a números por el
operador de igualdad == . Una cadena vacía, al igual que false , se convierte en un cero.
JavaScript (JS) 30
Un operador de igualdad estricto === comprueba la igualdad sin conversión de tipo.
En otras palabras, si a y b son de diferentes tipos, entonces a === b retorna
inmediatamente false sin intentar convertirlos.
Intentémoslo:
Para un control de igualdad estricto === Estos valores son diferentes, porque cada uno de
ellos es de un tipo diferente.
Para una comparación no estricta == Hay una regla especial. Estos dos son una " pareja dulce
": son iguales entre sí (en el sentido de == ), pero no a ningún otro valor.
Para matemáticas y otras comparaciones < > <= >= null/undefined se convierten en números: null se
convierte en 0 , mientras que undefined se convierte en NaN .
Ahora veamos algunos hechos graciosos que suceden cuando aplicamos estas reglas. Y, lo que es
más importante, cómo no caer en una trampa con ellas.
Matemáticamente, eso es extraño. El último resultado afirma que " null es mayor o igual a
cero", así que en una de las comparaciones anteriores debe ser true , pero ambas son falsas.
La razón es que una comparación de igualdad == y las comparaciones > < >= <= funcionan de
manera diferente. Las comparaciones convierten a null en un número, tratándolo como 0 . Es por
eso que (3) null >= 0 es verdadero y (1) null > 0 es falso.
Por otro lado, el control de igualdad == para undefined y null se define de tal manera que, sin
ninguna conversión, son iguales entre sí y no son iguales a nada más. Es por eso que (2) null
== 0 es falso.
Un indefinido incomparable
El valor undefined no debe compararse con otros valores:
JavaScript (JS) 31
Las comparaciones (1) y (2) retornan falso porque no definido se convierte en NaN y NaN es un
valor numérico especial que retorna falso para todas las comparaciones.
La comparación de igualdad (3) retorna falso porque undefined sólo equivale a null y a ningún
otro valor.
Trata cualquier comparación con undefined/null (excepto la igualdad estricta === ) con sumo
cuidado.
No uses comparaciones >= > < <= con una variable que puede ser null/undefined , a menos que estés
realmente seguro de lo que estás haciendo. Si una variable puede tener estos valores,
verifícalos por separado.
Resumen
Los operadores de comparación retornan un valor booleano.
Los valores null y undefined son iguales == entre sí y no equivalen a ningún otro valor.
Ten cuidado al usar comparaciones como > o < con variables que ocasionalmente pueden
ser null/undefined . Revisar por separado si hay null/undefined es una buena idea.
La sentencia “if”
La sentencia if(...) evalúa la condición en los paréntesis, y si el resultado es verdadero
( true ), ejecuta un bloque de código.
Por ejemplo:
let year = prompt('¿En que año fué publicada la especificación ECMAScript-2015?', '');
Aquí la condición es una simple igualdad ( year == 2015 ), pero podría ser mucho más compleja.
Si queremos ejecutar más de una sentencia, debemos encerrar nuestro bloque de código entre
llaves:
if (year == 2015) {
alert( "¡Es Correcto!" );
alert( "¡Eres muy inteligente!" );
}
Recomendamos encerrar nuestro bloque de código entre llaves {} siempre que se utilice la
sentencia if , incluso si solo se va a ejecutar una sola sentencia. Al hacerlo mejoramos la
legibilidad.
JavaScript (JS) 32
Conversión Booleana
La sentencia if (…) evalúa la expresión dentro de sus paréntesis y convierte el resultado en
booleano.
El número 0 , un string vacío "" , null , undefined , y NaN , se convierten en false . Por esto
son llamados valores “falsos”.
El resto de los valores se convierten en true , entonces los llamaremos valores
“verdaderos”.
if (0) { // 0 es falso
...
}
if (1) { // 1 es verdadero
...
}
if (cond) {
...
}
La cláusula “else”
La sentencia if puede contener un bloque else (“si no”, “en caso contrario”) opcional. Este
bloque se ejecutará cuando la condición sea falsa.
Por ejemplo:
let year = prompt('¿En qué año fue publicada la especificación ECMAScript-2015?', '');
if (year == 2015) {
alert( '¡Lo adivinaste, correcto!' );
} else {
alert( '¿Cómo puedes estar tan equivocado?' ); // cualquier valor excepto 2015
}
Por ejemplo:
let year = prompt('¿En qué año fue publicada la especificación ECMAScript-2015?', '');
JavaScript (JS) 33
En el código de arriba, JavaScript primero revisa si year < 2015 . Si esto es falso, continúa a
la siguiente condición year > 2015 . Si esta también es falsa, mostrará la última alert .
Podría haber más bloques else if . Y el último else es opcional.
let accessAllowed;
let age = prompt('¿Qué edad tienes?', '');
El “operador condicional” nos permite ejecutar esto en una forma más corta y simple.
El operador está representado por el signo de cierre de interrogación ? . A veces es llamado
“ternario” porque el operador tiene tres operandos, es el único operador de JavaScript que
tiene esa cantidad.
La Sintaxis es:
Técnicamente, podemos omitir el paréntesis alrededor de age > 18 . El operador de signo de
interrogación tiene una precedencia baja, por lo que se ejecuta después de la comparación > .
En este ejemplo realizaremos lo mismo que en el anterior:
Pero los paréntesis hacen el código mas legible, asi que recomendamos utilizarlos.
Por favor tome nota:
En el ejemplo de arriba, podrías evitar utilizar el operador de signo de interrogación porque
esta comparación devuelve directamente true/false :
// es lo mismo que
let accessAllowed = age > 18;
Múltiples ‘?’
Una secuencia de operadores de signos de interrogación ? puede devolver un valor que depende
de más de una condición.
Por ejemplo:
JavaScript (JS) 34
let message = (age < 3) ? '¡Hola, bebé!' :
(age < 18) ? '¡Hola!' :
(age < 100) ? '¡Felicidades!' :
'¡Qué edad tan inusual!';
alert( message );
Puede ser difícil al principio comprender lo que está sucediendo. Pero después de una mirada
más cercana, podemos ver que es solo una secuencia ordinaria de condiciones:
3. Si es cierto, devuelve '¡Hola!' . De lo contrario, continúa con la expresión que está después
de los dos puntos siguientes “:”, la cual revisa si age < 100 .
if (age < 3) {
message = '¡Hola, bebé!';
} else if (age < 18) {
message = '¡Hola!';
} else if (age < 100) {
message = '¡Felicidades!';
} else {
message = '¡Qué edad tan inusual!';
}
(company == 'Netscape') ?
alert('¡Correcto!') : alert('Equivocado.');
La notación es más corta que la sentencia equivalente con if , lo cual seduce a algunos
programadores. Pero es menos legible.
if (company == 'Netscape') {
alert('¡Correcto!');
} else {
alert('Equivocado.');
}
Nuestros ojos leen el código verticalmente. Los bloques de código que se expanden múltiples
lineas son mas fáciles de entender que los las instrucciones largas horizontales.
El propósito del operador de signo de interrogación ? es para devolver un valor u otro
dependiendo de su condición. Por favor utilízala para exactamente esto. Utiliza la
sentencia if cuando necesites ejecutar código en ramas distintas.
JavaScript (JS) 35
Operadores Lógicos
Hay cuatro operadores lógicos en JavaScript: || (O), && (Y), ! (NO), ?? (Fusión de nulos).
Aquí cubrimos los primeros tres, el operador ?? se verá en el siguiente artículo.
Aunque sean llamados lógicos, pueden ser aplicados a valores de cualquier tipo, no solo
booleanos. El resultado también puede ser de cualquier tipo.
Veamos los detalles.
|| (OR)
El operador OR se representa con dos símbolos de linea vertical:
result = a || b;
En la programación clásica, el OR lógico esta pensado para manipular solo valores booleanos.
Si cualquiera de sus argumentos es true , retorna true , de lo contrario retorna false .
En JavaScript, el operador es un poco más complicado y poderoso. Pero primero, veamos qué
pasa con los valores booleanos.
Como podemos ver, el resultado es siempre true excepto cuando ambos operandos son false .
Si un operando no es un booleano, se lo convierte a booleano para la evaluación.
Por ejemplo, el número 1 es tratado como true , el número 0 como false :
La mayoría de las veces, OR || es usado en una declaración if para probar si alguna de las
condiciones dadas es true .
Por ejemplo:
let hour = 9;
if (hour < 10 || hour > 18) {alert( 'La oficina esta cerrada.' );
}
JavaScript (JS) 36
El algoritmo extendido trabaja de la siguiente forma.
Para cada operando, convierte el valor a booleano. Si el resultado es true , se detiene y
retorna el valor original de ese operando.
Si todos los operandos han sido evaluados (todos eran false ), retorna el ultimo operando.
alert(undefined || null || 0); // 0 (todos son valores falsos, retorna el último valor)
Esto brinda varios usos interesantes comparados al “OR puro, clásico, de solo booleanos”.
JavaScript (JS) 37
&& (AND)
El operador AND es representado con dos ampersands && :
result = a && b;
En la programación clásica, AND retorna true si ambos operandos son valores verdaderos
y false en cualquier otro caso.
Un ejemplo con if :
Al igual que con OR, cualquier valor es permitido como operando de AND:
Para cada operando, los convierte a un booleano. Si el resultado es false , se detiene y
retorna el valor original de dicho operando.
Si todos los operandos han sido evaluados (todos fueron valores verdaderos), retorna el
último operando.
En otras palabras, AND retorna el primer valor falso o el último valor si ninguno fue
encontrado.
Las reglas anteriores son similares a las de OR. La diferencia es que AND retorna el primer
valor falso mientras que OR retorna el primer valor verdadero.
Ejemplo:
JavaScript (JS) 38
También podemos pasar varios valores de una vez. Observa como el primer valor falso es
retornado:
No remplace if con || ni &&
A veces, la gente usa el operador AND && como una "forma más corta de escribir if ".
Por ejemplo:
let x = 1;
La acción en la parte derecha de && sería ejecutada sólo si la evaluación la alcanza. Eso es,
solo si (x > 0) es verdadero.
Así que básicamente tenemos un análogo para:
let x = 1;
Aunque la variante con && parece más corta, if es más obvia y tiende a ser un poco más
legible. Por lo tanto, recomendamos usar cada construcción para su propósito: use if si
queremos si y use && si queremos AND.
! (NOT)
El operador booleano NOT se representa con un signo de exclamación ! .
result = !value;
Por ejemplo:
alert(!true); // false
alert(!0); // true
Un doble NOT !! es a veces usado para convertir un valor al tipo booleano:
JavaScript (JS) 39
Eso es, el primer NOT convierte el valor a booleano y retorna el inverso, y el segundo NOT lo
invierte de nuevo. Al final, tenemos una simple conversión a booleano.
Hay una manera un poco mas prolija de realizar lo mismo – una función integrada Boolean :
La precedencia de NOT ! es la mayor de todos los operadores lógicos, así que siempre se
ejecuta primero, antes que && o || .
El operador “nullish coalescing” (fusión de null) se escribe con un doble signo de cierre de
interrogación ?? .
Como este trata a null y a undefined de forma similar, usaremos un término especial para este
artículo. Diremos que una expresión es “definida” cuando no es null ni undefined .
El resultado de a ?? b :
Es decir, ?? devuelve el primer argumento cuando este no es null ni undefined . En caso
contrario, devuelve el segundo.
El operador “nullish coalescing” no es algo completamente nuevo. Es solamente una sintaxis
agradable para obtener el primer valor “definido” de entre dos.
Podemos reescribir result = a ?? b usando los operadores que ya conocemos:
Ahora debería estar absolutamente claro lo que ?? hace. Veamos dónde podemos utilizarlo.
El uso típico de ?? es brindar un valor predeterminado.
Por ejemplo, aquí mostramos user si su valor está “definido” (que no es null ni undefined ). De
otro modo, muestra Anonymous :
let user;
También podemos usar una secuencia de ?? para seleccionar el primer valor que no
sea null/undefined de una lista.
Digamos que tenemos los datos de un usuario en las variables firstName , lastName y nickName . Todos
ellos podrían ser indefinidos si el usuario decide no ingresar los valores correspondientes.
Queremos mostrar un nombre usando una de estas variables, o mostrar “anónimo” si todas ellas
son null/undefined .
Usemos el operador ?? para ello:
JavaScript (JS) 40
// Muestra el primer valor definido:
alert(firstName ?? lastName ?? nickName ?? "Anonymous"); // Supercoder
Comparación con ||
El operador OR || puede ser usado de la misma manera que ?? , tal como está explicado en
el capítulo previo
Por ejemplo, en el código de arriba podemos reemplazar ?? por || y obtener el mismo
resultado:
Históricamente, el operador OR || estuvo primero. Existe desde el origen de JavaScript, así
que los desarrolladores lo estuvieron usando para tal propósito durante mucho tiempo.
Por otro lado, el operador “nullish coalescing” ?? fue una adición reciente, y la razón es
que la gente no estaba del todo satisfecha con || .
La gran diferencia es que:
El || no distingue entre false , 0 , un string vacío "" , y null/undefined . Todos son lo mismo:
valores “falsos”. Si cualquiera de ellos es el primer argumento de || , obtendremos el segundo
argumento como resultado.
Pero en la práctica podemos querer usar el valor predeterminado solamente cuando la variable
es null/undefined , es decir cuando el valor realmente es desconocido o no fue establecido.
Por ejemplo considera esto:
height || 100 verifica si height es “falso”, y 0 lo es. - así el resultado de || es el segundo
argumento, 100 . height ?? 100 verifica si height es null/undefined , y no lo es. - así el resultado
es height como está, que es 0 .
En la práctica, una altura cero es a menudo un valor válido que no debería ser reemplazado
por un valor por defecto. En este caso ?? hace lo correcto.
Precedencia
La precedencia del operador ?? es la misma de || . Ambos son iguales a 3 en la Tabla MDN.
Esto significa que ambos operadores, || y ?? , son evaluados antes que = y ? , pero después de
la mayoría de las demás operaciones como + y * .
Así que podemos necesitar añadir paréntesis:
alert(area); // 5000
JavaScript (JS) 41
Caso contrario, si omitimos los paréntesis, entonces * tiene una mayor precedencia y se
ejecutará primero. Eso sería lo mismo que:
// sin paréntesis
let area = height ?? 100 * width ?? 50;
Resumen
El operador “nullish coalescing” ?? brinda una manera concisa de seleccionar un valor
“definido” de una lista.
El operador ?? tiene una precedencia muy baja, un poco más alta que ? y = .
Si llegó a este artículo buscando otro tipo de bucles, aquí están los enlaces:
El bucle “while”
El bucle while (mientras) tiene la siguiente sintaxis:
JavaScript (JS) 42
while (condition) {
// código
// llamado "cuerpo del bucle"
}
Mientras la condición condition sea verdadera, el código del cuerpo del bucle será ejecutado.
Por ejemplo, el bucle debajo imprime i mientras se cumpla i < 3 :
let i = 0;
while (i < 3) { // muestra 0, luego 1, luego 2
alert( i );
i++;
}
Cada ejecución del cuerpo del bucle se llama iteración. El bucle en el ejemplo de arriba
realiza 3 iteraciones.
Si faltara i++ en el ejemplo de arriba, el bucle sería repetido (en teoría) eternamente. En
la práctica, el navegador tiene maneras de detener tales bucles desmedidos; y en el
JavaScript del lado del servidor, podemos eliminar el proceso.
Cualquier expresión o variable puede usarse como condición del bucle, no solo las
comparaciones: El while evaluará y transformará la condición a un booleano.
Por ejemplo, una manera más corta de escribir while (i != 0) es while (i) :
let i = 3;
while (i) { // cuando i sea 0, la condición se volverá falsa y el bucle se detendráalert( i );
i--;
}
let i = 3;
while (i) alert(i--);
El bucle “do…while”
La comprobación de la condición puede ser movida debajo del cuerpo del bucle usando la
sintaxis do..while :
do {
// cuerpo del bucle
} while (condition);
El bucle primero ejecuta el cuerpo, luego comprueba la condición, y, mientras sea un valor
verdadero, la ejecuta una y otra vez.
Por ejemplo:
let i = 0;
do {
alert( i );
i++;
} while (i < 3);
Esta sintaxis solo debe ser usada cuando quieres que el cuerpo del bucle sea ejecutado al
menos una vez sin importar que la condición sea verdadera. Usualmente, se prefiere la otra
forma: while(…) {…} .
JavaScript (JS) 43
El bucle “for”
El bucle for es más complejo, pero también el más usado.
Se ve así:
parte
cuerpo alert(i) Se ejecuta una y otra vez mientras la condición sea verdadera.
Se ejecuta comenzar
→ (si condición → ejecutar cuerpo y ejecutar paso)
→ (si condición → ejecutar cuerpo y ejecutar paso)
→ (si condición → ejecutar cuerpo y ejecutar paso)
→ ...
Si eres nuevo en bucles, te podría ayudar regresar al ejemplo y reproducir cómo se ejecuta
paso por paso en una pedazo de papel.
Esto es lo que sucede exactamente en nuestro caso:
// se ejecuta comenzar
let i = 0
// si condición → ejecutar cuerpo y ejecutar paso
if (i < 3) { alert(i); i++ }
// si condición → ejecutar cuerpo y ejecutar paso
if (i < 3) { alert(i); i++ }
// si condición → ejecutar cuerpo y ejecutar paso
if (i < 3) { alert(i); i++ }
// ...finaliza, porque ahora i == 3
let i = 0;
JavaScript (JS) 44
for (i = 0; i < 3; i++) { // usa una variable existente
alert(i); // 0, 1, 2
}
Omitiendo partes
Cualquier parte de for puede ser omitida.
Por ejemplo, podemos quitar comienzo si no necesitamos realizar nada al inicio del bucle.
Como aquí:
let i = 0;
for (;;) {
// se repite sin limites
}
Por favor, nota que los dos punto y coma ; del for deben estar presentes. De otra manera,
habría un error de sintaxis.
Rompiendo el bucle
Normalmente, se sale de un bucle cuando la condición se vuelve falsa.
Pero podemos forzar una salida en cualquier momento usando la directiva especial break .
Por ejemplo, el bucle debajo le pide al usuario por una serie de números, “rompiéndolo”
cuando un número no es ingresado:
let sum = 0;
while (true) {
sum += value;
}
alert( 'Suma: ' + sum );
La directiva break es activada en la línea (*) si el usuario ingresa una línea vacía o cancela
la entrada. Detiene inmediatamente el bucle, pasando el control a la primera línea después de
el bucle. En este caso, alert .
La combinación “bucle infinito + break según sea necesario” es ideal en situaciones donde la
condición del bucle debe ser comprobada no al inicio o al final de el bucle, sino a la mitad
o incluso en varias partes del cuerpo.
JavaScript (JS) 45
Continuar a la siguiente iteración
La directiva continue es una “versión más ligera” de break . No detiene el bucle completo. En su
lugar, detiene la iteración actual y fuerza al bucle a comenzar una nueva (si la condición lo
permite).
Podemos usarlo si hemos terminado con la iteración actual y nos gustaría movernos a la
siguiente.
El bucle debajo usa continue para mostrar solo valores impares:
Para los valores pares de i , la directiva continue deja de ejecutar el cuerpo y pasa el
control a la siguiente iteración de for (con el siguiente número). Así que el alert solo es
llamado para valores impares.
if (i % 2) {
alert( i );
}
Desde un punto de vista técnico, esto es idéntico al ejemplo de arriba. Claro, podemos
simplemente envolver el código en un bloque if en vez de usar continue .
Pero como efecto secundario, esto crearía un nivel más de anidación (la llamada a alert dentro
de las llaves). Si el código dentro de if posee varias líneas, eso podría reducir la
legibilidad en general.
if (i > 5) {
alert(i);
} else {
continue;
}
JavaScript (JS) 46
Por ejemplo, en el código debajo usamos un bucle sobre i y j , solicitando las
coordenadas (i,j) de (0,0) a (3,3) :
}
}
alert('Listo!');
El break ordinario después de input solo nos sacaría del bucle interno. Eso no es suficiente.
¡Etiquetas, vengan al rescate!
Una etiqueta es un identificador con un signo de dos puntos “:” antes de un bucle:
La declaración break <labelName> en el bucle debajo nos saca hacia la etiqueta:
alert('Listo!');
En el código de arriba, break outer mira hacia arriba por la etiqueta llamada outer y nos saca
de dicho bucle.
Así que el control va directamente de (*) a alert('Listo!') .
outer:
for (let i = 0; i < 3; i++) { ... }
La directiva continue también puede usar usada con una etiqueta. En este caso, la ejecución del
código salta a la siguiente iteración del bucle etiquetado.
Una directiva break debe estar en el interior del bucle. Aunque, técnicamente, puede estar en
cualquier bloque de código etiquetado:
JavaScript (JS) 47
label: {
// ...
break label; // funciona
// ...
}
…Aunque 99.9% del tiempo break se usa dentro de bucles, como hemos visto en ejemplos previos.
Un continue es solo posible dentro de un bucle.
Resumen
Cubrimos 3 tipos de bucles:
for (;;) – La condición es comprobada antes de cada iteración, con ajustes adicionales
disponibles.
Para crear un bucle “infinito”, usualmente se usa while(true) . Un bucle como este, tal y como
cualquier otro, puede ser detenido con la directiva break .
Si queremos detener la iteración actual y adelantarnos a la siguiente, podemos usar la
directiva continue .
break/continue soportan etiquetas antes del bucle. Una etiqueta es la única forma de
usar break/continue para escapar de un bucle anidado para ir a uno exterior.
La sentencia "switch"
La sintaxis
switch tiene uno o mas bloques case y un opcional default .
Se ve de esta forma:
switch(x) {
case 'valor1': // if (x === 'valor1')
...
[break]
default:
...
[break]
}
El valor de x es comparado contra el valor del primer case (en este caso, valor1 ), luego
contra el segundo ( valor2 ) y así sucesivamente, todo esto bajo una igualdad estricta.
Si no se cumple ningún caso entonces el código default es ejecutado (si existe).
Ejemplo
Un ejemplo de switch (se resalta el código ejecutado):
JavaScript (JS) 48
let a = 2 + 2;
switch (a) {
case 3:
alert( 'Muy pequeño' );
break;
case 4:
alert( '¡Exacto!' );
break;case 5:
alert( 'Muy grande' );
break;
default:
alert( "Desconozco estos valores" );
}
Aquí el switch inicia comparando a con la primera variante case que es 3 . La comparación
falla.
Luego 4 . La comparación es exitosa, por tanto la ejecución empieza desde case 4 hasta
el break más cercano.
Si no existe break entonces la ejecución continúa con el próximo case sin ninguna revisión.
Un ejemplo sin break :
let a = 2 + 2;
switch (a) {
case 3:
alert( 'Muy pequeño' );
case 4:
alert( '¡Exacto!' );
case 5:
alert( 'Muy grande' );
default:
alert( "Desconozco estos valores" );}
alert( '¡Exacto!' );
alert( 'Muy grande' );
alert( "Desconozco estos valores" );
Por ejemplo:
let a = "1";
let b = 0;
switch (+a) {
case b + 1:
alert("esto se ejecuta, porque +a es 1, exactamente igual b+1");
break;default:
alert("esto no se ejecuta");
}
Aquí +a da 1 , esto es comparado con b + 1 en case , y el código correspondiente es ejecutado.
Agrupamiento de “case”
Varias variantes de case los cuales comparten el mismo código pueden ser agrupadas.
Por ejemplo, si queremos que se ejecute el mismo código para case 3 y case 5 :
let a = 2 + 2;
switch (a) {
case 4:
JavaScript (JS) 49
alert('¡Correcto!');
break;
El tipo importa
Vamos a enfatizar que la comparación de igualdad es siempre estricta. Los valores deben ser
del mismo tipo para coincidir.
case '2':
alert( 'Dos' );
break;
case 3:
alert( '¡Nunca ejecuta!' );
break;
default:
alert( 'Un valor desconocido' );
}
3. Pero para 3 , el resultado del prompt es un string "3" , el cual no es estrictamente
igual === al número 3 . Por tanto ¡Tenemos un código muerto en case 3 ! La
variante default se ejecutará.
Funciones
Muy a menudo necesitamos realizar acciones similares en muchos lugares del script.
Por ejemplo, debemos mostrar un mensaje atractivo cuando un visitante inicia sesión, cierra
sesión y tal vez en otros momentos.
Las funciones son los principales “bloques de construcción” del programa. Permiten que el
código se llame muchas veces sin repetición.
Ya hemos visto ejemplos de funciones integradas, como alert(message) , prompt(message,
default) y confirm(question) . Pero también podemos crear funciones propias.
Declaración de funciones
Para crear una función podemos usar una declaración de función.
Se ve como aquí:
JavaScript (JS) 50
function showMessage() {
alert( '¡Hola a todos!' );
}
La palabra clave function va primero, luego va el nombre de función, luego una lista
de parámetros entre paréntesis (separados por comas, vacía en el ejemplo anterior) y
finalmente el código de la función entre llaves, también llamado “el cuerpo de la función”.
Por ejemplo:
function showMessage() {
alert( '¡Hola a todos!' );
}
showMessage();
showMessage();
La llamada showMessage() ejecuta el código de la función. Aquí veremos el mensaje dos veces.
Este ejemplo demuestra claramente uno de los propósitos principales de las funciones: evitar
la duplicación de código…
Si alguna vez necesitamos cambiar el mensaje o la forma en que se muestra, es suficiente
modificar el código en un lugar: la función que lo genera.
Variables Locales
Una variable declarada dentro de una función solo es visible dentro de esa función.
Por ejemplo:
function showMessage() {
let message = "Hola, ¡Soy JavaScript!"; // variable localalert( message );
}
Variables Externas
Una función también puede acceder a una variable externa, por ejemplo:
function showMessage() {
let message = 'Hola, ' + userName;
alert(message);
}
function showMessage() {
userName = "Bob"; // (1) Cambió la variable externa
JavaScript (JS) 51
let message = 'Hola, ' + userName;
alert(message);
}
showMessage();
function showMessage() {
let userName = "Bob"; // declara variable locallet message = 'Hello, ' + userName; // Bobalert(message);
}
Variables globales
Variables declaradas fuera de cualquier función, como la variable externa userName en el código
anterior, se llaman globales.
Las variables globales son visibles desde cualquier función (a menos que se les superpongan
variables locales con el mismo nombre).
Es una buena práctica reducir el uso de variables globales. El código moderno tiene pocas o
ninguna variable global. La mayoría de las variables residen en sus funciones. Aunque a veces
puede justificarse almacenar algunos datos a nivel de proyecto.
Parámetros
Podemos pasar datos arbitrarios a funciones usando parámetros.
En el siguiente ejemplo, la función tiene dos parámetros: from y text .
Cuando la función se llama (*) y (**) , los valores dados se copian en variables
locales from y text . Y la función las utiliza.
Aquí hay un ejemplo más: tenemos una variable from y la pasamos a la función. Tenga en cuenta:
la función cambia from , pero el cambio no se ve afuera, porque una función siempre obtiene una
copia del valor:
from = '*' + from + '*'; // hace que "from" se vea mejoralert( from + ': ' + text );
}
JavaScript (JS) 52
Cuando un valor es pasado como un parámetro de función, también se denomina argumento.
Un argumento es el valor que es pasado a la función cuando esta es llamada (es el término
para el momento en que se llama).
Declaramos funciones listando sus parámetros, luego las llamamos pasándoles argumentos.
En el ejemplo de arriba, se puede decir: "la función showMessage es declarada con dos
parámetros, y luego llamada con dos argumentos: from y "Hola" ".
Valores predeterminados
Si una función es llamada, pero no se le proporciona un argumento, su valor correspondiente
se convierte en undefined .
Por ejemplo, la función mencionada anteriormente showMessage(from, text) se puede llamar con un solo
argumento:
showMessage("Ann");
Eso no es un error. La llamada mostraría "Ann: undefined" . Como no se pasa un valor de text , este
se vuelve undefined .
Podemos especificar un valor llamado “predeterminado” o “por defecto” (es el valor que se usa
si el argumento fue omitido) en la declaración de función usando = :
Aquí "sin texto" es un string, pero puede ser una expresión más compleja, la cual solo es
evaluada y asignada si el parámetro falta. Entonces, esto también es posible:
En JavaScript, se evalúa un parámetro predeterminado cada vez que se llama a la función sin
el parámetro respectivo.
Por otro lado, se llamará independientemente cada vez que text se omita.
Parámetros predeterminados en viejo código JavaScript
Años atrás, JavaScript no soportaba la sintaxis para parámetros predeterminados. Entonces se
usaban otras formas para especificarlos.
En estos días, aún podemos encontrarlos en viejos scripts.
JavaScript (JS) 53
Por ejemplo, una verificación explícita de undefined :
… O usando el operador || :
function showMessage(text) {
// ...
function showMessage(text) {
// si text es indefinida o falsa, la establece a 'vacío'
text = text || 'vacío';
...
}
function showCount(count) {
// si count es undefined o null, muestra "desconocido"
alert(count ?? "desconocido");
}
showCount(0); // 0
showCount(null); // desconocido
showCount(); // desconocido
Devolviendo un valor
Una función puede devolver un valor al código de llamada como resultado.
El ejemplo más simple sería una función que suma dos valores:
function sum(a, b) {
return a + b;
}
JavaScript (JS) 54
La directiva return puede estar en cualquier lugar de la función. Cuando la ejecución lo
alcanza, la función se detiene y el valor se devuelve al código de llamada (asignado
al result anterior).
Puede haber muchos return en una sola función. Por ejemplo:
function checkAge(age) {
if (age > 18) {
return true;} else {
return confirm('¿Tienes permiso de tus padres?');}
}
if ( checkAge(age) ) {
alert( 'Acceso otorgado' );
} else {
alert( 'Acceso denegado' );
}
Es posible utilizar return sin ningún valor. Eso hace que la función salga o termine
inmediatamente.
Por ejemplo:
function showMovie(age) {
if ( !checkAge(age) ) {
return;}
En el código de arriba, si checkAge(age) devuelve false , entonces showMovie no mostrará la alert .
Una función con un return vacío, o sin return, devuelve undefined
Si una función no devuelve un valor, es lo mismo que si devolviera undefined :
function doNothing() {
return;
}
return
(una + expresion + o + cualquier + cosa * f(a) + f(b))
Eso no funciona, porque JavaScript asume un punto y coma después del return . Eso funcionará
igual que:
JavaScript (JS) 55
Nomenclatura de funciones
Las funciones son acciones. Entonces su nombre suele ser un verbo. Debe ser breve, lo más
preciso posible y describir lo que hace la función, para que alguien que lea el código
obtenga una indicación de lo que hace la función.
Es una práctica generalizada comenzar una función con un prefijo verbal que describe
vagamente la acción. Debe haber un acuerdo dentro del equipo sobre el significado de los
prefijos.
Por ejemplo, funciones que comienzan con "show" usualmente muestran algo.
Funciones que comienza con…
Con los prefijos en su lugar, un vistazo al nombre de una función permite comprender qué tipo
de trabajo realiza y qué tipo de valor devuelve.
Una función – una acción
Una función debe hacer exactamente lo que sugiere su nombre, no más.
Dos acciones independientes por lo general merecen dos funciones, incluso si generalmente se
convocan juntas (en ese caso, podemos hacer una tercera función que llame a esas dos).
Algunos ejemplos de cómo se rompen estas reglas:
getAge – está mal que muestre una alert con la edad (solo debe obtenerla).
createForm – está mal que modifique el documento agregándole el form (solo debe crearlo y
devolverlo).
checkPermission – está mal que muestre el mensaje acceso otorgado/denegado (solo debe realizar la
verificación y devolver el resultado).
En estos ejemplos asumimos los significados comunes de los prefijos. Tú y tu equipo pueden
acordar significados diferentes, aunque usualmente no muy diferente. En cualquier caso, debe
haber una compromiso firme de lo que significa un prefijo, de lo que una función con prefijo
puede y no puede hacer. Todas las funciones con el mismo prefijo deben obedecer las reglas. Y
el equipo debe compartir ese conocimiento.
Funciones == Comentarios
Las funciones deben ser cortas y hacer exactamente una cosa. Si esa cosa es grande, tal vez
valga la pena dividir la función en algunas funciones más pequeñas. A veces, seguir esta
regla puede no ser tan fácil, pero definitivamente es algo bueno.
JavaScript (JS) 56
Una función separada no solo es más fácil de probar y depurar, – ¡su existencia es un gran
comentario!
Por ejemplo, comparemos las dos funciones showPrimes(n) siguientes. Cada una devuelve números
primos hasta n .
La primera variante usa una etiqueta:
function showPrimes(n) {
nextPrime: for (let i = 2; i < n; i++) {
La segunda variante usa una función adicional isPrime(n) para probar la primalidad:
function showPrimes(n) {
function isPrime(n) {
for (let i = 2; i < n; i++) {
if ( n % i == 0) return false;
}
return true;
}
La segunda variante es más fácil de entender, ¿no? En lugar del código, vemos un nombre de la
acción. ( isPrime ). A veces las personas se refieren a dicho código como autodescriptivo.
Por lo tanto, las funciones se pueden crear incluso si no tenemos la intención de
reutilizarlas. Estructuran el código y lo hacen legible.
Resumen
Una declaración de función se ve así:
Los valores pasados a una función como parámetros se copian a sus variables locales.
Una función puede acceder a variables externas. Pero funciona solo de adentro hacia
afuera. El código fuera de la función no ve sus variables locales.
Una función puede devolver un valor. Si no lo hace, entonces su resultado es undefined .
Para que el código sea limpio y fácil de entender, se recomienda utilizar principalmente
variables y parámetros locales en la función, no variables externas.
Siempre es más fácil entender una función que obtiene parámetros, trabaja con ellos y
devuelve un resultado que una función que no obtiene parámetros, pero modifica las variables
externas como un efecto secundario.
Nomenclatura de funciones:
Un nombre debe describir claramente lo que hace la función. Cuando vemos una llamada a la
función en el código, un buen nombre nos da al instante una comprensión de lo que hace y
devuelve.
Una función es una acción, por lo que los nombres de las funciones suelen ser verbales.
JavaScript (JS) 57
Existen muchos prefijos de funciones bien conocidos como create… , show… , get… , check… y así.
Úsalos para insinuar lo que hace una función.
Las funciones son los principales bloques de construcción de los scripts. Ahora hemos
cubierto los conceptos básicos, por lo que en realidad podemos comenzar a crearlos y usarlos.
Pero ese es solo el comienzo del camino. Volveremos a ellos muchas veces, profundizando en
sus funciones avanzadas.
Expresiones de función
En JavaScript, una función no es una “estructura mágica del lenguaje”, sino un tipo de valor
especial.
La sintaxis que usamos antes se llama Declaración de Función:
function sayHi() {
alert( "Hola" );
}
Existe otra sintaxis para crear una función que se llama una Expresión de Función.
Esto nos permite crear una nueva función en el medio de cualquier expresión
Por ejemplo:
Aquí podemos ver una variable sayHi obteniendo un valor —la nueva función— creada como function()
{ alert("Hello"); } .
Como la creación de una función ocurre en el contexto de una expresión de asignación, (el
lado derecho de = ), esto es una Expresión de función.
Note que no hay un nombre después de la palabra clave function . Omitir el nombre está permitido
en las expresiones de función.
Aquí la asignamos directamente a la variable, así que el significado de estos ejemplos de
código es el mismo: "crear una función y ponerla en la variable sayHi ".
En situaciones más avanzadas, que cubriremos más adelante, una función puede ser creada e
inmediatamente llamada o agendada para uso posterior, sin almacenarla en ningún lugar,
permaneciendo así anónima.
La función es un valor
Reiteremos: no importa cómo es creada la función, una función es un valor. Ambos ejemplos
arriba almacenan una función en la variable sayHi .
function sayHi() {
alert( "Hola" );
}
Tenga en cuenta que la última línea no ejecuta la función, porque no hay paréntesis después
de sayHi . Existen lenguajes de programación en los que cualquier mención del nombre de una
función causa su ejecución, pero JavaScript no funciona así.
En JavaScript, una función es un valor, por lo tanto podemos tratarlo como un valor. El
código de arriba muestra su representación de cadena, que es el código fuente.
JavaScript (JS) 58
Por supuesto que es un valor especial, en el sentido que podemos invocarlo de esta
forma sayHi() .
Pero sigue siendo un valor. Entonces podemos trabajar con ello como trabajamos con otro tipo
de valores.
Podemos copiar una función a otra variable:
3. Ahora la función puede ser llamada de ambas maneras, sayHi() y func() .
También podríamos haber usado una expresión de función para declarar sayHi en la primera
línea:
function sayHi() {
// ...
}
La respuesta es simple: una expresión de función se crea aquí como function(…) {…} dentro de la
sentencia de asignación let sayHi = …; . El punto y coma se recomienda para finalizar la
sentencia, no es parte de la sintaxis de función.
El punto y coma estaría allí para una asignación más simple tal como let sayHi = 5; , y también
está allí para la asignación de función.
Funciones Callback
Veamos más ejemplos del pasaje de funciones como valores y el uso de expresiones de función.
Escribimos una función ask(question, yes, no) con tres argumentos:
question Texto de la pregunta yes Función a ejecutar si la respuesta es “Yes” no Función a ejecutar
si la respuesta es “No”
La función deberá preguntar la question y, dependiendo de la respuesta del usuario,
llamar yes() o no() :
JavaScript (JS) 59
function ask(question, yes, no) {
if (confirm(question)) yes()
else no();
}function showOk() {
alert( "Estás de acuerdo." );
}
function showCancel() {
alert( "Cancelaste la ejecución." );
}
// uso: las funciones showOk, showCancel son pasadas como argumentos de ask
ask("Estás de acuerdo?", showOk, showCancel);
ask(
"Estás de acuerdo?",
function() { alert("Estás de acuerdo"); },
function() { alert("Cancelaste la ejecución."); }
);
Aquí, las funciones son declaradas justo dentro del llamado ask(...) . No tienen nombre, y por
lo tanto se denominan anónimas. Tales funciones no se pueden acceder fuera de ask (porque no
están asignadas a variables), pero eso es justo lo que queremos aquí.
Éste código aparece en nuestros scripts de manera muy natural, está en el archivo de comandos
de JavaScript.
Una función es un valor representando una “acción”
Valores regulares tales como cadena de caracteres o números representan los datos.
// Declaración de Función
function sum(a, b) {
return a + b;
}
JavaScript (JS) 60
asignación” = :
// Expresión de Función
let sum = function(a, b) {
return a + b;
};
Una Expresión de Función es creada cuando la ejecución la alcance y es utilizable desde ahí
en adelante.
Una vez que el flujo de ejecución pase al lado derecho de la asignación let sum = function… – aquí
vamos, la función es creada y puede ser usada (asignada, llamada, etc.) de ahora en adelante.
Las Declaraciones de Función son diferente.
Una Declaración de Función puede ser llamada antes de ser definida.
Por ejemplo, una Declaración de Función global es visible en todo el script, sin importar
dónde se esté.
Esto se debe a los algoritmos internos. Cuando JavaScript se prepara para ejecutar el script,
primero busca Declaraciones de Funciones globales en él y crea las funciones. Podemos pensar
en esto como una “etapa de inicialización”.
Y después de que se procesen todas las Declaraciones de Funciones, el código se ejecuta.
Entonces tiene acceso a éstas funciones.
Por ejemplo, esto funciona:
La Declaración de Función sayHi es creada cuando JavaScript está preparándose para iniciar el
script y es visible en todas partes.
…Si fuera una Expresión de Función, entonces no funcionaría:
Las Expresiones de Función son creadas cuando la ejecución las alcance. Esto podría pasar
solamente en la línea (*) . Demasiado tarde.
Otra característica especial de las Declaraciones de Funciones es su alcance de bloque.
Por ejemplo, imaginemos que necesitamos declarar una función welcome() dependiendo de la
variable age que obtengamos durante el tiempo de ejecución. Y luego planeamos usarlo algún
tiempo después.
function welcome() {
alert("Hola!");
}
} else {
function welcome() {
alert("Saludos!");
JavaScript (JS) 61
}
Esto se debe a que una Declaración de Función sólo es visible dentro del bloque de código en
el que reside.
function welcome() {
alert("¡Saludos!");
}
}
¿Qué podemos hacer para que welcome sea visible fuera de ‘if’?
El enfoque correcto sería utilizar una Expresión de Función y asignar welcome a la variable que
se declara fuera de ‘if’ y tiene la visibilidad adecuada.
Este código funciona según lo previsto:
let welcome;
welcome = function() {
alert("Hola!");
};
} else {
welcome = function() {
alert("Saludos!");
};
welcome(); // ahora ok
welcome(); // ahora ok
JavaScript (JS) 62
También es un poco más fácil de buscar function f(…) {…} en el código comparado con let f = function(…)
Resumen
Las funciones son valores. Se pueden asignar, copiar o declarar en cualquier lugar del
código.
Si la función se declara como una declaración separada en el flujo del código principal,
eso se llama “Declaración de función”.
En la mayoría de los casos, cuando necesitamos declarar una función, es preferible una
Declaración de Función, ya que es visible antes de la declaración misma. Eso nos da más
flexibilidad en la organización del código, y generalmente es más legible.
Por lo tanto, deberíamos usar una Expresión de Función solo cuando una Declaración de Función
no sea adecuada para la tarea. Hemos visto un par de ejemplos de eso en este capítulo, y
veremos más en el futuro.
Hay otra sintaxis muy simple y concisa para crear funciones, que a menudo es mejor que las
Expresiones de funciones.
Se llama “funciones de flecha”, porque se ve así:
Esto crea una función func que acepta los parámetros arg1..argN , luego evalúa la expression del
lado derecho mediante su uso y devuelve su resultado.
En otras palabras, es la versión más corta de:
alert( sum(1, 2) ); // 3
Como puedes ver, (a, b) => a + b significa una función que acepta dos argumentos llamados a y b .
Tras la ejecución, evalúa la expresión a + b y devuelve el resultado.
JavaScript (JS) 63
Si solo tenemos un argumento, se pueden omitir paréntesis alrededor de los parámetros, lo
que lo hace aún más corto.
Por ejemplo:
Si no hay parámetros, los paréntesis estarán vacíos; pero deben estar presentes:
sayHi();
Las funciones de flecha se pueden usar de la misma manera que las expresiones de función.
welcome();
Las funciones de flecha pueden parecer desconocidas y poco legibles al principio, pero eso
cambia rápidamente a medida que los ojos se acostumbran a la estructura.
Son muy convenientes para acciones simples de una línea, cuando somos demasiado flojos para
escribir muchas palabras.
A veces necesitamos una función más compleja, con múltiples expresiones o sentencias. En ese
caso debemos encerrarlos entre llaves. La diferencia principal es que las llaves necesitan
usar un return para devolver un valor (tal como lo hacen las funciones comunes).
Como esto:
alert( sum(1, 2) ); // 3
Resumen
Las funciones de flecha son útiles para acciones simples, especialmente las de una sola
línea. Vienen en dos variantes:
JavaScript (JS) 64
1. Sin llaves: (...args) => expression – el lado derecho es una expresión: la función la evalúa y
devuelve el resultado. Pueden omitirse los paréntesis si solo hay un argumento, por
ejemplo n => n*2 .
2. Con llaves: (...args) => { body } – las llaves nos permiten escribir varias declaraciones dentro
de la función, pero necesitamos un return explícito para devolver algo.
Especiales JavaScript
Este capítulo resume brevemente las características de JavaScript que hemos aprendido hasta
ahora, prestando especial atención a los detalles relevantes.
Estructura de Código
Las declaraciones se delimitan con un punto y coma:
alert('Hola'); alert('Mundo');
En general, un salto de línea también se trata como un delimitador, por lo que también
funciona:
alert('Hola')
alert('Mundo')
Esto se llama “inserción automática de punto y coma”. A veces no funciona, por ejemplo:
[1, 2].forEach(alert)
La mayoría de las guías de estilo de código coinciden en que debemos poner un punto y coma
después de cada declaración.
Los puntos y comas no son necesarios después de los bloques de código {...} y los
constructores de sintaxis como los bucles:
function f() {
// no se necesita punto y coma después de la declaración de función
}
for(;;) {
// no se necesita punto y coma después del bucle
}
…Pero incluso si colocásemos un punto y coma “extra” en alguna parte, eso no sería un error.
Solo sería ignorado.
Más en: Estructura del código.
Modo estricto
Para habilitar completamente todas las características de JavaScript moderno, debemos
comenzar los scripts con "use strict" .
'use strict';
...
JavaScript (JS) 65
Algunas características modernas del lenguaje (como las clases que estudiaremos en el futuro)
activan el modo estricto implícitamente.
Más en: El modo moderno, "use strict".
Variables
Se pueden declarar usando:
let
let x = 5;
x = "John";
null – un tipo con el valor único null , que significa “vacío” o “no existe”,
undefined – un tipo con el valor único undefined , que significa “no asignado”,
object y symbol – para estructuras de datos complejas e identificadores únicos, aún no los
hemos aprendido.
Interacción
Estamos utilizando un navegador como entorno de trabajo, por lo que las funciones básicas de
la interfaz de usuario serán:
prompt(question, [default]) Hace una pregunta question , y devuelve lo que ingresó el visitante o null si
presiona “cancelar”. confirm(question) Hace una pregunta question , y sugiere elegir entre Aceptar y
Cancelar. La elección se devuelve como booleano true/false . alert(message) Muestra un message .
Todas estas funciones son modales, pausan la ejecución del código y evitan que el visitante
interactúe con la página hasta que responda.
Por ejemplo:
JavaScript (JS) 66
alert( "Visitante: " + userName ); // Alice
alert( "Quiere té: " + isTeaWanted ); // true
Operadores
JavaScript soporta los siguientes operadores:
AritméticosLos normales: * + - / , también % para los restos y ** para aplicar potencia de un
número.
El binario más + concatena textos. Si uno de los operandos es un texto, el otro también se
convierte en texto:
Bucles
Cubrimos 3 tipos de bucles:
// 1
while (condition) {
...
}
// 2
do {
...
} while (condition);
JavaScript (JS) 67
// 3
for(let i = 0; i < 10; i++) {
...
}
La variable declarada en el bucle for(let...) sólo es visible dentro del bucle. Pero también
podemos omitir el let y reutilizar una variable existente.
Directivas break/continue permiten salir de todo el ciclo/iteración actual. Use etiquetas para
romper bucles anidados.
La construcción “switch”
La construcción “switch” puede reemplazar múltiples revisiones con if . “switch”
utiliza === (comparación estricta).
Por ejemplo:
switch (age) {
case 18:
case "18":
alert("¡Funciona!");
break;
default:
alert("Todo valor que no sea igual a uno de arriba");
}
Funciones
Cubrimos tres formas de crear una función en JavaScript:
function sum(a, b) {
let result = a + b;
return result;
}
return result;
};
3. Funciones de flecha:
JavaScript (JS) 68
// sin argumentos
let sayHi = () => alert("Hello");
Las funciones pueden tener variables locales: son aquellas declaradas dentro de su cuerpo.
Estas variables solo son visibles dentro de la función.
Las funciones siempre devuelven algo. Si no hay return , entonces el resultado es undefined .
El panel “sources/recursos”
Tu version de Chrome posiblemente se vea distinta, pero sigue siendo obvio lo que hablamos
aquí.
1. La Zona de recursos lista los archivos HTML, JavaScript, CSS y otros, incluyendo imágenes
que están incluidas en la página. Las extensiones de Chrome quizás también aparezcan aquí.
Ahora puedes hacer click en el mismo botón de activación otra vez para esconder la lista de
recursos y darnos más espacio.
Consola
Si presionamos Esc , la consola se abrirá debajo. Podemos escribir los comandos y
presionar Enter para ejecutar.
Después de que se ejecuta una sentencia, el resultado se muestra debajo.
JavaScript (JS) 69
Por ejemplo, aquí 1+2 da el resultado 3 , mientras que la llamada a función hello("debugger") no
devuelve nada, entonces el resultado es undefined :
…y mucho más.
Breakpoints Condicionales
El comando “debugger”
También podemos pausar el código utilizando el comando debugger , así:
function hello(name) {
let phrase = `Hello, ${name}!`;
Este comando solo funciona cuando el panel de herramientas de desarrollo está abierto, de
otro modo el navegador lo ignora.
JavaScript (JS) 70
En el momento actual el debugger está dentro de la función hello() , llamada por un script
en index.html (no dentro de ninguna función, por lo que se llama “anonymous”).
Trazado de la ejecución
Ahora es tiempo de trazar el script.
Hay botones para esto en le panel superior derecho. Revisémoslos.
– “Reanudar”: continúa la ejecución, hotkey F8.Reanuda la ejecución. Si no hay breakpoints
adicionales, entonces la ejecución continúa y el debugger pierde el control.
Esto es lo que podemos ver al hacer click:
La ejecución continuó, alcanzando el siguiente breakpoint dentro de say() y pausándose allí.
Revisa el “Call stack” a la derecha. Ha incrementado su valor en una llamada. Ahora estamos
dentro de say() . – “Siguiente paso”: ejecuta el siguiente comando, hotkey F9.Ejecuta la
siguiente sentencia. Si la cliqueamos ahora, se mostrara alert .
Otro clic volverá a ejecutar otro comando, y así uno por uno, a través de todo el script. –
JavaScript (JS) 71
Esto es útil cuando queremos movernos múltiples pasos adelante, pero somos muy flojos como
para definir un breakpoint.
Logging
Para escribir algo en la consola, existe la función console.log .
Por ejemplo, esto muestra los valores desde el 0 hasta el 4 en la consola:
Los usuarios regulares no ven este output, ya que está en la consola. Para verlo, debemos
abrir la consola de desarrolladores y presionar la tecla Esc y en otro tab: se abrirá la
consola debajo.
Si tenemos suficiente log en nuestro código, podemos entonces ver lo que va pasando en
nuestro registro, sin el debugger.
Resumen
Como podemos ver, hay tres formas principales para pausar un script:
1. Un breakpoint.
2. La declaración debugger .
Cuando se pausa, podemos hacer “debug”: examinar variables y rastrear el código para ver
dónde la ejecución funciona mal.
Hay muchas más opciones en la consola de desarrollo que las que se cubren aquí. El manual
completo lo conseguimos en https://developers.google.com/web/tools/chrome-devtools.
La información de este capítulo es suficiente para debuggear, pero luego, especialmente si
hacemos muchas cosas con el explorador, por favor revisa las capacidades avanzadas de la
consola de desarrolladores.
Ah, y también puedes hacer click en todos lados en la consola a ver qué pasa. Esta es
probablemente la ruta más rápida para aprender a usar la consola de desarrolladores. ¡Tampoco
olvides el click derecho!
Estilo de codificación
Nuestro código debe ser lo más limpio y fácil de leer como sea posible.
Ese es en realidad el arte de la programación: tomar una tarea compleja y codificarla de
manera correcta y legible para los humanos. Un buen estilo de código ayuda mucho en eso.
Sintaxis
Aquí hay una hoja de ayuda con algunas reglas sugeridas (ver abajo para más detalles):
JavaScript (JS) 72
Ahora discutamos en detalle las reglas y las razones para ellas.
No existen reglas “usted debe”
Nada está escrito en piedra aquí. Estos son preferencias de estilos, no dogmas religiosos.
Llaves
En la mayoría de proyectos de Javascript las llaves están escritas en estilo “Egipcio” con la
llave de apertura en la misma linea como la correspondiente palabra clave – no en una nueva
linea. Debe haber también un espacio después de la llave de apertura, como esto:
if (condition) {
// hacer esto
// ...y eso
// ...y eso
}
Una construcción de una sola línea, como if (condition) doSomething() , es un caso límite importante.
¿Deberíamos usar llaves?
Aquí están las variantes anotadas para que puedas juzgar la legibilidad por ti mismo.
1. 😠 Los principiantes a veces hacen eso. ¡Malo! Las llaves no son necesarias:
if (n < 0) {alert(`Power ${n} is not supported`);}
2. 😠 Dividir en una línea separada sin llaves. Nunca haga eso, es fácil cometer un error al
agregar nuevas líneas:
if (n < 0)
alert(`Power ${n} is not supported`);
JavaScript (JS) 73
4. 😃 La mejor variante:
if (n < 0) {
alert(`Power ${n} is not supported`);
}
Para un código muy breve, se permite una línea, p. if (cond) return null . Pero un bloque de código
(la última variante) suele ser más legible.
Tamaño de línea
A nadie le gusta leer una larga línea horizontal de código. Es una buena práctica dividirlos.
Por ejemplo:
Y para sentencias if :
if (
id === 123 &&
moonPhase === 'Waning Gibbous' &&
zodiacSign === 'Libra'
) {
letTheSorceryBegin();
}
La longitud máxima de la línea debe acordarse con el equipo de trabajo. Suele tener 80 o 120
caracteres.
Indentación (sangría)
Hay dos tipos de indentación:
show(parameters,
aligned, // 5 espacios de relleno a la izquierda
one,
after,
another
) {
// ...
}
function pow(x, n) {
let result = 1;
// <--
for (let i = 0; i < n; i++) {
JavaScript (JS) 74
result *= x;
}
// <--
return result;
}
Insertar una nueva línea extra donde ayude a hacer el código mas legible. No debe haber
más de nueve líneas de código sin una indentación vertical.
Punto y coma
Debe haber un punto y coma después de cada declaración, incluso si se puede omitir.
Hay idiomas en los que un punto y coma es realmente opcional y rara vez se usa. Sin embargo,
en JavaScript, hay casos en los que un salto de línea no se interpreta como un punto y coma,
lo que deja el código vulnerable a errores. Vea más sobre eso en el capítulo Estructura del
código.
Si eres un programador de JavaScript experimentado, puedes elegir un estilo de código sin
punto y coma como StandardJS. De lo contrario, es mejor usar punto y coma para evitar
posibles escollos. La mayoría de los desarrolladores ponen punto y coma.
Niveles anidados
Intenta evitar anidar el código en demasiados niveles de profundidad.
Algunas veces es buena idea usar la directiva “continue” en un bucle para evitar anidamiento
extra.
Por ejemplo, en lugar de añadir un if anidado como este:
Podemos escribir:
function pow(x, n) {
if (n < 0) {
alert("Negative 'n' not supported");
} else {
let result = 1;
return result;
}
}
Opción 2:
function pow(x, n) {
if (n < 0) {
alert("Negative 'n' not supported");
return;
}
JavaScript (JS) 75
let result = 1;
return result;
}
El segundo es más legible porque el “caso especial” de n < 0 se maneja desde el principio. Una
vez que se realiza la verificación, podemos pasar al flujo de código “principal” sin la
necesidad de anidamiento adicional.
Colocación de funciones
Si está escribiendo varias funciones “auxiliares” y el código que las usa, hay tres formas de
organizar las funciones.
function setHandler(elem) {
...
}
function walkAround() {
...
}
function setHandler(elem) {
...
}
function walkAround() {
...
}
Guías de estilo
Una guía de estilo contiene reglas generales sobre “cómo escribir” el código, cuáles comillas
usar, cuántos espacios para indentar, la longitud máxima de la línea, etc. Muchas cosas
menores.
JavaScript (JS) 76
Cuando todos los miembros de un equipo usan la misma guía de estilo, el código se ve
uniforme, independientemente de qué miembro del equipo lo haya escrito.
Por supuesto, un equipo siempre puede escribir su propia guía de estilo, pero generalmente no
es necesario. Hay muchas guías existentes para elegir.
Idiomatic.JS
StandardJS
(y mucho mas)
Si eres un desarrollador novato, puedes comenzar con la guía al comienzo de este capítulo.
Luego, puedes buscar otras guías de estilo para recoger más ideas y decidir cuál te gusta
más.
Linters automatizados
Linters son herramientas que pueden verificar automáticamente el estilo de su código y hacer
sugerencias de mejora.
Lo mejor de ellos es que la comprobación de estilo también puede encontrar algunos errores,
como errores tipográficos en nombres de variables o funciones. Debido a esta característica,
se recomienda usar un linter incluso si no desea apegarse a un “estilo de código” en
particular.
Aquí hay algunas herramientas de linting conocidas:
1. Instala Node.JS.
2. Instala ESLint con el comando npm install -g eslint (npm es un instalador de paquetes de
Javascript).
{
"extends": "eslint:recommended",
"env": {
"browser": true,
"node": true,
"es6": true
},
"rules": {
"no-console": 0,
"indent": 2
}
}
JavaScript (JS) 77
Aquí la directiva "extends" denota que la configuración se basa en el conjunto de
configuraciones “eslint: recomendado”. Después de eso, especificamos el nuestro.
Resumen
Todas las reglas de sintaxis descritas en este capítulo (y en las guías de estilo
mencionadas) tienen como objetivo aumentar la legibilidad de su código. Todos ellos son
discutibles.
Cuando pensamos en escribir un código “mejor”, las preguntas que debemos hacernos son: “¿Qué
hace que el código sea más legible y fácil de entender?” y “¿Qué puede ayudarnos a evitar
errores?” Estas son las principales cosas a tener en cuenta al elegir y debatir estilos de
código.
La lectura de guías de estilo populares le permitirá mantenerse al día con las últimas ideas
sobre las tendencias de estilo de código y las mejores prácticas.
Comentarios
Como hemos aprendido en el capítulo Estructura del código, los comentarios pueden ser de una
sola línea: comenzando con // y de múltiples líneas: /* ... */ .
Normalmente los usamos para describir cómo y por qué el código funciona.
A primera vista, los comentarios pueden ser obvios, pero los principiantes en programación
generalmente los usan incorrectamente.
Comentarios incorrectos
Los principiantes tienden a utilizar los comentarios para explicar “lo que está pasando en el
código”. Así:
function showPrimes(n) {
nextPrime:
for (let i = 2; i < n; i++) {
JavaScript (JS) 78
La mejor variante, con una función externa isPrime :
function showPrimes(n) {
function isPrime(n) {
for (let i = 2; i < n; i++) {
if (n % i == 0) return false;
}
return true;
}
// ...
Entonces, una versión mejor puede ser reescribirlo en funciones de esta manera:
addWhiskey(glass);
addJuice(glass);
function addWhiskey(container) {
for(let i = 0; i < 10; i++) {
let drop = getWhiskey();
//...
}
}
function addJuice(container) {
for(let t = 0; t < 3; t++) {
let tomato = getTomato();
//...
}
}
De nuevo, la propias funciones nos dicen qué está pasando. No hay nada que comentar. Y
además, la estructura del código es mejor cuando está dividida. Queda claro qué hace cada
función, qué necesita y qué retorna.
En realidad, no podemos evitar totalmente los comentarios “explicativos”. Existen algoritmos
complejos. Y existen “trucos” ingeniosos con el propósito de optimizar. Pero generalmente,
tenemos que intentar mantener el código simple y auto descriptivo.
Comentarios correctos
JavaScript (JS) 79
Entonces, los comentarios explicativos suelen ser incorrectos. ¿Qué comentarios son
correctos?
/**
* Devuelve x elevado a la potencia de n.
*
* @param {number} x El número a elevar.
* @param {number} n La potencia, debe ser un número natural.
* @return {number} x elevado a la potencia de n.
*/
function pow(x, n) {
...
}
Este tipo de comentarios nos permite entender el propósito de la función y cómo usarla de la
manera correcta sin tener que examinar su código.
Por cierto, muchos editores como WebStorm también pueden entenderlos y usarlos para proveer
auto completado y algún tipo de verificación automática para el código.
Además, existen herramientas como JSDoc 3 que pueden generar documentación en formato HTML de
los comentarios. Puedes leer más información sobre JSDoc en https://jsdoc.app.
¿Por qué se resuelve de esa manera?Lo que está escrito es importante. Pero lo que no está
escrito puede ser aún más importante para entender qué está pasando. ¿Por qué resuelven la
tarea exactamente de esa manera? El código no nos da ninguna respuesta.
Si hay muchas maneras de resolver el problema, ¿por qué esta? Especialmente cuando no es la
más obvia.
Sin dichos comentarios, las siguientes situaciones son posibles:
1. Tú (o tu compañero) abres el código escrito hace ya algún tiempo, y te das cuenta de que
es “subóptimo”.
2. Piensas: “Que estúpido que era antes, y que inteligente que soy ahora”, y lo reescribes
utilizando la variante “más obvia y correcta”.
3. …El impulso de reescribir era bueno. Pero en el proceso ves que la solución “más obvia” en
realidad falla. Incluso recuerdas vagamente el porqué, porque ya lo intentaste hace mucho.
Vuelves a la variante correcta, pero has estado perdiendo el tiempo.
Los comentarios que explican la solución correcta son muy importantes. Nos ayudan a continuar
el desarrollo de forma correcta.¿Alguna característica sutil del código? ¿Dónde se usan?Si el
código tiene algo sutil y contraintuitivo, definitivamente vale la pena comentarlo.
Resumen
Una señal importante de un buen desarrollador son los comentarios: su presencia e incluso su
ausencia.
Los buenos comentarios nos permiten mantener bien el código, volver después de un retraso y
usarlo de manera más efectiva.
Comenta esto:
Utilización de funciones.
Evita comentarios:
JavaScript (JS) 80
Escríbelos solo si es imposible escribir el código de manera tan simple y auto descriptiva
que no los necesite.
Los comentarios también son usados para herramientas de auto documentación como JSDoc3: los
leen y generan documentación en HTML (o documentos en otros formatos).
Código ninja
Los programadores ninjas del pasado usaron estos trucos para afilar la mente de los
mantenedores de código.
Los gurús de revisión de código los buscan en tareas de prueba.
Los desarrolladores novatos algunas veces los usan incluso mejor que los programadores
ninjas.
Léelos detenidamente y encuentra quién eres: ¿un ninja?, ¿un novato?, o tal vez ¿un revisor
de código?
IRONÍA detectada
Muchos intentan seguir los caminos de los ninjas. Pocos tienen éxito.
Fascinante, ¿cierto?. Si escribes de esa forma, un desarrollador que se encuentre esta línea
e intente entender cuál es el valor de i la va a pasar muy mal. Por lo que tendrá que venir a
ti, buscando una respuesta.
Diles que mientras más corto mucho mejor. Guíalos a los caminos del ninja.
Otra forma de programar más rápido es usando variables de una sola letra en todas partes.
Como a , b o c .
Una variable corta desaparece en el código como lo hace un ninja en un bosque. Nadie será
capaz de encontrarla usando “buscar” en el editor. E incluso si alguien lo hace, no será
capaz de “descifrar” el significado de a o b .
…Pero hay una excepción. Un verdadero ninja nunca usaría i como el contador en un
bucle "for" . En cualquier otro lugar, pero no aquí. Mira alrededor, hay muchas más letras
exóticas. Por ejemplo, x o y .
Una variable exótica como el contador de un bucle es especialmente genial si el cuerpo del
bucle toma 1-2 páginas (hazlo más grande si puedes). Entonces si alguien mira en las
profundidades del bucle, no será capaz de figurarse rápidamente que la variable llamada x es
el contador del bucle.
Usa abreviaciones
JavaScript (JS) 81
Si las reglas del equipo prohíben el uso de nombres de una sola letra o nombres vagos –
acórtalos, haz abreviaciones.
Como esto:
userAgent → ua .
…etc
Solo aquel con buena intuición será capaz de entender dichos nombres. Intenta acortar todo.
Solo una persona digna debería ser capaz de sostener el desarrollo de tu código.
El nombre ideal para una variable es data . Úsalo lo más que puedas. En efecto, toda
variable contiene data, ¿no?
…¿Pero qué hacer si data ya está siendo usado? Intenta con valor , también es universal.
Después de todo, una variable eventualmente recibe un valor .
…Pero, ¿Y si ya no hay más de tales nombres? Simplemente añade un número: data1, item2, elem5 …
Prueba de atención
Solo un programador realmente atento debería ser capaz de entender tu código. Pero, ¿cómo
comprobarlo? ``
Una de las maneras – usa nombre de variables similares, como date y data .
Combínalos donde puedas.
Una lectura rápida de dicho código se hace imposible. Y cuando hay un error de tipografía….
Ummm… Estamos atrapados por mucho tiempo, hora de tomar té.
Sinónimos inteligentes
El Tao que puede ser expresado no es el Tao eterno. El nombre que puede
ser nombrado no es el nombre eterno.
Lao Tse (Tao Te Ching)
JavaScript (JS) 82
Usando nombres similares para las mismas cosas hace tu vida mas interesante y le muestra al
público tu creatividad.
Por ejemplo, considera prefijos de funciones. Si una función muestra un mensaje en la
pantalla – comiénzalo con mostrar... , como mostarMensaje . Y entonces si otra función muestra en la
pantalla otra cosa, como un nombre de usuario, comiénzalo con presentar... (como presentarNombre ).
Insinúa que hay una diferencia sutil entre dichas funciones, cuando no lo hay.
Haz un pacto con tus compañeros ninjas del equipo: si John comienza funciones de “mostrar”
con presentar... en su código, entonces Peter podría usar exhibir.. , y Ann – pintar... . Nota como el
código es mucho más interesante y diverso ahora.
Reutilizar nombres
Una vez que el todo se divide, las partesnecesitan nombres.Ya hay
suficientes nombres.Uno debe saber cuándo parar.
Laozi (Tao Te Ching)
function ninjaFunction(elem) {
// 20 líneas de código trabajando con elem
elem = clone(elem);
Un colega programador que quiera trabajar con elem en la segunda mitad de la función será
sorprendido… ¡Solo durante la depuración, después de examinar el código encontrara que está
trabajando con un clon!
JavaScript (JS) 83
Matarás dos pájaros de un solo tiro. Primero, el código se hará más largo y menos legible, y
segundo, un colega desarrollador podría gastar una gran cantidad de tiempo intentado entender
el significado del guion bajo.
Un ninja inteligente coloca los guiones bajos en un solo lugar del código y los evita en
otros lugares. Eso hace que el código sea mucho más frágil y aumenta la probabilidad de
errores futuros.
Muestra tu amor
¡Deja que todos vean cuán magníficas son tus entidades! Nombres
como superElement , megaFrame and niceItem iluminaran sin duda al lector.
En efecto, por una parte, algo es escrito: super.. , mega.. , nice.. , pero por otra parte – no da
ningún detalle. Un lector podría decidir mirar por un significado oculto y meditar por una
hora o dos.
Usa los mismos nombres para variables dentro y fuera de una función. Así de simple. Sin el
esfuerzo de inventar nuevos nombres.
function render() {
let user = anotherValue();
...
...many lines...
...
... // <-- un programador quiere trabajar con user aquí y...
...
}
Un programador que se adentra en render probablemente no notara que hay un user local opacando
al de afuera.
Entonces intentaran trabajar con user asumiendo que es la variable externa, el resultado
de authenticateUser() … ¡Se activa la trampa! Hola, depurador…
¡Muestra tu pensamiento original! Deja que la llamada de comprobarPermiso retorne no true/false sino
un objeto complejo con los resultados de tu comprobación.
¡Funciones poderosas!
JavaScript (JS) 84
El gran Tao fluye por todas partes,tanto a la izquierda como a la
derecha.
Laozi (Tao Te Ching)
Resumen
Todos los consejos anteriores fueron extraidos de código real… Algunas veces, escrito por
desarrolladores experimentados. Quizás incluso más experimentado que tú ;)
Sigue muchos de ellos, y tu código será realmente tuyo, nadie querrá cambiarlo.
Sigue todos, y tu código será una lección valiosa para desarrolladores jóvenes buscando
iluminación.
Los tests automáticos serán usados en tareas que siguen, y son ampliamente usados en
proyectos reales.
JavaScript (JS) 85
BDD son tres cosas en uno: tests, documentación y ejemplos.
describe("pow", function() {
});
El flujo de desarrollo
El flujo de desarrollo se ve así:
1. Se escribe una especificación inicial, con tests para la funcionalidad más básica.
5. Añadimos más casos de uso a la spec, seguramente no soportados aún por la implementación.
Los tests empiezan a fallar.
JavaScript (JS) 86
verificar que funcionan (van a fallar todos).
La spec en acción
En este tutorial estamos usando las siguientes librerías JavaScript para los tests:
Mocha – el framework central: provee funciones para test comunes como describe e it y la
función principal que ejecuta los tests.
Chai – una librería con muchas funciones de comprobación (assertions). Permite el uso de
diferentes comprobaciones. De momento usaremos assert.equal .
Sinon – una librería para espiar funciones, emular funciones incorporadas al lenguaje y
más. La necesitaremos a menudo más adelante.
Estas librerías son adecuadas tanto para tests en el navegador como en el lado del servidor.
Aquí nos enfocaremos en el navegador.
<!DOCTYPE html><html><head><!-- incluir css para mocha, para mostrar los resultados -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/mocha/3.2.0/mocha.css"><!-- incluir el código del framework moch
<script src="https://cdnjs.cloudflare.com/ajax/libs/mocha/3.2.0/mocha.js"></script><script>
mocha.setup('bdd'); // configuración mínima
</script><!-- incluir chai -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/chai/3.5.0/chai.js"></script><script>
// chai tiene un montón de cosas, hacemos assert global
let assert = chai.assert;
</script></head><body><script>
function pow(x, n) {
/* código a escribir de la función, de momento vacío */
}
</script><!-- el script con los tests (describe, it...) -->
<script src="test.js"></script><!-- el elemento con id="mocha" que contiene los resultados de los tests -->
<div id="mocha"></div><!-- ¡ejectuar los tests! -->
<script>
mocha.run();
</script></body></html>
2. El <script> con la función a comprobar, en nuestro caso con el código de pow .
3. Los tests – en nuestro caso un fichero externo test.js que contiene un sentencia describe("pow",
...) al inicio.
El resultado:
De momento, el test falla. Es lógico: tenemos el código vacío en la función pow , así
que pow(2,3) devuelve undefined en lugar de 8 .
Para más adelante, ten en cuenta que hay avanzados test-runners (Herramientas para ejecutar
los test en diferentes entornos de forma automática), como karma y otros. Por lo que
generalmente no es un problema configurar muchos tests diferentes.
Implementación inicial
Vamos a realizar una implementación simple de pow , suficiente para pasar el test:
function pow(x, n) {
return 8; // :) ¡hacemos trampas!
}
¡Ahora funciona!
JavaScript (JS) 87
Mejoramos el spec
Lo que hemos hecho es una trampa. La función no funciona bien: ejecutar un cálculo diferente,
como pow(3,4) , nos devuelve un resultado incorrecto, pero el test pasa.
… pero la situación es habitual, ocurre en la práctica. Los tests pasan, pero la función no
funciona bien. Nuestra especificación está incompleta. Necesitamos añadir más casos de uso a
la especificación.
Vamos a incluir un test para ver si pow(3,4) = 81 .
Podemos escoger entre dos formas de organizar el test:
describe("pow", function() {
});
describe("pow", function() {
});
Hacer los tests separados es útil para recoger información sobre qué está pasando, de forma
que la segunda manera es mejor.
Mejoramos la implementación
Vamos a escribir algo más real para que pasen los tests:
function pow(x, n) {
let result = 1;
return result;
}
JavaScript (JS) 88
Para estar seguros de que la función trabaja bien, vamos a hacer comprobaciones para más
valores. En lugar de escribir bloques it manualmente, vamos a generarlos con un for :
describe("pow", function() {
function makeTest(x) {
let expected = x * x * x;
it(`${x} elevado a 3 es ${expected}`, function() {
assert.equal(pow(x, 3), expected);
});
}
});
El resultado:
Describe anidados
Vamos a añadir más tests. Pero antes, hay que apuntar que la función makeTest y la
instrucción for deben ser agrupados juntos. No queremos makeTest en otros tests, solo se
necesita en el for : su tarea común es comprobar cómo pow eleva a una potencia concreta.
Agrupar tests se realiza con describe :
describe("pow", function() {
});// ... otros test irían aquí, se puede escribir describe como it
});
El describe anidado define un nuevo subgrupo de tests. En la salida podemos ver la indentación
en los títulos:
En el futuro podemos añadir más it y describe en el primer nivel con funciones de ayuda para
ellos mismos, no se solaparán con makeTest .
describe("test", function() {
});
JavaScript (JS) 89
Inicio testing – antes de todos los tests (before)
Antes de un test – entramos al test (beforeEach)
1
Después de un test – salimos del test (afterEach)
Antes de un test – entramos al test (beforeEach)
2
Después de un test – salimos del test (afterEach)
Final testing – después de todos los tests (after)
describe("pow", function() {
// ...
});
function pow(x, n) {
if (n < 0) return NaN;
if (Math.round(n) != n) return NaN;let result = 1;
return result;
}
JavaScript (JS) 90
Ahora funciona y todos los tests pasan:
Abre el ejemplo final en un sandbox.
Resumen
En BDD, la especificación va primero, seguida de la implementación. Al final tenemos tanto la
especificación como la implementación.
El spec puede ser usado de tres formas:
2. Como Docs – los títulos de los describe e it nos dicen lo que la función hace.
3. Como Ejemplos – los tests son también ejemplos funcionales que muestran cómo una función
puede ser usada.
Esto es especialmente importante en proyectos largos cuando una función es usada en muchos
sitios. Cuando cambiamos una función, no hay forma manual de comprobar si cada sitio donde se
usaba sigue funcionando correctamente.
1. Realizar el cambio como sea. Luego nuestros usuarios encontrarán errores porque
probablemente fallemos en encontrarlos.
2. O, si el castigo por errores es duro, la gente tendrá miedo de hacer cambios en las
funciones. Entonces el código envejecerá, nadie querrá meterse en él y eso no es bueno
para el desarrollo.
Polyfills y transpiladores
JavaScript (JS) 91
Una buena página para ver el estado actual de soporte de características del lenguaje
es https://kangax.github.io/compat-table/es6/ (es grande, todavía tenemos mucho que
aprender).
Como programadores, queremos las características más recientes. Cuanto más, ¡mejor!
Por otro lado, ¿cómo hacer que nuestro código moderno funcione en intérpretes más viejos que
aún no entienden las características más nuevas?
Hay dos herramientas para ello:
1. Transpiladores
2. Polyfills.
Transpiladores
Un transpilador es un software que traduce un código fuente a otro código fuente. Puede
analizar (“leer y entender”) código moderno y rescribirlo usando sintaxis y construcciones
más viejas para que también funcione en intérpretes antiguos.
Por ejemplo, antes del año 2020 JavaScript no tenía el operador “nullish coalescing” ?? .
Entonces, si un visitante lo usa en un navegador desactualizado, este fallaría en entender un
código como height = height ?? 100 .
Un transpilador analizaría nuestro código y rescribiría height ?? 100 como
Ahora el código rescrito es apto para los intérpretes de JavaScript más viejos.
Usualmente, un desarrollador ejecuta el transpilador en su propia computadora y luego
despliega el código transpilado al servidor.
Polyfills
Nuevas características en el lenguaje pueden incluir no solo construcciones sintácticas y
operadores, sino también funciones integradas.
Por ejemplo, Math.trunc(n) es una función que corta la parte decimal de un número,
ej. Math.trunc(1.23) devuelve 1 .
En algunos (muy desactualizados) intérpretes JavaScript no existe Math.trunc , así que tal código
fallará.
Aquí estamos hablando de nuevas funciones, no de cambios de sintaxis. No hay necesidad de
transpilar nada. Solo necesitamos declarar la función faltante.
Un script que actualiza o agrega funciones nuevas es llamado “polyfill”. Este llena los
vacíos agregando las implementaciones que faltan.
En este caso particular, el polyfill para Math.trunc es un script que lo implementa:
JavaScript (JS) 92
if (!Math.trunc) { // no existe tal función
// implementarla
Math.trunc = function(number) {
// Math.ceil y Math.floor existen incluso en los intérpretes antiguos
// los cubriremos luego en el tutorial
return number < 0 ? Math.ceil(number) : Math.floor(number);
};
}
JavaScript es un lenguaje muy dinámico, los scripts pueden agregar o modificar cualquier
función, incluso las integradas.
Dos librerías interesantes de polyfills son:
core js – da muchísimo soporte, pero permite que se incluyan solamente las características
necesitadas.
Resumen
En este artículo queremos motivarte a estudiar las características más modernas y hasta
experimentales del lenguaje, incluso si aún no tienen buen soporte en los intérpretes
JavaScript.
Pero no olvides usar transpiladores (si usas sintaxis u operadores modernos) y polyfills
(para añadir funciones que pueden estar ausentes). Ellos se asegurarán de que el código
funcione.
Por ejemplo, cuando estés más familiarizado con JavaScript puedes configurar la construcción
de código basado en webpack con el plugin babel-loader.
Buenos recursos que muestran el estado actual de soporte para varias característica:
P.S. Google Chrome usualmente es el más actualizado con las características del lenguaje,
pruébalo si algún demo del tutorial falla. Aunque la mayoría de los demos funciona con
cualquier navegador moderno.
Objetos: lo básico
Objetos
Como aprendimos en el capítulo Tipos de datos, hay ocho tipos de datos en JavaScript. Siete
de ellos se denominan “primitivos”, porque sus valores contienen solo un dato (sea un string ,
un número o lo que sea).
En contraste, los objetos son usados para almacenar colecciones de varios datos y entidades
más complejas asociados con un nombre clave. En JavaScript, los objetos penetran casi todos
los aspectos del lenguaje. Por lo tanto, debemos comprenderlos primero antes de profundizar
en cualquier otro lugar.
Podemos crear un objeto usando las llaves {…} con una lista opcional de propiedades. Una
propiedad es un par “key:value”, donde key es un string (también llamado “nombre clave”),
y value puede ser cualquier cosa. P.D. Para fines prácticos de la lección, nos referiremos a
este par de conceptos como “clave:valor”.
Podemos imaginar un objeto como un gabinete con archivos firmados. Cada pieza de datos es
almacenada en su archivo por la clave. Es fácil encontrar un archivo por su nombre o
agregar/eliminar un archivo.
JavaScript (JS) 93
Se puede crear un objeto vacío (“gabinete vacío”) utilizando una de estas dos sintaxis:
Literales y propiedades
Podemos poner inmediatamente algunas propiedades dentro de {...} como pares “clave:valor”:
Una propiedad tiene una clave (también conocida como “nombre” o “identificador”) antes de los
dos puntos ":" y un valor a la derecha.
En el objeto user hay dos propiedades:
Podemos imaginar al objeto user resultante como un gabinete con dos archivos firmados con las
etiquetas “name” y “age”.
JavaScript (JS) 94
alert( user.age ); // 30
user.isAdmin = true;
delete user.age;
También podemos nombrar propiedades con más de una palabra. Pero, de ser así, debemos colocar
la clave entre comillas "..." :
let user = {
name: "John",
age: 30,
"likes birds": true // Las claves con más de una palabra deben ir entre comillas
};
let user = {
name: "John",
age: 30,}
JavaScript (JS) 95
Eso se llama una coma “final” o “colgante”. Facilita agregar, eliminar y mover propiedades,
porque todas las líneas se vuelven similares.
Corchetes
La notación de punto no funciona para acceder a propiedades con claves de más de una palabra:
JavaScript no entiende eso. Piensa que hemos accedido a user.likes y entonces nos da un error de
sintaxis cuando aparece el inesperado birds .
El punto requiere que la clave sea un identificador de variable válido. Eso implica que: no
contenga espacios, no comience con un dígito y no incluya caracteres especiales ( $ y _ sí se
permiten).
Existe una “notación de corchetes” alternativa que funciona con cualquier string:
// asignando
user["likes birds"] = true;
// obteniendo
alert(user["likes birds"]); // true
// eliminando
delete user["likes birds"];
Ahora todo está bien. Nota que el string dentro de los corchetes está adecuadamente entre
comillas (cualquier tipo de comillas servirían).
Las llaves también nos proveen de una forma para obtener la clave de la propiedad como
resultado de cualquier expresión como una variable – en lugar de una cadena literal – de la
siguiente manera:
Aquí la variable key puede calcularse en tiempo de ejecución o depender de la entrada del
usuario y luego lo usamos para acceder a la propiedad. Eso nos da mucha flexibilidad.
Por ejemplo:
let user = {
name: "John",
age: 30
};
let user = {
name: "John",
age: 30
};
JavaScript (JS) 96
Propiedades calculadas
Podemos usar corchetes en un objeto literal al crear un objeto. A esto se le
llama propiedades calculadas.
Por ejemplo:
let bag = {
[fruit]: 5, // El nombre de la propiedad se obtiene de la variable fruit};
El significado de una propiedad calculada es simple: [fruit] significa que se debe tomar la
clave de la propiedad fruit .
Los corchetes son mucho más potentes que la notación de punto. Permiten cualquier nombre de
propiedad, incluso variables. Pero también es más engorroso escribirlos.
Entonces, la mayoría de las veces, cuando los nombres de propiedad son conocidos y simples,
se utiliza el punto. Y si necesitamos algo más complejo, entonces cambiamos a corchetes.
En el ejemplo anterior las propiedades tienen los mismos nombres que las variables. El uso de
variables para la creación de propiedades es tán común que existe un atajo para valores de
propiedad especial para hacerla más corta.
En lugar de name:name , simplemente podemos escribir name , tal cual:
JavaScript (JS) 97
name, // igual que name:name
age, // igual que age:age
// ...
};}
let user = {
name, // igual que name:name
age: 30
};
En resumen, no hay limitaciones en los nombres de propiedades. Pueden ser cadenas o símbolos
(un tipo especial para identificadores que se cubrirán más adelante).
Por ejemplo, un número 0 se convierte en cadena "0" cuando se usa como clave de propiedad:
let obj = {
0: "test" // igual que "0": "test"
};
// ambos alerts acceden a la misma propiedad (el número 0 se convierte a una cadena "0")
alert( obj["0"] ); // test
alert( obj[0] ); // test (la misma propiedad)
Hay una pequeña sorpresa por una propiedad especial llamada __proto__ . No podemos establecerlo
dentro de un valor que no sea de objeto:
La lectura de una propiedad no existente solo devuelve undefined . Así que podemos probar
fácilmente si la propiedad existe:
JavaScript (JS) 98
let user = {};
alert( user.noSuchProperty === undefined ); // true significa que "no existe tal propiedad"
La sintaxis es:
"key" in object
Por ejemplo:
Nota que a la izquierda de in debe estar el nombre de la propiedad que suele ser un string
entre comillas.
Si omitimos las comillas, significa que es una variable. Esta variable debe almacenar la
clave real que será probada. Por ejemplo:
Pero… ¿Por qué existe el operador in ? ¿No es suficiente comparar con undefined ?
La mayoría de las veces las comparaciones con undefined funcionan bien. Pero hay un caso
especial donde esto falla y aún así "in" funciona correctamente.
let obj = {
test: undefined
};
alert( obj.test ); // es undefined, entonces... ¿Quiere decir realmente existe tal propiedad?
Situaciones como esta suceden raramente ya que undefined no debe ser explícitamente asignado.
Comúnmente usamos null para valores “desconocidos” o “vacíos”. Por lo que el operador in es
un invitado exótico en nuestro código.
El bucle "for..in"
Para recorrer todas las claves de un objeto existe una forma especial de bucle: for..in . Esto
es algo completamente diferente a la construcción for(;;) que estudiaremos más adelante.
La sintaxis:
JavaScript (JS) 99
let user = {
name: "John",
age: 30,
isAdmin: true
};
Nota que todas las construcciones “for” nos permiten declarar variables para bucle dentro del
bucle, como let key aquí.
Además podríamos usar otros nombres de variables en lugar de key . Por ejemplo, "for (let prop in
let codes = {
"49": "Germany",
"41": "Switzerland",
"44": "Great Britain",
// ..,
"1": "USA"
};
El objeto puede usarse para sugerir al usuario una lista de opciones. Si estamos haciendo un
sitio principalmente para el público alemán, probablemente queremos que 49 sea el primero.
Pero si ejecutamos el código, veremos una imagen totalmente diferente:
Los códigos telefónicos van en orden ascendente porque son números enteros. Entonces vemos 1,
41, 44, 49 .
…Por otro lado, si las claves no son enteras, se enumeran en el orden de creación, por
ejemplo:
let user = {
name: "John",
// Las propiedades que no son enteras se enumeran en el orden de creaciónfor (let prop in user) {
alert( prop ); // name, surname, age
}
Entonces, para solucionar el problema con los códigos telefónicos, podemos “hacer trampa”
haciendo que los códigos no sean enteros. Agregar un signo más "+" antes de cada código será
más que suficiente.
Justo así:
let codes = {
"+49": "Germany",
"+41": "Switzerland",
"+44": "Great Britain",
// ..,
"+1": "USA"
};
Resumen
Los objetos son arreglos asociativos con varias características especiales.
Operadores adicionales:
Para comprobar si existe una propiedad con la clave proporcionada: "key" in obj .
Para crear bucles sobre un objeto: bucle for (let key in obj) .
Lo que hemos estudiado en este capítulo se llama “objeto simple”, o solamente Object .
Hay muchos otros tipos de objetos en JavaScript:
…Y así.
Tienen sus características especiales que estudiaremos más adelante. A veces las personas
dicen algo como "Tipo array " o "Tipo date ", pero formalmente no son tipos en sí, sino que
pertenecen a un tipo de datos de “objeto” simple y lo amplían a varias maneras.
Los objetos en JavaScript son muy poderosos. Aquí acabamos de arañar la superficie de un tema
que es realmente enorme. Trabajaremos estrechamente con los objetos y aprenderemos más sobre
ellos en otras partes del tutorial.
Una de las diferencias fundamentales entre objetos y primitivos es que los objetos son
almacenados y copiados “por referencia”, en cambio los primitivos: strings, number, boolean,
etc.; son asignados y copiados “como un valor completo”.
Esto es fácil de entender si miramos un poco “bajo cubierta” de lo que pasa cuando copiamos
por valor.
Como resultado tenemos dos variables independientes, cada una almacenando la cadena "Hello!" .
Una variable no almacena el objeto mismo sino su “dirección en memoria”, en otras palabras
“una referencia” a él.
let user = {
name: "John"
};
Cuando ejecutamos acciones con el objeto, por ejemplo tomar una propiedad user.name , el motor
JavaScript busca aquella dirección y ejecuta la operación en el objeto mismo.
Ahora tenemos dos variables, cada una con una referencia al mismo objeto:
Como puedes ver, aún hay un objeto, ahora con dos variables haciendo referencia a él.
Podemos usar cualquiera de las variables para acceder al objeto y modificar su contenido:
admin.name = 'Pete'; // cambiado por la referencia "admin"alert(user.name); // 'Pete', los cambios se ven desde la referencia "user"
Es como si tuviéramos un gabinete con dos llaves y usáramos una de ellas ( admin ) para acceder
a él y hacer cambios. Si más tarde usamos la llave ( user ), estaríamos abriendo el mismo
gabinete y accediendo al contenido cambiado.
Por ejemplo, aquí a y b tienen referencias al mismo objeto, por lo tanto son iguales:
let a = {};
let b = a; // copia la referencia
Y aquí dos objetos independientes no son iguales, aunque se vean iguales (ambos están
vacíos):
let a = {};
let b = {}; // dos objetos independientes
alert( a == b ); // false
Los objetos
Un efecto importante de almacenar objetos como referencias es que un objeto declarado
como const puede ser modificado.
Por ejemplo:
const user = {
name: "John"
};
Puede parecer que la línea (*) causaría un error, pero no lo hace. El valor de user es
constante, este valor debe siempre hacer referencia al mismo objeto, pero las propiedades de
dicho objeto pueden cambiar.
En otras palabras: const user da un error solamente si tratamos de establecer user=... como un
todo.
Dicho esto, si realmente necesitamos hacer constantes las propiedades del objeto, también es
posible, pero usando métodos totalmente diferentes. Los mencionaremos en el
capítulo Indicadores y descriptores de propiedad.
Podemos crear un nuevo objeto y replicar la estructura del existente iterando a través de sus
propiedades y copiándolas en el nivel primitivo.
Como esto:
let user = {
name: "John",
age: 30
};
Object.assign(dest, ...sources)
Esto copia las propiedades de todos los objetos fuentes dentro del destino dest y lo devuelve
como resultado
let user = {
name: "John",
age: 30
};
Aquí, copia todas las propiedades de user en un objeto vacío y lo devuelve.
También hay otras formas de clonar un objeto, por ejemplo usando la sintaxis spread clone =
Clonación anidada
Hasta ahora supusimos que todas las propiedades de user eran primitivas. Pero las propiedades
pueden ser referencias a otros objetos.
Como esto:
let user = {
name: "John",
sizes: {
height: 182,
width: 50
}
};
Ahora no es suficiente copiar clone.sizes = user.sizes , porque user.sizes es un objeto y será copiado
por referencia. Entonces clone y user compartirán las mismas tallas (.sizes):
let user = {
name: "John",
sizes: {
height: 182,
width: 50
}
};
structuredClone
La llamada a structuredClone(object) clona el object con todas sus propiedadas anidadas.
Podemos usarlo en nuestro ejemplo:
let user = {
name: "John",
sizes: {
height: 182,
width: 50
}
};
El método structuredClone puede clonar la mayoría de los tipos de datos, como objetos, arrays,
valores primitivos.
También soporta referencias circulares, cuando una propiedad de objeto referencia el objeto
mismo (directamente o por una cadena de referencias).
Por ejemplo:
Como puedes ver, clone.me hace referencia a clone , no a user ! Así que la referencia circular fue
clonada correctamente también.
// error
structuredClone({
f: function() {}
});
Para manejar estos casos complejos podemos necesitar una combinación de métodos de clonación,
escribir código personalizado o, para no reinventar la rueda, tomar una implementación
existente, por ejemplo _.cloneDeep(obj) de la librería JavaScript lodash.
Resumen
Los objetos son asignados y copiados por referencia. En otras palabras, una variable almacena
no el valor del objeto sino una referencia (la dirección en la memoria) del valor. Entonces,
copiar tal variable o pasarla como argumento de función copia la referencia, no el objeto.
Todas la operaciones a través de referencias copiadas (como agregar y borrar propiedades) son
efectuadas en el mismo y único objeto .
Recolección de basura
Alcance
El concepto principal del manejo de memoria en JavaScript es alcance.
Puesto simple, los valores “alcanzables” son aquellos que se pueden acceder o utilizar de
alguna manera: Se garantiza que serán conservados en la memoria.
1. Hay un conjunto base de valores inherentemente accesibles, que no se pueden eliminar por
razones obvias.
Por ejemplo:
Variables Globales
2. Cualquier otro valor se considera accesible si se lo puede alcanzar desde una raíz por una
referencia o por una cadena de referencias.
Por ejemplo, si hay un objeto en una variable global, y ese objeto tiene una propiedad que
hace referencia a otro objeto, este objeto también se considera accesible. Y aquellos a
los que este objeto hace referencia también son accesibles. Ejemplos detallados a
continuación.
Un ejemplo sencillo
Aquí va el ejemplo más simple:
user = null;
Ahora John se vuelve inalcanzable. No hay forma de acceder a él, no hay referencias a él. El
recolector de basura desechará los datos y liberará la memoria.
Dos referencias
Ahora imaginemos que copiamos la referencia de user a admin :
user = null;
… el objeto todavía es accesible a través de la variable global admin , por lo que debe quedar
en la memoria. Si también sobrescribimos admin , entonces se puede eliminar.
Objetos entrelazados
Ahora un ejemplo más complejo. La familia:
return {
father: man,
mother: woman
La función marry “casa” dos objetos dándoles referencias entre sí y devuelve un nuevo objeto
que los contiene a ambos.
delete family.father;
delete family.mother.husband;
No es suficiente eliminar solo una de estas dos referencias, porque todos los objetos aún
serían accesibles.
Pero si eliminamos ambos, entonces podemos ver que John ya no tiene referencias entrantes:
Las referencias salientes no importan. Solo los entrantes pueden hacer que un objeto sea
accesible. Entonces, John ahora es inalcanzable y será eliminado de la memoria con todos sus
datos que también se volvieron inaccesibles.
family = null;
Es obvio que John y Ann todavía están vinculados, ambos tienen referencias entrantes. Pero
eso no es suficiente.
El antiguo objeto "family" se ha desvinculado de la raíz, ya no se hace referencia a él, por lo
que toda la isla se vuelve inalcanzable y se eliminará.
Algoritmos internos
El algoritmo básico de recolección de basura se llama “marcar y barrer”.
Luego visita los objetos marcados y marca sus referencias. Todos los objetos visitados son
recordados, para no visitar el mismo objeto dos veces en el futuro.
…Y así sucesivamente hasta que cada referencia alcanzable (desde las raíces) sean
visitadas.
Podemos ver claramente una “isla inalcanzable” al lado derecho. Ahora veamos cómo el
recolector de basura maneja “marcar y barrer”.
… luego se continúa con las referencias salientes de estos objetos, y se continúa marcando
mientras sea posible:
Hay otras optimizaciones y tipos de algoritmos de recolección de basura. Por mucho que quiera
describirlos aquí, tengo que evitarlo porque diferentes motores implementan diferentes
ajustes y técnicas. Y, lo que es aún más importante, las cosas cambian a medida que se
desarrollan los motores, por lo que probablemente no vale la pena profundizar sin una
necesidad real. Por supuesto, si tienes verdadero interés, a continuación hay algunos enlaces
para ti.
Resumen
Los principales puntos a saber:
Ser referenciado no es lo mismo que ser accesible (desde una raíz): un conjunto de objetos
interconectados pueden volverse inalcanzables como un todo, como vimos en el ejemplo de
arriba.
Un libro general “The Garbage Collection Handbook: The Art of Automatic Memory Management”
(R. Jones et al) cubre algunos de ellos.
Si estás familiarizado con la programación de bajo nivel, la información más detallada sobre
el recolector de basura V8 se encuentra en el artículo A tour of V8: Garbage Collection.
Los objetos son creados usualmente para representar entidades del mundo real, como usuarios,
órdenes, etc.:
let user = {
name: "John",
age: 30
};
Y en el mundo real un usuario puede actuar: seleccionar algo del carrito de compras, hacer
login, logout, etc.
Ejemplos de métodos
Para empezar, enseñemos al usuario user a decir hola:
let user = {
name: "John",
age: 30
};
user.sayHi = function() {
alert("¡Hola!");
};
user.sayHi(); // ¡Hola!
Aquí simplemente usamos una expresión de función para crear la función y asignarla a la
propiedad user.sayHi del objeto.
Entonces la llamamos con user.sayHi() . ¡El usuario ahora puede hablar!
Por supuesto, podríamos usar una función pre-declarada como un método, parecido a esto:
let user = {
// ...
};
// primero, declara
function sayHi() {
alert("¡Hola!");
};
user.sayHi(); // ¡Hola!
user = {
sayHi: function() {
alert("Hello");
}
};
A decir verdad, las notaciones no son completamente idénticas. Hay diferencias sutiles
relacionadas a la herencia de objetos (por cubrir más adelante) que por ahora no son
relevantes. En casi todos los casos la sintaxis abreviada es la preferida.
“this” en métodos
Es común que un método de objeto necesite acceder a la información almacenada en el objeto
para cumplir su tarea.
Por ejemplo, el código dentro de user.sayHi() puede necesitar el nombre del usuario user .
Para acceder al objeto, un método puede usar la palabra clave this .
El valor de this es el objeto “antes del punto”, el usado para llamar al método.
Por ejemplo:
let user = {
name: "John",
age: 30,
sayHi() {
// "this" es el "objeto actual"
alert(this.name);}
};
user.sayHi(); // John
Aquí durante la ejecución de user.sayHi() , el valor de this será user .
Técnicamente, también es posible acceder al objeto sin this , haciendo referencia a él por
medio de la variable externa:
let user = {
name: "John",
age: 30,
sayHi() {
alert(user.name); // "user" en vez de "this"}
};
…Pero tal código no es confiable. Si decidimos copiar user a otra variable, por ejemplo admin =
user y sobrescribir user con otra cosa, entonces accederá al objeto incorrecto.
Eso queda demostrado en las siguientes lineas:
sayHi() {
alert( user.name ); // lleva a un error}
};
Si usamos this.name en vez de user.name dentro de alert , entonces el código funciona.
“this” no es vinculado
En JavaScript, la palabra clave this se comporta de manera distinta a la mayoría de otros
lenguajes de programación. Puede ser usado en cualquier función, incluso si no es el método
de un objeto.
function sayHi() {
alert( this.name );
}
El valor de this es evaluado durante el tiempo de ejecución, dependiendo del contexto.
Por ejemplo, aquí la función es asignada a dos objetos diferentes y tiene diferentes “this”
en sus llamados:
function sayHi() {
alert( this.name );
}
La regla es simple: si obj.f() es llamado, entonces this es obj durante el llamado de f .
Entonces es tanto user o admin en el ejemplo anterior.
function sayHi() {
alert(this);
}
sayHi(); // undefined
En este caso this es undefined en el modo estricto. Si tratamos de acceder a this.name , habrá un
error.
En modo no estricto el valor de this en tal caso será el objeto global ( window en un navegador,
llegaremos a ello en el capítulo Objeto Global). Este es un comportamiento histórico que "use
strict" corrige.
El concepto de this evaluado en tiempo de ejecución tiene sus pros y sus contras. Por un lado,
una función puede ser reusada por diferentes objetos. Por otro, la mayor flexibilidad crea
más posibilidades para equivocaciones.
Nuestra posición no es juzgar si la decisión del diseño de lenguaje es buena o mala. Vamos a
entender cómo trabajar con ello, obtener beneficios y evitar problemas.
Por ejemplo, aquí arrow() usa this desde fuera del método user.sayHi() :
let user = {
firstName: "Ilya",
sayHi() {
let arrow = () => alert(this.firstName);
arrow();
}
};
user.sayHi(); // Ilya
Esto es una característica especial de las funciones de flecha, útil cuando no queremos
realmente un this separado sino tomarlo de un contexto externo. Más adelante en el
capítulo Funciones de flecha revisadas las trataremos en profundidad.
Resumen
Las funciones que son almacenadas en propiedades de objeto son llamadas “métodos”.
Cuando una función es declarada, puede usar this , pero ese this no tiene valor hasta que la
función es llamada.
Ten en cuenta que las funciones de flecha son especiales: ellas no tienen this . Cuando this es
accedido dentro de una función de flecha, su valor es tomado desde el exterior.
El sintaxis habitual {...} nos permite crear un objeto. Pero a menudo necesitamos crear varios
objetos similares, como múltiples usuarios, elementos de menú, etcétera.
Función constructora
Por ejemplo:
function User(name) {
this.name = name;
this.isAdmin = false;
}
Cuando una función es ejecutada con new , realiza los siguientes pasos:
function User(name) {
// this = {}; (implícitamente)// agrega propiedades a this
this.name = name;
this.isAdmin = false;
let user = {
name: "Jack",
isAdmin: false
};
Ahora si queremos crear otros usuarios, podemos llamar a new User("Ann") , new User("Alice") ,
etcétera. Mucho más corto que usar literales todo el tiempo y también fácil de leer.
Tomemos nota otra vez: técnicamente cualquier función (excepto las de flecha pues no tienen
this) puede ser utilizada como constructor. Puede ser llamada con new , y ejecutará el
algoritmo de arriba. La “primera letra mayúscula” es un acuerdo general, para dejar en claro
que la función debe ser ejecutada con new .
new function() { … }
Si tenemos muchas líneas de código todas sobre la creación de un único objeto complejo,
podemos agruparlas en un constructor de función que es llamado inmediatamente de esta manera:
Este constructor no puede ser llamado de nuevo porque no es guardado en ninguna parte, sólo
es creado y llamado. Por lo tanto este truco apunta a encapsular el código que construye el
objeto individual, sin reutilización futura.
function User() {
alert(new.target);
}
// sin "new":
User(); // undefined// con "new":
new User(); // function User { ... }
Esto puede ser utilizado dentro de la función para conocer si ha sido llamada con new , "en
modo constructor "; o sin él, “en modo regular”.
También podemos hacer que ambas formas de llamarla, con new y “regular”, realicen lo mismo:
function User(name) {
if (!new.target) { // si me ejecutas sin new
return new User(name); // ...Agregaré new por ti
}
this.name = name;
}
Este enfoque es utilizado aveces en las librerías para hacer el sintaxis más flexible. Así la
gente puede llamar a la función con o sin new y aún funciona.
Sin embargo, probablemente no sea algo bueno para usar en todas partes, porque
omitir new hace que sea un poco menos obvio lo que está sucediendo. Con new todos sabemos que
se está creando el nuevo objeto.
Si return es llamado con un objeto, entonces se devuelve tal objeto en vez de this .
En otras palabras, return con un objeto devuelve ese objeto, en todos los demás casos se
devuelve this .
function BigUser() {
this.name = "John";
Y aquí un ejemplo con un return vacío (o podemos colocar un primitivo después de él, no
importa):
this.name = "John";
Omitir paréntesis aquí no se considera “buen estilo”, pero la especificación permite esa
sintaxis.
Métodos en constructor
Utilizar constructor de funciones para crear objetos nos da mucha flexibilidad. La función
constructor puede tener argumentos que definan cómo construir el objeto y qué colocar dentro.
Por supuesto podemos agregar a this no sólo propiedades, sino también métodos.
Por ejemplo, new User(name) de abajo, crea un objeto con el name dado y el método sayHi :
function User(name) {
this.name = name;
this.sayHi = function() {
alert( "Mi nombre es: " + this.name );
};
}
Para crear objetos complejos existe una sintaxis más avanzada, classes, que cubriremos más
adelante.
Resumen
Las funciones Constructoras o, más corto, constructores, son funciones normales, pero
existe un común acuerdo para nombrarlas con la primera letra en mayúscula.
Las funciones Constructoras sólo deben ser llamadas utilizando new . Tal llamado implica la
creación de un this vacío al comienzo y devolver el this rellenado al final.
Después de aprender aquello, volvemos a los objetos y los cubrimos en profundidad en los
capítulos Prototipos y herencia y Clases.
El encadenamiento opcional ?. es una forma a prueba de errores para acceder a las propiedades
anidadas de los objetos, incluso si no existe una propiedad intermedia.
Como ejemplo, digamos que tenemos objetos user que contienen información de nuestros usuarios.
La mayoría de nuestros usuarios tienen la dirección en la propiedad user.address , con la calle
en user.address.street , pero algunos no la proporcionaron.
En tal caso, cuando intentamos obtener user.address.street en un usuario sin dirección obtendremos
un error:
alert(user.address.street); // Error!
Este es el resultado esperado. JavaScript funciona así, como user.address es undefined , el intento
de obtener user.address.street falla dando un error.
En muchos casos prácticos preferiríamos obtener undefined en lugar del error (dando a entender
“sin calle”)
… y otro ejemplo. En desarrollo web, podemos obtener un objeto que corresponde a un elemento
de página web usando el llamado a un método especial como document.querySelector('.elem') , que
devuelve null cuando no existe tal elemento.
Esto funciona, no hay error… Pero es bastante poco elegante. Como puedes
ver, "user.address" aparece dos veces en el código.
El mismo caso, pero con la búsqueda de document.querySelector :
Esto es horrible, podemos tener problemas para siquiera entender tal código.
Poniendo AND en el camino completo a la propiedad asegura que todos los componentes existen
(si no, la evaluación se detiene), pero no es lo ideal.
Como puedes ver, los nombres de propiedad aún están duplicados en el código. Por ejemplo en
el código de arriba user.address aparece tres veces.
Es por ello que el encadenamiento opcional ?. fue agregado al lenguaje. ¡Para resolver este
problema de una vez por todas!
Encadenamiento opcional
El encadenamiento opcional ?. detiene la evaluación y devuelve undefined si el valor antes
del ?. es undefined o null .
De aquí en adelante en este artículo, por brevedad, diremos que algo “existe” si no
es null o undefined .
Leer la dirección con user?.Address funciona incluso si el objeto user no existe:
Tenga en cuenta: la sintaxis ?. hace opcional el valor delante de él, pero no más allá.
Por ejemplo, en user?.address.street.name , el ?. permite que user sea null/undefined (y
devuelve undefined en tal caso), pero solo a user . El resto de las propiedades son accedidas de
La variable debe ser declarada (con let/const/var user o como parámetro de función). El
encadenamiento opcional solo funciona para variables declaradas.
Short-circuiting (Cortocircuitos)
Como se dijo antes, el ?. detiene inmediatamente (“cortocircuito”) la evaluación si la parte
izquierda no existe.
Por ejemplo:
En el siguiente código, algunos de nuestros usuarios tienen el método admin , y otros no:
let userAdmin = {
admin() {
alert("I am admin");
}
};
Aquí, en ambas líneas, primero usamos el punto ( userAdmin.admin ) para obtener la propiedad admin ,
porque asumimos que el objeto user existe y es seguro leerlo.
Entonces ?.() comprueba la parte izquierda: si la función admin existe, entonces se ejecuta
(para userAdmin ). De lo contrario (para userGuest ) la evaluación se detiene sin errores.
La sintaxis ?.[] también funciona si quisiéramos usar corchetes [] para acceder a las
propiedades en lugar de punto . . Al igual que en casos anteriores, permite leer de forma
let user1 = {
firstName: "John"
};
Podemos usar ?. para una lectura y eliminación segura, pero no para escribir
Resumen
La sintaxis de encadenamiento opcional ?. tiene tres formas:
Como podemos ver, todos ellos son sencillos y fáciles de usar. El ?. comprueba si la parte
izquierda es null/undefined y permite que la evaluación continúe si no es así.
Una cadena de ?. permite acceder de forma segura a las propiedades anidadas.
Aún así, debemos aplicar ?. con cuidado, solamente donde sea aceptable que, de acuerdo con
nuestra lógica, la parte izquierda no exista. Esto es para que no nos oculte errores de
programación, si ocurren.
Tipo Symbol
Según la especificación, solo dos de los tipos primitivos pueden servir como clave de
propiedad de objetos:
string, o
symbol.
Si se usa otro tipo, como un número, este se autoconvertirá a string. Así, obj[1] es lo mismo
que obj["1"] , y obj[true] es lo mismo que obj["true"] .
Hasta ahora solo estuvimos usando strings.
Symbols
El valor de “Symbol” representa un identificador único.
let id = Symbol();
Al crearlo, podemos agregarle una descripción (también llamada symbol name), que será útil en
la depuración de código:
Se garantiza que los símbolos son únicos. Aunque declaremos varios Symbols con la misma
descripción, éstos tendrán valores distintos. La descripción es solamente una etiqueta que no
afecta nada más.
Por ejemplo, aquí hay dos Symbols con la misma descripción… pero no son iguales:
Si estás familiarizado con Ruby u otro lenguaje que también tiene symbols, por favor no te
confundas. Los Symbols de Javascript son diferentes.
Para resumir: un symbol es un “valor primitivo único” con una descripción opcional. Veamos
dónde podemos usarlos.
let id = Symbol("id");
alert(id); // TypeError: No puedes convertir un valor Symbol en string
Esta es una “protección del lenguaje” para evitar errores, ya que String y Symbol son
fundamentalmente diferentes y no deben convertirse accidentalmente uno en otro.
Si realmente queremos mostrar un Symbol, necesitamos llamar el
método .toString() explícitamente:
let id = Symbol("id");
alert(id.toString()); // Symbol(id), ahora sí funciona
let id = Symbol("id");
alert(id.description); // id
Claves “Ocultas”
Los Symbols nos permiten crear propiedades “ocultas” en un objeto, a las cuales ninguna otra
parte del código puede accesar ni sobrescribir accidentalmente.
Por ejemplo, si estamos trabajando con objetos user que pertenecen a código de terceros y
queremos agregarles identificadores:
Utilicemos una clave symbol para ello:
let id = Symbol("id");
user[id] = 1;
alert( user[id] ); // podemos accesar a la información utilizando el symbol como nombre de clave
Como los objetos user pertenecen a otro código, es inseguro agregarles campos pues podría
afectar su comportamiento predefinido en ese otro código. Sin embargo, los símbolos no pueden
ser accedidos accidentalmente. El código de terceros no se percataría de los símbolos nuevos,
por lo que se considera seguro agregar símbolos a los objetos user .
Además, imagina que otro script quiere tener su propio identificador “id” dentro de user para
sus propios fines.
// ...
let id = Symbol("id");
No habrá conflicto porque los Symbols siempre son diferentes, incluso si tienen el mismo
nombre.
… pero si utilizamos un string "id" en lugar de un Symbol para el mismo propósito,
ciertamente habrá un conflicto:
let id = Symbol("id");
let user = {
name: "John",
[id]: 123 // no "id": 123};
Se hace así porque necesitamos que el valor de la variable id sea la clave, no el string
“id”.
Por ejemplo:
let id = Symbol("id");
let user = {
name: "John",
age: 30,
[id]: 123
};
Object.keys(user) también los ignora. Esto forma parte del principio general de “ocultamiento
de propiedades simbólicas”. Si otro script o si otra librería itera sobre nuestro objeto,
este no accesará inesperadamente a la clave de Symbol.
let id = Symbol("id");
let user = {
[id]: 123
};
No hay paradoja aquí. Es así por diseño. La idea es que cuando clonamos un objeto o cuando
fusionamos objetos, generalmente queremos que se copien todas las claves (incluidos los
Symbol como id ).
Symbols Globales
Como hemos visto, normalmente todos los Symbols son diferentes aunque tengan el mismo nombre.
Pero algunas veces necesitamos que symbols con el mismo nombre sean la misma entidad.
Para lograr esto, existe un global symbol registry. Ahí podemos crear symbols y accesarlos
después, lo cual nos garantiza que cada vez que se acceda a la clave con el mismo nombre,
esta te devuelva exactamente el mismo symbol.
Esta llamada revisa el registro global, y si existe un symbol descrito como key , lo
retornará; de lo contrario creará un nuevo symbol Symbol(key) y lo almacenará en el registro con
el key dado.
Por ejemplo:
// el mismo symbol
alert( id === idAgain ); // true
Los Symbols dentro de este registro son llamados global symbols y están disponibles y al
alcance de todo el código en la aplicación.
Eso suena a Ruby
En algunos lenguajes de programación, como Ruby, hay un solo Symbol por cada nombre.
En Javascript, como podemos ver, eso es verdad para los global symbols.
Symbol.keyFor
Para los global symbols, no solo Symbol.for(key) devuelve un symbol por su nombre. Para hacer lo
opuesto, – devolver el nombre de un global symbol – podemos usar: Symbol.keyFor(sym) .
Por ejemplo:
El Symbol.keyFor utiliza internamente el registro “global symbol registry” para buscar la clave
del symbol, por lo tanto, no funciona para los symbol que no están dentro del registro. Si el
symbol no es global, no será capaz de encontrarlo y por lo tanto devolverá undefined .
Por ejemplo:
System symbols
Existen varios symbols del sistema que JavaScript utiliza internamente, y que podemos usar
para ajustar varios aspectos de nuestros objetos.
Symbol.hasInstance
Symbol.isConcatSpreadable
Symbol.iterator
Symbol.toPrimitive
…y así.
Por ejemplo, Symbol.toPrimitive nos permite describir el objeto para su conversión primitiva. Más
adelante veremos su uso.
Otros symbols también te serán más familiares cuando estudiemos las características
correspondientes.
Resumen
Symbol es un tipo de dato primitivo para identificadores únicos.
Symbols son siempre valores distintos aunque tengan el mismo nombre. Si queremos que symbols
con el mismo nombre tengan el mismo valor, entonces debemos guardarlos en el registro
global: Symbol.for(key) retornará un symbol (en caso de no existir, lo creará) con el key como su
nombre. Todas las llamadas de Symbol.for con ese nombre retornarán siempre el mismo symbol.
Si queremos agregar una propiedad a un objeto que “pertenece” a otro script u otra
librería, podemos crear un symbol y usarlo como clave. Una clave symbol no aparecerá en
los ciclos for..in , por lo que no podrá ser procesada accidentalmente junto con las demás
propiedades. Tampoco puede ser accesada directamente, porque un script ajeno no tiene
nuestro symbol. Por lo tanto la propiedad estará protegida contra uso y escritura
accidentales.
Podemos “ocultar” ciertos valores dentro de un objeto que solo estarán disponibles dentro
de ese script usando las claves de symbol.
2. Existen diversos symbols del sistema que utiliza Javascript, a los cuales podemos accesar
por medio de Symbol.* . Podemos usarlos para alterar algunos comportamientos. Por ejemplo,
¿Qué sucede cuando los objetos se suman obj1 + obj2 , se restan obj1 - obj2 o se imprimen
utilizando alert(obj) ?
JavaScript no permite personalizar cómo los operadores trabajan con los objetos. Al contrario
de otros lenguajes de programación como Ruby o C++, no podemos implementar un método especial
para manejar una suma (u otros operadores).
Esto es una limitación importante: el resultado de obj1 + obj2 (u otra operación) ¡no puede ser
otro objeto!
Por ejemplo no podemos hacer objetos que representen vectores o matrices (o conquistas o lo
que sea), sumarlas y esperar un objeto “sumado” como resultado. Tal objetivo arquitectural
cae automáticamente “fuera del tablero”.
Como técnicamente no podemos hacer mucho aquí, no se hacen matemáticas con objetos en
proyectos reales. Cuando ocurre, con alguna rara excepción es por un error de código.
En este capítulo cubriremos cómo un objeto se convierte a primitivo y cómo podemos
personalizarlo.
Tenemos dos propósitos:
1. Nos permitirá entender qué ocurre en caso de errores de código, cuando tal operación
ocurre accidentalmente.
2. Hay excepciones, donde tales operaciones son posibles y se ven bien. Por ejemplo al restar
o comparar fechas (objetos Date ). Las discutiremos más adelante.
Reglas de conversión
En el capítulo Conversiones de Tipos, hemos visto las reglas para las conversiones de valores
primitivos numéricos, strings y booleanos. Pero dejamos un hueco en los objetos. Ahora, como
sabemos sobre métodos y símbolos, es posible completarlo.
1. No hay conversión a boolean. Todos los objetos son true en un contexto booleano, tan simple
como eso. Solo hay conversiones numéricas y de strings.
Podemos implementar la conversión de tipo string y numérica por nuestra cuenta, utilizando
métodos de objeto especiales.
Ahora entremos en los detalles técnicos, porque es la única forma de cubrir el tópico en
profundidad.
Hints (sugerencias)
¿Cómo decide JavaScript cuál conversión aplicar?
// salida
alert(obj);
// utilizando un objeto como clave
anotherObj[obj] = 123;"number"
Para una conversión de objeto a número, como cuando hacemos operaciones matemáticas:
// conversión explícita
let num = Number(obj);
Symbol.toPrimitive
Empecemos por el primer método. Hay un símbolo incorporado llamado Symbol.toPrimitive que debe
utilizarse para nombrar el método de conversión, así:
obj[Symbol.toPrimitive] = function(hint) {
// aquí va el código para convertir este objeto a un primitivo
// debe devolver un valor primitivo
// hint = "sugerencia", uno de: "string", "number", "default"
};
Si el método Symbol.toPrimitive existe, es usado para todos los hints y no serán necesarios más
métodos.
Por ejemplo, aquí el objeto user lo implementa:
[Symbol.toPrimitive](hint) {
alert(`sugerencia: ${hint}`);
return hint == "string" ? `{name: "${this.name}"}` : this.money;
}
};
// demostración de conversiones:
alert(user); // sugerencia: string -> {name: "John"}
alert(+user); // sugerencia: number -> 1000
alert(user + 500); // sugerencia: default -> 1500
Como podemos ver en el código, user se convierte en un string autodescriptivo o en una
cantidad de dinero, depende de la conversión. Un único método user[Symbol.toPrimitive] maneja todos
los casos de conversión.
toString/valueOf
Si no existe Symbol.toPrimitive entonces JavaScript trata de encontrar los
métodos toString y valueOf :
Para una sugerencia “string”: trata de llamar a toString primero; pero si no existe, o si
devuelve un objeto en lugar de un valor primitivo, llama a valueOf (así, toString tiene
prioridad en conversiones string).
Los métodos toString y valueOf provienen de tiempos remotos. No son símbolos (los símbolos no
existían en aquel tiempo) sino métodos “normales” nombrados con strings. Proporcionan una
forma alternativa “al viejo estilo” de implementar la conversión.
Estos métodos deben devolver un valor primitivo. Si toString o valueOf devuelve un objeto,
entonces se ignora (lo mismo que si no hubiera un método).
De forma predeterminada, un objeto simple tiene los siguientes métodos toString y valueOf :
Por lo tanto, si intentamos utilizar un objeto como un string, como en un alert o algo así,
entonces por defecto vemos [object Object] .
El valueOf predeterminado se menciona aquí solo en favor de la integridad, para evitar
confusiones. Como puede ver, devuelve el objeto en sí, por lo que se ignora. No me pregunte
por qué, es por razones históricas. Entonces podemos asumir que no existe.
Implementemos estos métodos para personalizar la conversión.
Por ejemplo, aquí user hace lo mismo que el ejemplo anterior utilizando una combinación
de toString y valueOf en lugar de Symbol.toPrimitive :
let user = {
name: "John",
money: 1000,
// para sugerencia="string"
toString() {
};
Como podemos ver, el comportamiento es el mismo que en el ejemplo anterior con Symbol.toPrimitive .
A menudo queremos un único lugar “general” para manejar todas las conversiones primitivas. En
este caso, podemos implementar solo toString , así:
let user = {
name: "John",
toString() {
return this.name;
}
};
En ausencia de Symbol.toPrimitive y valueOf , toString manejará todas las conversiones primitivas.
No hay control para que toString devuelva exactamente un string, ni para que el
método Symbol.toPrimitive con una sugerencia "number" devuelva un número.
Por razones históricas, si toString o valueOf devuelve un objeto, no hay ningún error, pero
dicho valor se ignora (como si el método no existiera). Esto se debe a que en la antigüedad
no existía un buen concepto de “error” en JavaScript.
Más conversiones
Como ya sabemos, muchos operadores y funciones realizan conversiones de tipo, por ejemplo la
multiplicación * convierte operandos en números.
Por ejemplo:
let obj = {
// toString maneja todas las conversiones en ausencia de otros métodos
toString() {
return "2";
}
};
alert(obj * 2); // 4, objeto convertido a valor primitivo "2", luego la multiplicación lo convirtió en un número
El + binario concatenará los strings en la misma situación, ya que acepta con gusto un
string:
let obj = {
toString() {
return "2";
}
};
alert(obj + 2); // 22 ("2" + 2), la conversión a valor primitivo devolvió un string => concatenación
Resumen
La conversión de objeto a valor primitivo es llamada automáticamente por muchas funciones y
operadores incorporados que esperan un valor primitivo.
"default" (pocos operadores, usualmente los objetos lo implementan del mismo modo
que "number" )
Todos estos métodos deben devolver un primitivo para funcionar (si está definido).
Tipos de datos
Métodos en tipos primitivos
JavaScript nos permite trabajar con tipos de datos primitivos (string, number, etc) como si
fueran objetos. Los primitivos también ofrecen métodos que podemos llamar. Los estudiaremos
pronto, pero primero veamos cómo trabajan porque, por supuesto, los primitivos no son objetos
(y aquí lo haremos aún más evidente).
Hay 7 tipos primitivos: string , number , bigint , boolean , symbol , null y undefined .
Un objeto
Puede ser creado con {} . Ejemplo: {name: "John", age: 30} . Hay otras clases de objetos en
JavaScript; las funciones, por ejemplo, son objetos.
Una de las mejores cosas de los objetos es que podemos almacenar una función como una de sus
propiedades.
let john = {
name: "John",
sayHi: function() {
alert("Hi buddy!");
}
};
john.sayHi(); // Hi buddy!
Ya existen muchos objetos integrados al lenguaje, como los que trabajan con fechas, errores,
elementos HTML, etc. Ellos tienen diferentes propiedades y métodos.
Hay muchas cosas que uno querría hacer con los tipos primitivos, como un string o un
number. Sería grandioso accederlas usando métodos.
Los Primitivos deben ser tan rápidos y livianos como sea posible.
1. Los primitivos son aún primitivos. Con un valor único, como es deseable.
3. Para que esto funcione, se crea una envoltura especial, un “object wrapper” (objeto
envoltorio) que provee la funcionalidad extra y luego es destruido.
El motor JavaScript optimiza este proceso enormemente. Incluso puede saltarse la creación del
objeto extra por completo. Pero aún se debe adherir a la especificación y comportarse como si
creara uno.
Un number tiene sus propios métodos, por ejemplo toFixed(n) redondea el número a la precisión
dada:
Algunos lenguajes como Java permiten crear “wrapper objects” para primitivos explícitamente
usando una sintaxis como new Number(1) o new Boolean(false) .
Por ejemplo:
Por otro lado, usar las mismas funciones String/Number/Boolean sin new es totalmente sano y útil.
Ellas convierten un valor al tipo primitivo correspondiente: a un string, number, o boolean.
alert(null.test); // error
Resumen
Los primitivos excepto null y undefined proveen muchos métodos útiles. Los estudiaremos en
los próximos capítulos.
Formalmente, estos métodos trabajan a través de objetos temporales, pero los motores de
JavaScript están bien afinados para optimizarlos internamente así que llamarlos no es
costoso.
Números
2. Los números BigInt representan enteros de longitud arbitraria. A veces son necesarios
porque un número regular no puede exceder 2 53 ni ser menor a 2 53 manteniendo la
Aquí _ es “azúcar sintáctica”, hace el número más legible. El motor JavaScript simplemente
ignora _ entre dígitos, así que es exactamente igual al “billion” de más arriba.
Pero en la vida real tratamos de evitar escribir una larga cadena de ceros porque es fácil
tipear mal.
En JavaScript, acortamos un número agregando la letra "e" y especificando la cantidad de
ceros:
En otras palabras, "e" multiplica el número por el 1 seguido de la cantidad de ceros dada.
Ahora escribamos algo muy pequeño. Digamos 1 microsegundo (un millonésimo de segundo):
Igual que antes, el uso de "e" puede ayudar. Si queremos evitar la escritura de ceros
explícitamente, podríamos expresar lo mismo como:
Si contamos los ceros en 0.000001 , hay 6 de ellos en total. Entonces naturalmente es 1e-6 .
En otras palabras, un número negativo detrás de "e" significa una división por el 1 seguido
de la cantidad dada de ceros:
Los sistemas binario y octal son raramente usados, pero también soportados mediante el uso de
los prefijos 0b y 0o :
Solo 3 sistemas numéricos tienen tal soporte. Para otros sistemas numéricos, debemos usar la
función parseInt (que veremos luego en este capítulo).
toString(base)
El método num.toString(base) devuelve la representación num en una cadena, en el sistema numérico
con la base especificada.
Ejemplo:
alert( num.toString(16) ); // ff
alert( num.toString(2) ); // 11111111
base=16 usada para colores hex, codificación de caracteres, etc; los dígitos pueden
ser 0..9 o A..F .
base=36 Es el máximo, los dígitos pueden ser 0..9 o A..Z . Aquí el alfabeto inglés completo
es usado para representar un número. Un uso peculiar pero práctico para la base 36 es
cuando necesitamos convertir un largo identificador numérico en algo más corto, por
ejemplo para abreviar una url. Podemos simplemente representarlo en el sistema numeral de
base 36 :
Redondeo
Una de las operaciones más usadas cuando se trabaja con números es el redondeo.
Math.floor Redondea hacia abajo: 3.1 se convierte en 3 , y -1.1 se hace -2 . Math.ceil Redondea hacia
arriba: 3.1 torna en 4 , y -1.1 torna en -1 . Math.round Redondea hacia el entero más
cercano: 3.1 redondea a 3 , 3.6 redondea a 4 , el caso medio 3.5 redondea
a 4 también. Math.trunc (no soportado en Internet Explorer)Remueve lo que haya tras el punto
decimal sin redondear: 3.1 torna en 3 , -1.1 torna en -1 .
Aquí, la tabla que resume las diferencias entre ellos:
3.1 3 4 3 3
3.6 3 4 4 3
-1.1 -2 -1 -1 -1
-1.6 -2 -1 -2 -1
Estas funciones cubren todas las posibles formas de lidiar con la parte decimal de un número.
Pero ¿si quisiéramos redondear al enésimo n-th dígito tras el decimal?
Por ejemplo, tenemos 1.2345 y queremos redondearlo a 2 dígitos obteniendo solo 1.23 .
Hay dos formas de hacerlo:
1. Multiplicar y dividir.
Por ejemplo, para redondear el número a dos dígitos tras el decimal, podemos multiplicarlo
por 100 , llamar la función de redondeo y entonces volverlo a dividir.
alert( Math.round(num * 100) / 100 ); // 1.23456 -> 123.456 -> 123 -> 1.23
Ten en cuenta que el resultado de toFixed es una cadena. Si la parte decimal es más corta
que lo requerido, se agregan ceros hasta el final:
Podemos convertirlo a “number” usando el operador unario más o llamando a Number() ; por
ejemplo, escribir +num.toFixed(5) .
Cálculo impreciso
Internamente, un número es representado en formato de 64-bit IEEE-754, donde hay exactamente
64 bits para almacenar un número: 52 de ellos son usados para almacenar los dígitos, 11 para
almacenar la posición del punto decimal, y 1 bit es para el signo.
Si un número es verdaderamente grande, puede rebasar el almacén de 64 bit y obtenerse el
valor numérico Infinity :
Es así, al comprobar si la suma de 0.1 y 0.2 es 0.3 , obtenemos false .
Un número es almacenado en memoria en su forma binaria, una secuencia de bits, unos y ceros.
Pero decimales como 0.1 , 0.2 que se ven simples en el sistema decimal son realmente
fracciones sin fin en su forma binaria.
¿Qué es 0.1 ? Es un uno dividido por 10 1/10 , un décimo. En sistema decimal es fácilmente
representable. Compáralo con un tercio: 1/3 , se vuelve una fracción sin fin 0.33333(3) .
No solo JavaScript
Ten en cuenta que toFixed siempre devuelve un string. Esto asegura que tiene 2 dígitos después
del punto decimal. Esto es en verdad conveniente si tenemos un sitio de compras y necesitamos
mostrar $0.30 . Para otros casos, podemos usar el + unario para forzar un número:
A veces podemos tratar de evitar los decimales del todo. Si estamos tratando con una tienda,
podemos almacenar precios en centavos en lugar de dólares. Pero ¿y si aplicamos un descuento
de 30%? En la práctica, evitar la parte decimal por completo es raramente posible.
Simplemente se redondea y se corta el “rabo” decimal cuando es necesario.
Algo peculiar
Prueba ejecutando esto:
Esto sufre del mismo problema: Una pérdida de precisión. Hay 64 bits para el número, 52 de
ellos pueden ser usados para almacenar dígitos, pero no es suficiente. Entonces los dígitos
menos significativos desaparecen.
JavaScript no dispara error en tales eventos. Hace lo mejor que puede para ajustar el número
al formato deseado, pero desafortunadamente este formato no es suficientemente grande.
Dos ceros
Esto es porque el signo es representado por un bit, así cada número puede ser positivo o
negativo, incluyendo al cero.
Infinity (y Infinity ) es un valor numérico especial que es mayor (menor) que cualquier otra
cosa.
Ambos pertenecen al tipo number , pero no son números “normales”, así que hay funciones
especiales para chequearlos:
Pero ¿necesitamos esta función? ¿No podemos simplemente usar la comparación === NaN ?
Desafortunadamente no. El valor NaN es único en que no es igual a nada, incluyendo a sí
mismo:
no NaN/Infinity/-Infinity :
A veces isFinite es usado para validar si un valor string es un número regular:
Ten en cuenta que un valor vacío o un string de solo espacios es tratado como 0 en todas las
funciones numéricas incluyendo isFinite .
Number.isNaN(value) devuelve true si el argumento pertenece al tipo de dato number y si es NaN .
// Note la diferencia:
alert( Number.isNaN("str") ); // false, porque "str" pertenece a al tipo string, no al tipo number
alert( isNaN("str") ); // true, porque isNaN convierte el string "str" a number y obtiene NaN como resultado de su conversión
// Note la diferencia:
alert( Number.isFinite("123") ); // false, porque "123" pertenece a "string", no a "number"
alert( isFinite("123") ); // true, porque isFinite convierte el string "123" al number 123
En un sentido, Number.isNaN y Number.isFinite son más simples y directas que las
funciones isNaN e isFinite . Pero en la práctica isNaN e isFinite son las más usadas, porque son
más cortas.
Comparación con Object.is
Existe un método nativo especial, Object.is , que compara valores al igual que === , pero es más
confiable para dos casos extremos:
1. Funciona con NaN : Object.is(NaN, NaN) === true , lo que es una buena cosa.
2. Los valores 0 y 0 son diferentes: Object.is(0, -0) === false . false es técnicamente correcto,
porque internamente el número puede tener el bit de signo diferente incluso aunque todos
los demás bits sean ceros.
parseInt y parseFloat
La conversión numérica usando un más + o Number() es estricta. Si un valor no es exactamente
un número, falla:
Siendo la única excepción los espacios al principio y al final del string, pues son
ignorados.
Pero en la vida real a menudo tenemos valores en unidades como "100px" o "12pt" en CSS. También
el símbolo de moneda que en varios países va después del monto, tenemos "19€" y queremos
extraerle la parte numérica.
Estas “leen” el número desde un string hasta que dejan de poder hacerlo. Cuando se topa con
un error devuelve el número que haya registrado hasta ese momento. La función parseInt devuelve
un entero, mientras que parseFloat devolverá un punto flotante:
Hay situaciones en que parseInt/parseFloat devolverán NaN . Ocurre cuando no puedo encontrar
dígitos:
La función parseInt() tiene un segundo parámetro opcional. Este especifica la base de sistema
numérico, entonces parseInt puede también analizar cadenas de números hexa, binarios y otros:
Unos ejemplos:
Math.random() Devuelve un número aleatorio entre 0 y 1 (no incluyendo 1)
c...) y Math.min(a, b, c...) Devuelven el mayor y el menor de entre una cantidad arbitraria de
argumentos.
Hay más funciones y constantes en el objeto Math , incluyendo trigonometría, que puedes
encontrar en la documentación del objeto Math.
Resumen
Para escribir números con muchos ceros:
Agregar "e" con la cantidad de ceros al número. Como: 123e6 es 123 con 6 ceros 123000000 .
un número negativo después de "e" causa que el número sea dividido por 1 con los ceros
dados:. 123e-6 significa 0.000123 ( 123 millonésimos).
Number.isNaN(value) verifica que el tipo de dato sea number , y si lo es, verifica si es NaN
no NaN/Infinity/-Infinity
Number.isFinite(value) verifica que el tipo de dato sea number , y si lo es, verifica que no
sea NaN/Infinity/-Infinity
Usa parseInt/parseFloat para una conversión “suave”, que lee un número desde un string y
devuelve el valor del número que pudiera leer antes de encontrar error.
Asegúrate de recordar que hay pérdida de precisión cuando se trabaja con decimales.
Strings
En JavaScript, los datos textuales son almacenados como strings (cadena de caracteres). No
hay un tipo de datos separado para caracteres unitarios.
Comillas
Recordemos los tipos de comillas.
Los strings pueden estar entre comillas simples, comillas dobles o backticks (acento grave):
function sum(a, b) {
return a + b;
}
Otra ventaja de usar backticks es que nos permiten extender en múltiples líneas el string:
Se ve natural, ¿no es cierto? Pero las comillas simples y dobles no funcionan de esa manera.
Si intentamos usar comillas simples o dobles de la misma forma, obtendremos un error:
Los backticks además nos permiten especificar una “función de plantilla” antes del primer
backtick. La sintaxis es: func`string` . La función func es llamada automáticamente, recibe el
string y la expresión insertada, y los puede procesar. Eso se llama “plantillas etiquetadas”.
Es raro verlo implementado, pero puedes leer más sobre esto en el manual.
Caracteres especiales
Es posible crear strings de múltiples líneas usando comillas simples, usando un llamado
“carácter de nueva línea”, escrito como \n , lo que denota un salto de línea:
Como ejemplo más simple, estas dos líneas son iguales, pero escritas en forma diferente:
Carácter Descripción
\n Nueva línea
En Windows, los archivos de texto usan una combinación de dos caracteres \r\n para
\r representar un corte de línea, mientras que en otros SO es simplemente ‘\n’. Esto es por
razones históricas, la mayoría del software para Windows también reconoce ‘\n’.
Como puedes ver, todos los caracteres especiales empiezan con la barra invertida \ . Se lo
llama “carácter de escape”.
Las llamadas comillas “escapadas” \' , \" , \` se usan para insertar una comilla en un string
entrecomillado con el mismo tipo de comilla.
Por ejemplo:
Como puedes ver, debimos anteponer un carácter de escape \ antes de cada comilla ya que de
otra manera hubiera indicado el final del string.
Obviamente, solo necesitan ser escapadas las comillas que son iguales a las que están
rodeando al string. Una solución más elegante es cambiar a comillas dobles o backticks:
Además de estos caracteres especiales, también hay una notación especial para códigos
Unicode \u… que se usa raramente. Los cubrimos en el capítulo opcional acerca de Unicode.
alert(`Mi\n`.length); // 3
Nota que \n es un solo carácter, por lo que el largo total es 3 .
Accediendo caracteres
Para acceder a un carácter en la posición pos , se debe usar corchetes, [pos] , o llamar al
método str.at(pos). El primer carácter comienza desde la posición cero:
// el primer carácter
alert( str[0] ); // H
alert( str.at(0) ); // H
// el último carácter
alert( str[str.length - 1] ); // a
alert( str.at(-1) );
Lo usual para resolverlo es crear un nuevo string y asignarlo a str reemplazando el string
completo.
Por ejemplo:
Cambiando capitalización
Los métodos toLowerCase() y toUpperCase() cambian los caracteres a minúscula y mayúscula
respectivamente:
alert('Interfaz'.toUpperCase()); // INTERFAZ
alert('Interfaz'.toLowerCase()); // interfaz
alert('Interfaz'[0].toLowerCase()); // 'i'
str.indexOf
El primer método es str.indexOf(substr, pos).
Por ejemplo:
Por ejemplo, la primera ocurrencia de "id" es en la posición 1 . Para buscar por la siguiente
ocurrencia, comencemos a buscar desde la posición 2 :
alert(str.indexOf('id', 2)); // 11
Si estamos interesados en todas las ocurrencias, podemos correr indexOf en un bucle. Cada nuevo
llamado es hecho utilizando la posición posterior a la encontrada anteriormente:
let pos = 0;
while (true) {
let foundPos = str.indexOf(target, pos);
if (foundPos == -1) break;
alert(`Encontrado en ${foundPos}`);
pos = foundPos + 1; // continuar la búsqueda desde la siguiente posición
}
if (str.indexOf("Widget")) {
alert("Lo encontramos"); // no funciona!
}
La alerta en el ejemplo anterior no se muestra ya que str.indexOf("Widget") retorna 0 (lo que
significa que encontró el string en la posición inicial). Eos correcto,
pero if considera 0 como falso .
Por ello debemos preguntar por -1 :
alert('Hola'.includes('Adiós')); // false
El segundo argumento opcional de str.includes es la posición desde donde comienza a buscar:
alert('Midget'.includes('id')); // true
alert('Midget'.includes('id', 3)); // false, desde la posición 3 no hay "id"
Obteniendo un substring
Existen 3 métodos en JavaScript para obtener un substring: substring , substr y slice .
str.slice(comienzo [, final]) Retorna la parte del string desde comienzo hasta (pero sin incluir) final .
Por ejemplo:
Si no existe el segundo argumento, entonces slice va hasta el final del string:
También son posibles valores negativos para comienzo/final . Estos indican que la posición es
contada desde el final del string.
Los argumentos negativos (al contrario de slice) no son soportados, son tratados
como 0 . str.substr(comienzo [, largo]) Retorna la parte del string desde comienzo , con el largo dado.
A diferencia de los métodos anteriores, este nos permite especificar el largo en lugar de la
posición final:
Este método reside en el Anexo B de la especificación del lenguaje. Esto significa que solo
necesitan darle soporte los motores Javascript de los navegadores, y no es recomendable su
uso. Pero en la práctica, es soportado en todos lados.
¿Cuál elegir?
Todos son capaces de hacer el trabajo. Formalmente, substr tiene una pequeña desventaja: no es
descrito en la especificación central de JavaScript, sino en el anexo B, el cual cubre
características sólo de navegadores, que existen principalmente por razones históricas. Por
lo que entornos sin navegador pueden fallar en compatibilidad. Pero en la práctica, funciona
en todos lados.
De las otras dos variantes, slice es algo más flexible, permite argumentos negativos y es más
corta.
Entones, es suficiente recordar únicamente slice .
Comparando strings
Como aprendimos en el capítulo Comparaciones, los strings son comparados carácter por
carácter en orden alfabético.
Aunque existen algunas singularidades.
Esto puede conducir a resultados extraños si ordenamos los nombres de estos países.
Usualmente, se esperaría que Zealand apareciera después de Österreich en la lista.
Para entender lo que pasa, debemos tener en cuenta que los strings en JavaScript son
codificados usando UTF-16. Esto significa: cada carácter tiene un código numérico
correspondiente.
Existen métodos especiales que permiten obtener el carácter para el código y viceversa.
str.codePointAt(pos) Devuelve un número decimal que representa el código de carácter en la
posición pos :
alert( String.fromCodePoint(90) ); // Z
alert( String.fromCodePoint(0x5a) ); // Z (también podemos usar un valor hexa como argumento)
Ahora veamos los caracteres con códigos 65..220 (el alfabeto latino y algo más)
transformándolos a string:
¿Lo ves? Caracteres en mayúsculas van primero, luego unos cuantos caracteres especiales,
luego las minúsculas.
Todas las letras minúsculas van después de las mayúsculas ya que sus códigos son mayores.
Algunas letras como Ö se mantienen apartadas del alfabeto principal. Aquí el código es
mayor que cualquiera desde a hasta z .
Comparaciones correctas
El algoritmo “correcto” para realizar comparaciones de strings es más complejo de lo que
parece, debido a que los alfabetos son diferentes para diferentes lenguajes. Una letra que se
ve igual en dos alfabetos distintos, pueden tener distintas posiciones.
Este provee un método especial para comparar strings en distintos lenguajes, siguiendo sus
reglas.
Por ejemplo:
alert('Österreich'.localeCompare('Zealand')); // -1
Este método tiene dos argumentos adicionales especificados en la documentación, la cual le
permite especificar el lenguaje (por defecto lo toma del entorno) y configura reglas
adicionales como sensibilidad a las mayúsculas y minúsculas, o si "a" y "á" deben ser
tratadas como iguales, etc.
Resumen
Existen 3 tipos de entrecomillado. Los backticks permiten que una cadena abarque varias
líneas e insertar expresiones ${…} .
Para comparar strings de acuerdo al idioma, usa: localeCompare , de otra manera serán
comparados por sus códigos de carácter.
Los strings también tienen métodos para buscar/reemplazar que usan “expresiones regulares”.
Este es un tema muy amplio, por ello es explicado en una sección separada del
tutorial Expresiones Regulares.
Además, es importante saber que los strings están basados en la codificación Unicode, y se
presentan algunas complicaciones en las comparaciones de string. Hay más acerca de Unicode en
el capítulo Unicode, String internals.
Arrays
Los objetos te permiten almacenar colecciones de datos a través de nombres. Eso está bien.
Pero a menudo necesitamos una colección ordenada, donde tenemos un 1ro, un 2do, un 3er
elemento y así sucesivamente. Por ejemplo, necesitamos almacenar una lista de algo: usuarios,
bienes, elementos HTML, etc.
No es conveniente usar objetos aquí, porque no proveen métodos para manejar el orden de los
elementos. No podemos insertar una nueva propiedad “entre” los existentes. Los objetos no
están hechos para eso.
Existe una estructura llamada Array (llamada en español arreglo o matriz/vector) para
almacenar colecciones ordenadas.
Declaración
Hay dos sintaxis para crear un array vacío:
alert( fruits.length ); // 3
Por ejemplo:
// mezcla de valores
let arr = [ 'Apple', { name: 'John' }, true, function() { alert('hello'); } ];
Coma residual
Un array, al igual que un objeto, puede tener una coma final:
let fruits = [
"Apple",
"Orange",
"Plum",];
La “coma final” hace más simple insertar y remover items, porque todas la líneas se vuelven
similares.
1] .
Un poco engorroso, ¿no es cierto? Necesitamos escribir el nombre de la variable dos veces.
para valores negativos de i , salta hacia atrás desde el final del array.
shift obtiene el elemento del principio, avanzando la cola, y así el segundo elemento se
vuelve primero.
Entonces los elementos nuevos son agregados o tomados siempre desde el “final”.
Una pila es usualmente mostrada como un mazo de cartas, donde las nuevas cartas son agregadas
al tope o tomadas desde el tope:
Los arrays en JavaScript pueden trabajar como colas o pilas. Ellos permiten agregar/quitar
elementos al/del principio o al/del final.
fruits.push("Pear");
alert( fruits ); // Apple, Orange, Pear
fruits.unshift('Apple');
Los métodos push y unshift pueden agregar múltiples elementos de una vez:
fruits.push("Orange", "Peach");
fruits.unshift("Pineapple", "Lemon");
Ellos extienden los objetos proveyendo métodos especiales para trabajar con colecciones
ordenadas de datos y también la propiedad length . Pero en el corazón es aún un objeto.
Recuerde, solo hay ocho tipos de datos básicos en JavaScript (consulte el capítulo Tipos de
datos para obtener más información). Array es un objeto y, por tanto, se comporta como un
objeto.
Por ejemplo, es copiado por referencia:
let arr = fruits; // copiado por referencia (dos variables referencian al mismo array)
…Pero lo que hace a los array realmente especiales es su representación interna. El motor
trata de almacenarlos en áreas de memoria contigua, uno tras otro, justo como muestra la
ilustración en este capítulo. Hay otras optimizaciones también para hacer que los arrays
trabajen verdaderamente rápido.
Pero todo esto se puede malograr si dejamos de trabajarlos como arrays de colecciones
ordenadas y comenzamos a usarlos como si fueran objetos comunes.
fruits[99999] = 5; // asigna una propiedad con un índice mucho mayor que su longitud
Esto es posible porque los arrays son objetos en su base. Podemos agregar cualquier propiedad
en ellos.
Pero el motor verá que estamos tratándolo como un objeto común. Las optimizaciones
específicas no son aptas para tales casos y serán desechadas, y sus beneficios desaparecerán.
Generar agujeros como: agregar arr[0] y luego arr[1000] (y nada entre ellos).
Piensa en los arrays como estructuras especiales para trabajar con datos ordenados. Ellos
proveen métodos especiales para ello. Los arrays están cuidadosamente afinados dentro de los
motores JavaScript para funcionar con datos ordenados contiguos, por favor úsalos de esa
manera. Y si necesitas claves arbitrarias, hay altas chances de que en realidad necesites
objetos comunes {} .
Performance
Los métodos push/pop son rápidos, mientras que shift/unshift son lentos.
No es suficiente tomar y eliminar el elemento con el índice 0 . Los demás elementos necesitan
ser renumerados también.
Cuanto más elementos haya en el array, más tiempo tomará moverlos, más operaciones en
memoria.
Algo similar ocurre con unshift : para agregar un elemento al principio del array, necesitamos
primero mover todos los elementos hacia la derecha, incrementando sus índices.
¿Y qué pasa con push/pop ? Ellos no necesitan mover nada. Para extraer un elemento del final, el
método pop limpia el índice y acorta length .
El método pop no necesita mover nada, porque los demás elementos mantienen sus índices. Es
por ello que es muy rápido.
Algo similar ocurre con el método push .
Pero para los arrays también hay otra forma de bucle, for..of :
for..of no da acceso al número del elemento en curso, solamente a su valor, pero en la mayoría
de los casos eso es suficiente. Y es más corto.
Técnicamente, y porque los arrays son objetos, es también posible usar for..in :
2. El bucle for..in está optimizado para objetos genéricos, no para arrays, y es de 10 a 100
veces más lento. Por supuesto es aún muy rápido. Una optimización puede que solo sea
importante en cuellos de botella, pero necesitamos ser concientes de la diferencia.
Acerca de “length”
La propiedad length automáticamente se actualiza cuando se modifica el array. Para ser
precisos, no es la cuenta de valores del array sino el mayor índice más uno.
Por ejemplo, un elemento simple con un índice grande da una longitud grande:
Otra cosa interesante acerca de la propiedad length es que se puede sobrescribir.
new Array()
Hay una sintaxis más para crear un array:
Es raramente usada porque con corchetes [] es más corto. También hay una característica
peculiar con ella.
Si new Array es llamado con un único argumento numérico, se crea un array sin items, pero con
la longitud “length” dada.
Veamos cómo uno puede dispararse en el pie:
Para evitar sorpresas solemos usar corchetes, salvo que sepamos lo que estamos haciendo.
Arrays multidimensionales
Los arrays pueden tener items que a su vez sean arrays. Podemos usarlos como arrays
multidimensionales, por ejemplo para almacenar matrices:
let matrix = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
];
toString
Los arrays tienen su propia implementación del método toString que devuelve un lista de
elementos separados por coma.
Por ejemplo:
alert( [] + 1 ); // "1"
alert( [1] + 1 ); // "11"
alert( [1,2] + 1 ); // "1,21"
Los arrays no tienen Symbol.toPrimitive ni un valueOf viable, ellos implementan la
conversión toString solamente, así [] se vuelve una cadena vacía, [1] se vuelve "1" y [1,2] se
Si uno de los argumentos de == es un objeto y el otro es un primitivo, entonces el objeto
se convierte en primitivo, como se explica en el capítulo Conversión de objeto a valor
primitivo.
…Con la excepción de null y undefined que son iguales == entre sí y nada más.
La comparación estricta === es aún más simple, ya que no convierte tipos.
Entonces, si comparamos arrays con == , nunca son iguales, a no ser que comparemos dos
variables que hacen referencia exactamente a la misma array.
Por ejemplo:
alert( [] == [] ); // falso
alert( [0] == [0] ); // falso
Estas arrays son técnicamente objetos diferentes. Así que no son iguales. El operador == no
hace comparaciones de elemento a elemento.
Comparaciones con primitivos también pueden dar resultados aparentemente extraños:
alert( 0 == [] ); // verdadero
alert('0' == [] ); // falso
Aquí, en ambos casos, comparamos un primitivo con un objeto array. Entonces la array [] se
convierte a primitivo para el propósito de comparar y se convierte en una string vacía '' .
Resumen
El llamado a new Array(number) crea un array con la longitud dada, pero sin elementos.
La propiedad length es la longitud del array o, para ser preciso, el último índice numérico
más uno. Se autoajusta al usar los métodos de array.
También podemos usar el método at(i) , que permite índices negativos. Para valores negativos
de i , cuenta hacia atrás desde el final del array. Cuando i >= 0 , funciona igual que arr[i] .
Podemos usar un array como una pila “deque” o “bicola” con las siguientes operaciones:
for (let i=0; i<arr.length; i++) – lo más rápido, compatible con viejos navegadores.
Para comparar arrays, no uses el operador == (como tampoco > , < y otros), ya que no tienen
un tratamiento especial para arrays. Lo manejan como cualquier objeto y no es lo que
normalmente queremos.
En su lugar puedes utilizar el bucle for..of para comparar arrays elemento a elemento.
Volveremos a los arrays y estudiaremos más métodos para agregar, quitar, extraer elementos y
ordenar arrays en el capítulo Métodos de arrays.
Métodos de arrays
Los arrays (también llamados arreglos o matrices) cuentan con muchos métodos. Para hacer las
cosas más sencillas, en este capítulo se encuentran divididos en dos partes.
Agregar/remover ítems
Ya conocemos algunos métodos que agregan o extraen elementos del inicio o final de un array:
splice
¿Cómo podemos borrar un elemento de un array?
El elemento fue borrado, pero el array todavía tiene 3 elementos; podemos ver que arr.length ==
3.
Es natural, porque delete obj.key borra el valor de key , pero es todo lo que hace. Esto está bien
en los objetos, pero en general lo que buscamos en los arrays es que el resto de los
elementos se desplace y se ocupe el lugar libre. Lo que esperamos es un array más corto.
Por lo tanto, necesitamos utilizar métodos especiales.
El método arr.splice funciona como una navaja suiza para arrays. Puede hacer todo: insertar,
remover y remplazar elementos.
La sintaxis es:
Esto modifica arr comenzando en el índice start : remueve la cantidad deleteCount de elementos y
luego inserta elem1, ..., elemN en su lugar. Lo que devuelve es un array de los elementos
removidos.
Este método es más fácil de entender con ejemplos.
Aquí podemos ver que splice devuelve un array con los elementos removidos:
El método splice también es capaz de insertar elementos sin remover ningún otro. Para eso
necesitamos establecer deleteCount en 0 :
// desde el index 2
// remover 0
// después insertar "el", "complejo" y "language"
arr.splice(2, 0,"el", "complejo", "language");
En este y en otros métodos de arrays, los índices negativos están permitidos. Estos índices
indican la posición comenzando desde el final del array, de la siguiente manera:
slice
El método arr.slice es mucho más simple que arr.splice .
La sintaxis es:
arr.slice([principio], [final])
Por ejemplo:
También podemos invocarlo sin argumentos: arr.slice() crea una copia de arr . Se utiliza a menudo
para obtener una copia que se puede transformar sin afectar el array original.
concat
El método arr.concat crea un nuevo array que incluye los valores de otros arrays y elementos
adicionales.
La sintaxis es:
arr.concat(arg1, arg2...)
Si un argumento argN es un array, entonces todos sus elementos son copiados. De otro modo el
argumento en sí es copiado.
Por ejemplo:
Normalmente, solo copia elementos desde arrays. Otros objetos, incluso si parecen arrays, son
agregados como un todo:
let arrayLike = {
0: "something",
length: 1
};
let arrayLike = {
0: "something",
1: "else",
[Symbol.isConcatSpreadable]: true,
length: 2
};
Iteración: forEach
El método arr.forEach permite ejecutar una función a cada elemento del array.
La sintaxis:
indexOf/lastIndexOf e includes
Los métodos arr.indexOf y arr.includes tienen una sintaxis similar y hacen básicamente lo
mismo que sus contrapartes de strings, pero operan sobre elementos en lugar de caracteres:
arr.indexOf(item, from) – busca item comenzando desde el index from , y devuelve el index donde
fue encontrado, de otro modo devuelve 1 .
Usualmente estos métodos se usan con un solo argumento: el item a buscar. De manera
predeterminada, la búsqueda es desde el principio.
Por ejemplo:
alert( arr.indexOf(0) ); // 1
alert( arr.indexOf(false) ); // 2
alert( arr.indexOf(null) ); // -1
Tener en cuenta que el método usa la comparación estricta ( === ). Por lo tanto, si
buscamos false , encontrará exactamente false y no cero.
Esto es porque includes fue agregado mucho después y usa un algoritmo interno de comparación
actualizado.
find y findIndex/findLastIndex
Imaginemos que tenemos un array de objetos. ¿Cómo podríamos encontrar un objeto con una
condición específica?
Para este tipo de casos es útil el método arr.find(fn)
La sintaxis es:
La función es llamada para cada elemento del array, uno después del otro:
Si devuelve true , la búsqueda se detiene y el item es devuelto. Si no encuentra nada, entonces
devuelve undefined .
Por ejemplo, si tenemos un array de usuarios, cada uno con los campos id y name . Encontremos
el elemento con id == 1 :
alert(user.name); // Celina
En la vida real los arrays de objetos son bastante comunes por lo que el método find resulta
muy útil.
Ten en cuenta que en el ejemplo anterior le pasamos a find la función item => item.id == 1 con un
argumento. Esto es lo más común, otros argumentos son raramente usados en esta función.
El método arr.findIndex tiene la misma sintaxis, pero devuelve el índice donde el elemento
fue encontrado en lugar del elemento en sí. Devuelve -1 cuando no lo encuentra.
El método arr.findLastIndex es como findIndex , pero busca de derecha a izquierda, similar
a lastIndexOf .
Un ejemplo:
let users = [
{id: 1, name: "John"},
{id: 2, name: "Pete"},
{id: 3, name: "Mary"},
{id: 4, name: "John"}
];
filter
El método find busca un único elemento (el primero) que haga a la función devolver true .
Si existieran varios elementos que cumplen la condición, podemos usar arr.filter(fn).
La sintaxis es similar a find , pero filter devuelve un array con todos los elementos
encontrados:
Por ejemplo:
let users = [
{id: 1, name: "Celina"},
{id: 2, name: "David"},
{id: 3, name: "Federico"}
];
alert(someUsers.length); // 2
Transformar un array
Pasamos ahora a los métodos que transforman y reordenan un array.
map
Este método llama a la función para cada elemento del array y devuelve un array con los
resultados.
La sintaxis es:
Por ejemplo, acá transformamos cada elemento en el valor de su respectivo largo (length):
sort(fn)
Cuando usamos arr.sort(), este ordena el propio array cambiando el orden de los elementos.
También devuelve un nuevo array ordenado, pero este usualmente se descarta ya que arr en sí
mismo es modificado.
Por ejemplo:
let arr = [ 1, 2, 15 ];
Los elementos fueron reordenados a 1, 15, 2 . Pero ¿por qué pasa esto?
Los elementos son ordenados como strings (cadenas de caracteres) por defecto
Todos los elementos son literalmente convertidos a string para ser comparados. En el caso de
strings se aplica el orden lexicográfico, por lo que efectivamente "2" > "15" .
Para usar nuestro propio criterio de reordenamiento, necesitamos proporcionar una función
como argumento de arr.sort() .
La función debe comparar dos valores arbitrarios, y devolver:
function compare(a, b) {
if (a > b) return 1; // si el primer valor es mayor que el segundo
if (a == b) return 0; // si ambos valores son iguales
if (a < b) return -1; // si el primer valor es menor que el segundo
}
function compareNumeric(a, b) {
if (a > b) return 1;
if (a == b) return 0;
if (a < b) return -1;
}
let arr = [ 1, 2, 15 ];
arr.sort(compareNumeric);alert(arr); // 1, 2, 15
Por cierto, si queremos saber qué elementos son comparados, nada nos impide ejecutar alert()
en ellos:
El algoritmo puede comparar un elemento con muchos otros en el proceso, pero trata de hacer
la menor cantidad de comparaciones posible.
Una función de comparación puede devolver cualquier número
En realidad, una función de comparación solo es requerida para devolver un número positivo
para “mayor” y uno negativo para “menor”.
let arr = [ 1, 2, 15 ];
alert(arr); // 1, 2, 15
¿Recuerdas las arrow functions? Podemos usarlas en este caso para un ordenamiento más
prolijo:
alert( paises.sort( (a, b) => a > b ? 1 : -1) ); // Andorra, Vietnam, Österreich (incorrecto)
reverse
El método arr.reverse revierte el orden de los elementos en arr .
Por ejemplo:
split y join
Analicemos una situación de la vida real. Estamos programando una app de mensajería y y el
usuario ingresa una lista de receptores delimitada por comas: Celina, David, Federico . Pero para
nosotros un array sería mucho más práctico que una simple string. ¿Cómo podemos hacer para
obtener un array?
El método split tiene un segundo argumento numérico opcional: un límite en la extensión del
array. Si se provee este argumento, entonces el resto de los elementos son ignorados. Sin
embargo en la práctica rara vez se utiliza:
Separar en letras
arr.join(glue) hace lo opuesto a split . Crea una string de arr elementos unidos
con glue (pegamento) entre ellos.
Por ejemplo:
reduce/reduceRight
Cuando necesitamos iterar sobre un array podemos usar forEach , for o for..of .
Cuando necesitamos iterar y devolver un valor por cada elemento podemos usar map .
La sintaxis es la siguiente:
Argumentos:
accumulator – es el resultado del llamado previo de la función, equivale a initial la primera
vez (si initial es dado como argumento).
index – es la posición.
array – es el array.
Mientras la función sea llamada, el resultado del llamado anterior se pasa al siguiente como
primer argumento.
Entonces, el primer argumento es el acumulador que almacena el resultado combinado de todas
las veces anteriores en que se ejecutó, y al final se convierte en el resultado de reduce .
¿Suena complicado?
alert(result); // 15
1. En la primera pasada, sum es el valor initial (el último argumento de reduce ), equivale a 0 ,
y current es el primer elemento de array, equivale a 1 . Entonces el resultado de la
función es 1 .
El flujo de cálculos:
O en la forma de una tabla, donde cada fila representa un llamado a una función en el próximo
elemento del array:
primer llamado 0 1 1
segundo llamado 1 2 3
tercer llamado 3 3 6
cuarto llamado 6 4 10
quinto llamado 10 5 15
Acá podemos ver claramente como el resultado del llamado anterior se convierte en el primer
argumento del llamado siguiente.
También podemos omitir el valor inicial:
alert( result ); // 15
Array.isArray
Los arrays no conforman un tipo diferente. Están basados en objetos.
Por eso typeof no ayuda a distinguir un objeto común de un array:
…Pero los arrays son utilizados tan a menudo que tienen un método especial para
eso: Array.isArray(value). Este devuelve true si el valor es un array y false si no lo es.
alert(Array.isArray({})); // false
alert(Array.isArray([])); // true
Ese parámetro no está explicado en la sección anterior porque es raramente usado. Pero para
ser exhaustivos necesitamos verlo.
arr.find(func, thisArg);
arr.filter(func, thisArg);
arr.map(func, thisArg);
// ...
// thisArg es el último argumento opcional
EL valor del parámetro thisArg se convierte en this para func .
Por ejemplo, acá usamos un método del objeto army como un filtro y thisArg da el contexto:
let army = {
minAge: 18,
maxAge: 27,
canJoin(user) {
return user.age >= this.minAge && user.age < this.maxAge;
}
};
let users = [
{age: 16},
fácil de entender.
Resumen
Veamos el ayudamemoria de métodos para arrays:
concat(...items) – devuelve un nuevo array: copia todos los elementos del array actual y le
agrega items . Si alguno de los items es un array, se toman sus elementos.
indexOf/lastIndexOf(item, pos) – busca por item comenzando desde la posición pos , devolviendo el
findIndex es similar a find , pero devuelve el índice en lugar del valor.
map(func) – crea un nuevo array a partir de los resultados de llamar a la func para cada
elemento.
la func para cada elemento, obteniendo un resultado parcial en cada llamada y pasándolo
a la siguiente.
Adicional:
Estos métodos son los más utilizados y cubren el 99% de los casos. Pero existen algunos más:
arr.some(fn)/arr.every(fn) comprueba el array.
La función fn es llamada para cada elemento del array de manera similar a map . Si
alguno/todos los resultados son true , devuelve true , si no, false .
Estos métodos se comportan con similitud a los operadores || y && : si fn devuelve un
valor verdadero, arr.some() devuelve true y detiene la iteración de inmediato; si fn devuelve
un valor falso, arr.every() devuelve false y detiene la iteración también.
arr.copyWithin(target, start, end) – copia sus elementos desde la posición start hasta la
posición end en si mismo, a la posición target (reescribe lo existente).
Iterables
Por supuesto, las matrices o arrays son iterables. Pero hay muchos otros objetos integrados
que también lo son. Por ejemplo, las cadenas o strings son iterables también. Como veremos,
muchos operadores y métodos se basan en la iterabilidad.
Si un objeto no es técnicamente una matriz, pero representa una colección (lista, conjunto)
de algo, entonces el uso de la sintaxis for..of es una gran forma de recorrerlo. Veamos cómo
funciona.
Symbol.iterator
Podemos comprender fácilmente el concepto de iterables construyendo uno.
Por ejemplo: tenemos un objeto que no es un array, pero parece adecuado para for..of .
Como un objeto range que representa un intervalo de números:
let range = {
from: 1,
to: 5
};
1. Cuando se inicia for..of , éste llama al método Symbol.iterator una vez (o genera un error si no
lo encuentra). El método debe devolver un iterador : un objeto con el método next() .
3. Cuando for..of quiere el siguiente valor, llama a next() en ese objeto.
4. El resultado de next() debe tener la forma {done: Boolean, value: any} , donde done=true significa que
el bucle ha finalizado; de lo contrario, el nuevo valor es value .
let range = {
from: 1,
to: 5
};
// ¡Ahora funciona!
for (let num of range) {
alert(num); // 1, luego 2, 3, 4, 5
}
Por lo tanto, el objeto iterador está separado del objeto sobre el que itera.
Técnicamente, podríamos fusionarlos y usar el range mismo como iterador para simplificar el
código.
De esta manera:
let range = {
from: 1,
to: 5,
[Symbol.iterator]() {
this.current = this.from;
return this;
},
next() {
if (this.current <= this.to) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
Ahora range[Symbol.iterator]() devuelve el objeto range en sí: tiene el método next() necesario y
recuerda el progreso de iteración actual en this.current . ¿Más corto? Sí. Y a veces eso también
está bien.
La desventaja es que ahora es imposible tener dos bucles for..of corriendo sobre el objeto
simultáneamente: compartirán el estado de iteración, porque solo hay un iterador: el objeto
en sí. Pero dos for-of paralelos es algo raro, incluso en escenarios asíncronos.
Iteradores Infinitos
También son posibles los iteradores infinitos. Por ejemplo, el objeto range se vuelve infinito
así: range.to = Infinity . O podemos hacer un objeto iterable que genere una secuencia infinita de
números pseudoaleatorios. También puede ser útil.
No hay limitaciones en next , puede devolver más y más valores, eso es normal.
Por supuesto, el bucle for..of sobre un iterable de este tipo sería interminable. Pero siempre
podemos detenerlo usando break .
String es iterable
Las matrices y cadenas son los iterables integrados más utilizados.
En una cadena o string, el bucle for..of recorre sus caracteres:
Vamos a iterar sobre una cadena exactamente de la misma manera que for..of , pero con llamadas
directas. Este código crea un iterador de cadena y obtiene valores de él “manualmente”:
Rara vez se necesita esto, pero nos da más control sobre el proceso que for..of . Por ejemplo,
podemos dividir el proceso de iteración: iterar un poco, luego parar, hacer otra cosa y luego
continuar.
simil-array son objetos que tienen índices y longitud o length , por lo que se “ven” como
arrays.
Cuando usamos JavaScript para tareas prácticas en el navegador u otros entornos, podemos
encontrar objetos que son iterables o array-like, o ambos.
Por ejemplo, las cadenas son iterables ( for..of funciona en ellas) y array-like (tienen índices
numéricos y length ).
Pero un iterable puede que no sea array-like. Y viceversa, un array-like puede no ser
iterable.
Por ejemplo, range en el ejemplo anterior es iterable, pero no es array-like porque no tiene
propiedades indexadas ni length .
Y aquí el objeto tiene forma de matriz, pero no es iterable:
Tanto los iterables como los array-like generalmente no son arrays, no tienen “push”, “pop”,
etc. Eso es bastante inconveniente si tenemos un objeto de este tipo y queremos trabajar con
él como con una matriz. P.ej. nos gustaría trabajar con range utilizando métodos de matriz.
¿Cómo lograr eso?
Array.from
Existe un método universal Array.from que toma un valor iterable o simil-array y crea
un Array ¨real¨ a partir de él. De esta manera podemos llamar y usar métodos que pertenecen a
una matriz.
Por ejemplo:
let arrayLike = {
0: "Hola",
1: "Mundo",
length: 2
};
Array.from en la línea (*) toma el objeto, y si es iterable o simil-array crea un nuevo array y
La sintaxis completa para Array.from también nos permite proporcionar una función opcional de
“mapeo”:
alert(arr); // 1,4,9,16,25
Aquí usamos Array.from para convertir una cadena en una matriz de caracteres:
alert(chars[0]); // 𝒳
alert(chars[1]); // 😂
alert(chars.length); // 2
A diferencia de str.split , Array.from se basa en la naturaleza iterable de la cadena y, por lo
tanto, al igual que for..of , funciona correctamente con pares sustitutos.
alert(chars);
Resumen
Los objetos que se pueden usar en for..of se denominan iterables.
Un iterador debe tener el método llamado next() que devuelve un objeto {done: Boolean, value:
any} , donde done: true marca el fin de la iteración; de lo contrario, value es el siguiente
valor.
El método Symbol.iterator se llama automáticamente por for..of , pero también podemos llamarlo
directamente.
Los objetos que tienen propiedades indexadas y longitud o length se llaman array-like. Dichos
objetos también pueden tener otras propiedades y métodos, pero carecen de los métodos
integrados de las matrices.
Map y Set
Hasta este momento, hemos aprendido sobre las siguientes estructuras de datos:
Pero eso no es suficiente para la vida real. Por eso también existen Map y Set .
Map
Map es, al igual que Objet , una colección de datos identificados por claves. La principal
diferencia es que Map permite claves de cualquier tipo.
map.get(clave) – devuelve el valor de la clave. Será undefined si la clave no existe en map.
map.has(clave) – devuelve true si la clave existe en map, false si no existe.
Por ejemplo:
alert( map.size ); // 3
Podemos ver que, a diferencia de los objetos, las claves no se convierten en strings.
Cualquier tipo de clave es posible en un Map.
Aunque map[clave] también funciona (por ejemplo podemos establecer map[clave] = 2), esto es tratar
a map como un objeto JavaScript simple, lo que implica tener todas las limitaciones
correspondientes (que solo se permita string/symbol como clave, etc.).
Por lo tanto, debemos usar los métodos de Map : set , get y demás.
El uso de objetos como claves es una de las características de Map más notables e
importantes. Esto no se aplica a los objetos: una clave de tipo string está bien en un Object ,
pero no podemos usar otro Object como clave.
Intentémoslo:
Como visitsCountObj es un objeto, convierte todas los objetos como john y ben en el mismo
string "[objeto Objeto]" . Definitivamente no es lo que queremos.
Cada llamada a map.set devuelve map en sí, así que podamos “encadenar” las llamadas:
map.set('1', 'str1')
.set(1, 'num1')
.set(true, 'bool1');
map.entries() -– devuelve un iterable para las entradas [clave, valor] . Es el que usa por defecto
en for..of .
Por ejemplo:
La iteración va en el mismo orden en que se insertaron los valores. Map conserva este orden,
a diferencia de un Objeto normal.
Si tenemos un objeto plano, y queremos crear un Map a partir de él, podemos usar el método
incorporado Object.entries(obj) que devuelve un array de pares clave/valor para un objeto en
ese preciso formato.
let obj = {
name: "John",
age: 30
};
Aquí, Object.entries devuelve el array de pares clave/valor: [ ["name","John"], ["age", 30] ] . Es lo que
necesita Map .
Existe el método Object.fromEntries que hace lo contrario: dado un array de pares [clave, valor],
crea un objeto a partir de ellos:
alert(prices.orange); // 2
Ejemplo: almacenamos los datos en un Map , pero necesitamos pasarlos a un código de terceros
que espera un objeto simple.
Aquí vamos:
alert(obj.orange); // 2
Set
Un Set es una colección de tipo especial: “conjunto de valores” (sin claves), donde cada
valor puede aparecer solo una vez.
set.delete(valor) – elimina el valor, y devuelve true si el valor existía al momento de la
llamada; si no, devuelve false .
set.has(valor) – devuelve true si el valor existe en el set, si no, devuelve false .
La característica principal es que llamadas repetidas de set.add(valor) con el mismo valor no
hacen nada. Esa es la razón por la cual cada valor aparece en Set solo una vez.
Por ejemplo, vienen visitantes y queremos recordarlos a todos. Pero las visitas repetidas no
deberían llevar a duplicados. Un visitante debe ser “contado” solo una vez.
Set es lo correcto para eso:
La alternativa a Set podría ser un array de usuarios, y código para verificar si hay
duplicados en cada inserción usando arr.find. Pero el rendimiento sería mucho peor, porque
este método recorre el array completo comprobando cada elemento. Set está optimizado
internamente para verificar unicidad.
Tenga en cuenta algo peculiar: la función callback pasada en forEach tiene 3 argumentos: un
valor, luego el mismo valor “valueAgain” y luego el objeto de destino que es set. El mismo
valor aparece en los argumentos dos veces.
Eso es por compatibilidad con Map donde la función callback tiene tres argumentos. Parece un
poco extraño, seguro. Pero en ciertos casos puede ayudar a reemplazar Map con Set y viceversa
con facilidad.
También soporta los mismos métodos que Map tiene para los iteradores:
set.entries() – devuelve un iterable para las entradas [clave, valor] , por su compatibilidad
con Map .
Resumen
Map – es una colección de valores con clave.
Métodos y propiedades:
new Map([iterable]) – crea el mapa, con un iterable (p.ej. array) de pares [clave,valor] para su
inicialización.
map.get(clave) – devuelve el valor de la clave: será undefined si la clave no existe en Map.
map.has(clave) – devuelve true si la clave existe, y false si no existe.
Métodos y propiedades:
set.delete(valor) – elimina el valor, devuelve true si valor existe al momento de la llamada; si
no, devuelve false .
set.has(valor) – devuelve true si el valor existe en el set, si no, devuelve false .
La iteración sobre Map y Set siempre está en el orden de inserción, por lo que no podemos
decir que estas colecciones están desordenadas, pero no podemos reordenar elementos u obtener
un elemento directamente por su número.
WeakMap y WeakSet
// sobrescribe la referencia
john = null;
Por ejemplo, si colocamos un objeto en un array, mientras el array esté vivo el objeto
también lo estará, incluso si no hay otras referencias a él.
Como aquí:
Del mismo modo, si usamos un objeto como la clave en un Map regular, entonces mientras exista
el Map , ese objeto también existe. Este objeto ocupa memoria y no puede ser reclamado por el
recolector de basura.
Por ejemplo:
WeakMap
La primera diferencia con Map es que en WeakMap las claves deben ser objetos, no valores
primitivos:
Ahora, si usamos un objeto como clave y no hay otras referencias a ese objeto, se eliminará
de la memoria (y del map) automáticamente.
Compárelo con el ejemplo del Map regular anterior. Ahora, si john solo existe como la clave
de WeakMap , se eliminará automáticamente del map (y de la memoria).
WeakMap no admite la iteración ni los métodos keys() , values() , entries() , así que no hay forma de
obtener todas las claves o valores de él.
WeakMap tiene solo los siguientes métodos:
weakMap.set(clave, valor)
weakMap.get(clave)
weakMap.delete(clave)
weakMap.has(clave)
¿Por qué tanta limitación? Eso es por razones técnicas. Si un objeto ha perdido todas las
demás referencias (como john en el código anterior), entonces se debe recolectar
automáticamente como basura. Pero técnicamente no se especifica exactamente cuándo se realiza
la limpieza.
El motor de JavaScript decide eso. Puede optar por realizar la limpieza de la memoria
inmediatamente o esperar y realizar la limpieza más tarde cuando ocurran más eliminaciones.
Por lo tanto, técnicamente no se conoce el recuento actual de elementos de un WeakMap . El motor
puede haberlo limpiado o no, o lo hizo parcialmente. Por esa razón, los métodos que acceden a
todas las claves/valores no son soportados.
Si estamos trabajando con un objeto que “pertenece” a otro código (tal vez incluso una
biblioteca de terceros), y queremos almacenar algunos datos asociados a él que solo deberían
existir mientras el objeto esté vivo, entonces WeakMap es exactamente lo que se necesita.
Ponemos los datos en un WeakMap utilizando el objeto como clave, y cuando el objeto sea
recolectado por el recolector de basura, esos datos también desaparecerán automáticamente.
Veamos un ejemplo.
Por ejemplo, tenemos un código que mantiene un recuento de visitas para los usuarios. La
información se almacena en un map: un objeto de usuario es la clave y el recuento de visitas
es el valor. Cuando un usuario se va (su objeto será recolectado por el recolector de
basura), ya no queremos almacenar su recuento de visitas.
//📁 visitsCount.js
let visitsCountMap = new Map(); // map: user => visits count
Y aquí hay otra parte del código, tal vez otro archivo usándolo:
//📁 main.js
let john = { name: "John" };
Ahora el objeto john debería ser recolectado como basura, pero permanece en la memoria, ya que
es una propiedad en visitCountMap .
//📁 visitsCount.js
let visitsCountMap = new WeakMap(); // weakmap: user => visits count
Ahora no tenemos que limpiar visitasCountMap . Después de que el objeto john se vuelve
inalcanzable por todos los medios excepto como una propiedad de WeakMap , se elimina de la
memoria junto con la información asociada a esa clave de WeakMap .
//📁 cache.js
let cache = new Map();
return cache.get(obj);
}
Para múltiples llamadas de proceso (obj) con el mismo objeto, solo calcula el resultado la
primera vez, y luego lo toma de caché . La desventaja es que necesitamos limpiar el ‘caché’
cuando el objeto ya no es necesario.
Si reemplazamos Map por WeakMap , este problema desaparece: el resultado en caché se eliminará
de la memoria automáticamente después de que el objeto se recolecte.
// 📁 cache.js
let cache = new WeakMap();// calcular y recordad el resultado
function process(obj) {
if (!cache.has(obj)) {
let result = /* calcular el resultado para */ obj;
cache.set(obj, result);
return result;
}
return cache.get(obj);
}
// 📁 main.js
let obj = {/* algún objeto */};
WeakSet
WeakSet se comporta de manera similar:
Es análogo a Set , pero en WeakSet solo podemos agregar objetos (no tipos primitivos).
Al igual que Set , admite add , has y delete , pero no size , keys() ni iteraciones.
Al ser “débil”, también sirve como almacenamiento adicional. Pero no para datos arbitrarios,
sino para hechos “sí/no”. Una membresía en WeakSet puede significar algo sobre el objeto.
Por ejemplo, podemos agregar usuarios a WeakSet para realizar un seguimiento de los que
visitaron nuestro sitio:
john = null;
La limitación más notable de WeakMap y WeakSet es la ausencia de iteraciones y la imposibilidad
de obtener todo el contenido actual. Esto puede parecer inconveniente, pero no impide
que WeakMap / WeakSet haga su trabajo principal: ser un almacenamiento “adicional” de datos para
objetos que se almacenan/administran en otro lugar.
Resumen
WeakMap es una colección similar a Map que permite solo objetos como propiedades y los elimina
junto con el valor asociado una vez que se vuelven inaccesibles por otros medios.
WeakSet es una colección tipo Set que almacena solo objetos y los elimina una vez que se
Sus principales ventajas son que tienen referencias débiles a los objetos, así pueden ser
fácilmente eliminados por el recolector de basura.
Esto viene al costo de no tener soporte para clear , size , keys , values …
WeakMap y WeakSet se utilizan como estructuras de dato “secundarias” además del almacenamiento de
objetos “principal”. Una vez que el objeto se elimina del almacenamiento principal, si solo
se encuentra como la clave de WeakMap o en un WeakSet , se limpiará automáticamente.
Tareas
Almacenar banderas "no leídas"
importancia: 5
Hay un array de mensajes:
let messages = [
{text: "Hello", from: "John"},
{text: "How goes?", from: "John"},
{text: "See you soon", from: "Alice"}
];
Su código puede acceder a él, pero los mensajes son administrados por el código de otra
persona. Se agregan mensajes nuevos, los códigos viejos se eliminan regularmente con ese
código, y usted no sabe los momentos exactos en que sucede.
Ahora, ¿qué estructura de datos podría usar para almacenar información sobre si el mensaje
“ha sido leído”? La estructura debe ser adecuada para dar la respuesta “¿se leyó?” para el
objeto del mensaje dado.
P.D Cuando un mensaje se elimina de messages , también debería desaparecer de su estructura.
P.P.D. No debemos modificar los objetos del mensaje, o agregarles nuestras propiedades. Como
son administrados por el código de otra persona, eso puede generarnos resultados no deseados.
solución
La pregunta ahora es: ¿qué estructura de datos es la adecuada para almacenar la información:
“¿cuándo se leyó el mensaje?”.
Alejémonos de las estructuras de datos individuales y hablemos sobre las iteraciones sobre
ellas.
En el capítulo anterior vimos métodos map.keys() , map.values() , map.entries() .
Estos métodos son genéricos, existe un acuerdo común para usarlos para estructuras de datos.
Si alguna vez creamos una estructura de datos propia, también deberíamos implementarla.
Map
Set
Array
Los objetos simples también admiten métodos similares, pero la sintaxis es un poco diferente.
Map Objeto
let user = {
name: "John",
age: 30
};
Aquí hay un ejemplo del uso de Object.values para recorrer los valores de propiedad:
let user = {
name: "John",
age: 30
};
Transformando objetos
Los objetos carecen de muchos métodos que existen para los arrays, tales como map , filter y
otros.
Si queremos aplicarlos, entonces podemos usar Object.entries seguido de Object.fromEntries :
2. Use métodos de array en ese array, por ejemplo map para transformar estos pares
clave/valor.
Puede parecer difícil a primera vista, pero se vuelve fácil de entender después de usarlo una
o dos veces. Podemos hacer poderosas cadenas de transformaciones de esta manera.
Asignación desestructurante
Las dos estructuras de datos más usadas en JavaScript son Object y Array .
Los objetos nos permiten crear una simple entidad que almacena items con una clave cada
uno.
Pero cuando los pasamos a una función, tal vez no necesitemos un objeto o array como un
conjunto sino en piezas individuales.
La desestructuración también funciona bien con funciones complejas que tienen muchos
argumentos, valores por defecto, etcétera. Pronto lo veremos.
Desestructuración de Arrays
Un ejemplo de cómo el array es desestructurado en variables:
// asignación desestructurante
// fija firstName = arr[0]
// y surname = arr[1]
let [firstName, surname] = arr;alert(firstName); // John
alert(surname); // Smith
Se ve genial cuando se combina con split u otro método que devuelva un array:
Como puedes ver, la sintaxis es simple. Aunque hay varios detalles peculiares. Veamos más
ejemplos para entenderlo mejor.
“Desestructuración” no significa “destructivo”.
Esto funciona, porque internamente una desestructuración trabaja iterando sobre el valor de
la derecha. Es una clase de azúcar sintáctica para llamar for..of sobre el valor a la derecha
del = y asignar esos valores.
alert(user.name); // John
alert(user.surname); // Smith
let user = {
name: "John",
age: 30
};
// recorrer claves-y-valores
for (let [key, value] of Object.entries(user)) {alert(`${key}:${value}`); // name:John, luego age:30
}
// Map itera como pares [key, value], muy conveniente para desestructurar
for (let [key, value] of user) {alert(`${key}:${value}`); // name:John, luego age:30
}
Hay un conocido truco para intercambiar los valores de dos variables usando asignación
desestructurante:
El resto ‘…’
En general, si el array es mayor que la lista de la izquierda, los ítems extras son omitidos.
Por ejemplo, aquí solo dos items son tomados, el resto simplemente es ignorado:
let [name1, name2] = ["Julius", "Caesar", "Consul", "of the Roman Republic"];
alert(name1); // Julius
alert(name2); // Caesar
// items posteriores no serán asignados a ningún lugar
si queremos también obtener todo lo que sigue, podemos agregarle un parámetro que obtiene “el
resto” usando puntos suspensivos “…”`:
let [name1, name2, ...rest] = ["Julius", "Caesar", "Consul", "of the Roman Republic"];
El valor de rest es un array con los elementos restantes del array original.
Podemos usar cualquier otro nombre de variable en lugar de rest , sólo hay que asegurar que
tenga tres puntos que lo antecedan y que esté último en la asignación desestructurante.
let [name1, name2, ...titles] = ["Julius", "Caesar", "Consul", "of the Roman Republic"];
// ahora titles = ["Consul", "of the Roman Republic"]
Valores predeterminados
Si el array es más corto que la lista de variables a la izquierda, no habrá errores. Los
valores ausentes son considerados undefined:
// valores predeterminados
let [name = "Guest", surname = "Anonymous"] = ["Julius"];alert(name); // Julius (desde array)
alert(surname); // Anonymous (predeterminado utilizado)
Los valores predeterminados pueden ser expresiones más complejas e incluso llamadas a
función, que serán evaluadas sólo si el valor no ha sido proporcionado.
Por ejemplo, aquí utilizamos la función prompt para dos valores predeterminados.
Observa que el prompt se ejecuta solamente para el valor faltante ( surname ).
let options = {
title: "Menu",
width: 100,
height: 200
};
Las propiedades options.title , options.width y options.height son asignadas a las variables
correspondientes.
El patrón de la izquierda puede ser más complejo y especificar el mapeo entre propiedades y
variables.
Si queremos asignar una propiedad a una variable con otro nombre, por ejemplo
que options.width vaya en la variable llamada w , lo podemos establecer usando dos puntos:
let options = {
title: "Menu",
width: 100,
height: 200
};
// { propiedadOrigen: variableObjetivo }
let {width: w, height: h, title} = options;// width -> w
// height -> h
// title -> title
alert(title); // Menu
alert(w); // 100
alert(h); // 200
Los dos puntos muestran “qué : va dónde”. En el ejemplo de arriba la propiedad width va
a w , height va a h , y title es asignado al mismo nombre.
Para propiedades potencialmente faltantes podemos establecer valores predeterminados
utilizando "=" , de esta manera:
let options = {
title: "Menu"
};
Al igual que con arrays o argumentos de función, los valores predeterminados pueden ser
cualquier expresión e incluso llamados a función, las que serán evaluadas si el valor no ha
sido proporcionado.
let options = {
title: "Menu"
};
let options = {
title: "Menu"
};
Si tenemos un objeto complejo con muchas propiedades, podemos extraer solamente las que
necesitamos:
let options = {
title: "Menu",
width: 100,
height: 200
};
alert(title); // Menu
Podemos usar el patrón resto de la misma forma que lo usamos con arrays. Esto no es soportado
en algunos navegadores antiguos (para IE, use el polyfill Babel), pero funciona en los
navegadores modernos.
Se ve así:
let options = {
title: "Menu",
height: 200,
width: 100
};
Esto no funcionará:
{
// una bloque de código
let message = "Hola";
// ...
alert( message );
}
Aquí JavaScript supone que tenemos un bloque de código, es por eso que hay un error. Nosotros
en cambio queremos desestructuración.
Para mostrarle a JavaScript que no es un bloque de código, podemos rodear la expresión entre
paréntesis (...) :
Desestructuración anidada
Si un objeto o array contiene objetos y arrays anidados, podemos utilizar patrones del lado
izquierdo más complejos para extraer porciones más profundas.
En el código de abajo options tiene otro objeto en la propiedad size y un array en la
propiedad items . El patrón en el lado izquierdo de la asignación tiene la misma estructura
para extraer valores de ellos:
let options = {
size: {
width: 100,
height: 200
},
items: ["Cake", "Donut"],
extra: true
};
alert(title); // Menu
alert(width); // 100
alert(height); // 200
alert(item1); // Cake
alert(item2); // Donut
Todas las propiedades del objeto options con excepción de extra que no está en el lado
izquierda, son asignadas a las variables correspondientes:
Por último tenemos width , height , item1 , item2 y title desde el valor predeterminado.
Tenga en cuenta que no hay variables para size e items , ya que tomamos su contenido en su
lugar.
En la vida real, el problema es cómo recordar el orden de los argumentos. Normalmente los
IDEs (Entorno de desarrollo integrado) intentan ayudarnos, especialmente si el código está
bien documentado, pero aún así… Otro problema es cómo llamar a una función si queremos que
use sus valores predeterminados en la mayoría de los argumentos.
¿Así?
Esto no es nada grato. Y se torna ilegible cuando tratamos con muchos argumentos.
¡La desestructuración llega al rescate!
Podemos pasar los argumentos como un objeto, y la función inmediatamente los desestructura en
variables:
showMenu(options);
También podemos usar desestructuración más compleja con objetos anidados y mapeo de dos
puntos:
let options = {
title: "My menu",
items: ["Item1", "Item2"]
};
function showMenu({
title = "Untitled",
width: w = 100, // width va a w
height: h = 200, // height va a h
items: [item1, item2] // el primer elemento de items va a item1, el segundo a item2
}) {alert( `${title} ${w} ${h}` ); // My Menu 100 200
alert( item1 ); // Item1
alert( item2 ); // Item2
}
showMenu(options);
function({
incomingProperty: varName = defaultValue // propiedadEntrante: nombreVariable = valorPredeterminado
...
})
Podemos solucionar esto, poniendo {} como valor predeterminado para todo el objeto de
argumentos:
En el código de arriba, todo el objeto de argumentos es {} por defecto, por lo tanto siempre
hay algo para desestructurar.
Resumen
La asignación desestructurante permite mapear instantáneamente un objeto o array en varias
variables.
Esto significa que la propiedad prop se asigna a la variable varName ; pero si no existe tal
propiedad, se usa el valor default .
Las propiedades de objeto que no fueron mapeadas son copiadas al objeto rest .
El primer item va a item1 , el segundo a item2 , todos los ítems restantes crean el
array resto .
Es posible extraer información desde arrays/objetos anidados, para esto el lado izquierdo
debe tener la misma estructura que el lado derecho.
Fecha y Hora
Aprendamos un nuevo objeto incorporado de JS: Date. Este objeto almacena la fecha, la hora, y
brinda métodos para administrarlas.
Por ejemplo, podemos usarlo para almacenar horas de creación o modificación, medir tiempo, o
simplemente mostrar en pantalla la fecha actual.
Creación
Para crear un nuevo objeto Date se lo instancia con new Date() junto con uno de los siguientes
argumentos:
new Date() Sin argumentos – crea un objeto Date para la fecha y la hora actuales:
Date(milliseconds) Crea un objeto Date con la cantidad de tiempo igual al número de milisegundos
(1/1000 de un segundo) transcurrido a partir del 1° de enero de 1970 UTC+0.
// 31 Dec 1969
let Dec31_1969 = new Date(-24 * 3600 * 1000);
Crea una fecha con los componentes pasados como argumentos en la zona horaria local. Sólo los
primeros dos parámetros son obligatorios.
• El año debería tener 4 dígitos. Por compatibilidad, aquí 2 dígitos serán considerados
‘19xx’, pero 4 dígitos es lo firmemente sugerido.
• La cuenta del mes comienza desde el 0 (enero), y termina en el 11 (diciembre).
• El parámetro fecha efectivamente es el día del mes, si está ausente se asume su valor en 1 .
• Si los parámetros horas/minutos/segundos/ms están ausentes, se asumen sus valores iguales a 0 .
Por ejemplo:
Si tu zona horaria está desplazada respecto de UTC el código de abajo va a mostrar horas
diferentes:
// fecha actual
let date = new Date();
// la hora respecto de la zona horaria UTC+0 (Hora de Londres sin horario de verano)
alert( date.getUTCHours() );
Además de los anteriormente mencionados, hay dos métodos especiales que no poseen una
variante de UTC:
getTime()Devuelve el timestamp para una fecha determinada – cantidad de milisegundos
transcurridos a partir del 1° de Enero de 1970 UTC+0.getTimezoneOffset()Devuelve la
diferencia entre UTC y el huso horario de la zona actual, en minutos:
setMonth(month, [date])
setDate(date)
setSeconds(sec, [ms])
setMilliseconds(ms)
A excepción de setTime() , todos los demás métodos poseen una variante UTC, por
ejemplo: setUTCHours() .
Como podemos ver, algunos métodos nos permiten fijar varios componentes al mismo tiempo, por
ej. setHours . Los componentes que no son mencionados no se modifican.
Por ejemplo:
today.setHours(0);
alert(today); // Sigue siendo el día de hoy, pero con la hora cambiada a 0.
today.setHours(0, 0, 0, 0);
alert(today); // Sigue siendo la fecha de hoy, pero ahora en formato 00:00:00 en punto.
Autocorrección
La autocorrección es una característica muy útil de los objetos Date . Podemos fijar valores
fuera de rango, y se ajustarán automáticamente.
Por ejemplo:
Por ejemplo, supongamos que necesitamos incrementar la fecha “28 Feb 2016” en 2 días. El
resultado puede ser “2 Mar” o “1 Mar” dependiendo de si es año bisiesto. Afortunadamente, no
tenemos de qué preocuparnos. Sólo debemos agregarle los 2 días y el objeto Date se encargará
del resto:
date.setDate(0); // el día mínimo es 1, entonces asume el último día del mes anterior
alert( date ); // 31 Dec 2015
Date.now()
Si lo único que queremos es medir el tiempo transcurrido, no es necesario utilizar el
objeto Date .
Podemos utilizar el método especial Date.now() que nos devuelve el timestamp actual.
Es el equivalente semántico a new Date().getTime() , pero no crea una instancia intermediaria del
objeto Date . De esta manera, el proceso es mas rápido y, por consiguiente, no afecta a la
recolección de basura.
Mayormente se utiliza por conveniencia o cuando la performance del código es fundamental,
como por ejemplo en juegos de JavaScript u otras aplicaciones específicas.
Por lo tanto, es mejor hacerlo de esta manera:
let start = Date.now(); // milisegundos transcurridos a partir del 1° de Enero de 1970// la función realiza su trabajo
for (let i = 0; i < 100000; i++) {
let doSomething = i * i * i;
}
let end = Date.now(); // listoalert( `El bucle tardó ${end - start} ms` ); // restamos números en lugar de fechas
Benchmarking
Si queremos realizar una medición de performance confiable de una función que vaya a consumir
muchos recursos de CPU, debemos hacerlo con precaución.
En este caso, vamos a medir dos funciones que calculen la diferencia entre dos fechas
determinadas: ¿Cuál es la más rápida?
// Tenemos date1 y date2. ¿Cuál de las siguientes funciones nos devuelve su diferencia, expresada en ms, más rápido?
function diffSubtract(date1, date2) {
return date2 - date1;
}
// o
function diffGetTime(date1, date2) {
return date2.getTime() - date1.getTime();
}
La primera idea sería ejecutar las funciones varias veces seguidas y medir la diferencia de
tiempo de ejecución. En nuestro caso, las funciones son bastante simples, por lo que debemos
hacerlo al menos unas 100000 veces.
Midamos:
function bench(f) {
let date1 = new Date(0);
let date2 = new Date();
¡Guau! ¡Utilizando el método getTime() es mucho más rápido! Esto es debido a que no se produce
ninguna conversión de tipo de dato, por lo que se le hace mucho mas fácil de optimizar a los
motores.
Es un escenario bastante posible para los sistemas operativos multi-procesos de hoy en día.
Como consecuencia, el primer benchmark dispondrá de una menor cantidad de recursos de CPU que
el segundo, lo que podría generar resultados engañosos.
function bench(f) {
let date1 = new Date(0);
let date2 = new Date();
let time1 = 0;
let time2 = 0;
Los motores modernos de JavaScript realizan una optimización avanzada únicamente a los
bloques de código que se ejecutan varias veces (no es necesario optimizar código que
raramente se ejecuta). En el ejemplo de abajo, las primeras ejecuciones no están bien
optimizadas, por lo que quizás querríamos agregar ejecuciones antes de realizar el benchmark,
a modo de “precalentamiento”:
El carácter 'Z' es opcional y especifica la zona horaria, con el formato +-hh:mm . Si se
incluye únicamente la letra Z equivale a UTC+0.
También es posible pasar como string variantes abreviadas, tales como YYYY-MM-DD o YYYY-MM o
incluso YYYY .
La llamada del método Date.parse(str) convierte el string en el formato especificado y nos
devuelve un timestamp (cantidad de milisegundos transcurridos desde el 1° de Enero de 1970
UTC+0). Si el formato del string no es válido, devuelve es NaN .
Por ejemplo:
let ms = Date.parse("2012-01-26T13:51:50.417-07:00");
alert(date);
Resumen
Los días de la semana en getDay() también se cuentan desde el cero (que corresponde al día
Domingo).
El objeto Date se autocorrige cuando recibe un componente fuera de rango. Es útil para
sumar o restar días/meses/horas.
Las fechas se pueden restar entre sí, dando el resultado expresado en milisegundos: esto
se debe a que el objeto Date toma el valor del timestamp cuando es convertido a número.
Node.js posee el módulo microtime , entre otros. Prácticamente casi cualquier dispositivo y
entorno de ejecución permite mayor precisión, sólo que no es posible almacenarla en Date .
Digamos que tenemos un objeto complejo y nos gustaría convertirlo en un string (cadena de
caracteres), para enviarlos por la red, o simplemente mostrarlo para fines de registro.
Naturalmente, tal string debe incluir todas las propiedades importantes.
let user = {
name: "John",
age: 30,
toString() {
return `{name: "${this.name}", age: ${this.age}}`;
}};
Por suerte no hay necesidad de escribir el código para manejar todo esto. La tarea ya ha sido
resuelta.
JSON.stringify
JSON (Notación de objeto JavaScript) es un formato general para representar valores y
objetos. Se lo describe como el estándar RFC 4627. En un principio fue creado para
Javascript, pero varios lenguajes tienen librerías para manejarlo también. Por lo tanto es
fácil utilizar JSON para intercambio de información cuando el cliente utiliza JavaScript y el
servidor está escrito en Ruby, PHP, Java, lo que sea.
let student = {
name: 'John',
age: 30,
isAdmin: false,
courses: ['html', 'css', 'js'],
spouse: null
};
alert(json);
/* Objeto JSON-codificado:
{
"name": "John",
"age": 30,
"isAdmin": false,
"courses": ["html", "css", "js"],
"spouse": null
}
*/
Los strings utilizan comillas dobles. No hay comillas simples o acentos abiertos en JSON.
Por lo tanto 'John' pasa a ser "John" .
Los nombres de propiedades de objeto también llevan comillas dobles. Eso es obligatorio.
Por lo tanto age:30 pasa a ser "age":30 .
Objects { ... }
Arrays [ ... ]
Primitives:
strings,
numbers,
null .
Por ejemplo:
// un string en JSON sigue siendo una cadena de caracteres, pero con comillas dobles
alert( JSON.stringify('test') ) // "test"
JSON es una especificación de sólo datos independiente del lenguaje, por lo tanto algunas
propiedades de objeto específicas de Javascript son omitidas por JSON.stringify .
Propiedades simbólicas.
let user = {
sayHi() { // ignorado
alert("Hello");
},
[Symbol("id")]: 123, // ignorado
something: undefined // ignorado
};
Normalmente esto está bien. Si esto no es lo que queremos, pronto veremos cómo personalizar
el proceso.
let meetup = {
title: "Conference",
room: {
number: 23,
participants: ["john", "ann"]
}};
alert( JSON.stringify(meetup) );
/* La estructura completa es convertida a String:
{
"title":"Conference",
"room":{"number":23,"participants":["john","ann"]},
}
*/
let room = {
number: 23
};
let meetup = {
title: "Conference",
participants: ["john", "ann"]
};
Aquí, la conversión falla debido a una referencia circular: room.occupiedBy hace referencia
a meetup , y meetup.place hace referencia a room :
Por ejemplo:
let room = {
number: 23
};
let meetup = {
title: "Conference",
participants: [{name: "John"}, {name: "Alice"}],
place: room // meetup hace referencia a room
};
let room = {
number: 23
};
let meetup = {
title: "Conference",
participants: [{name: "John"}, {name: "Alice"}],
place: room // meetup hace referencia a room
};
Ahora todo con excepción de occupiedBy está serializado. Pero la lista de propiedades es
bastante larga.
Por suerte podemos utilizar una función en lugar de un array como el sustituto .
La función se llamará para cada par de (propiedad, valor) y debe devolver el valor “sustituido”,
el cual será utilizado en lugar del original. O undefined si el valor va a ser omitido.
En nuestro caso, podemos devolver value “tal cual” para todo excepto occupiedBy . Para
ignorar occupiedBy , el código de abajo devuelve undefined :
let room = {
number: 23
};
let meetup = {
title: "Conference",
participants: [{name: "John"}, {name: "Alice"}],
place: room // meetup hace referencia a room
};
Por favor tenga en cuenta que la función replacer recibe todos los pares de propiedad/valor
incluyendo objetos anidados y elementos de array. Se aplica recursivamente. El valor
de this dentro de replacer es el objeto que contiene la propiedad actual.
es el objeto objetivo como un todo. Es por esto que la primer línea es ":[object Object]" en el
ejemplo de arriba.
La idea es proporcionar tanta capacidad para replacer como sea posible: tiene una oportunidad
de analizar y reemplazar/omitir incluso el objeto entero si es necesario.
Formato: espacio
El tercer argumento de JSON.stringify(value, replacer, space) es el número de espacios a utilizar para
un formato agradable.
let user = {
name: "John",
age: 25,
roles: {
isAdmin: false,
isEditor: true
}
};
El tercer argumento puede ser también string. En ese caso el string será usado como
indentación en lugar de un número de espacios.
El argumento space es utilizado únicamente para propósitos de registro y agradable impresión.
“toJSON” Personalizado
let room = {
number: 23
};
let meetup = {
title: "Conference",
date: new Date(Date.UTC(2017, 0, 1)),
room
};
alert( JSON.stringify(meetup) );
/*
{
"title":"Conference",
"date":"2017-01-01T00:00:00.000Z", // (1)
"room": {"number":23} // (2)
}
*/
Aquí podemos ver que date (1) se convirtió en un string. Esto es debido a que todas las fechas
tienen un método toJSON incorporado que devuelve este tipo de string.
Ahora incluyamos un toJSON personalizado para nuestro objeto room (2) :
let room = {
number: 23,
toJSON() {
return this.number;
}};
let meetup = {
title: "Conference",
room
};
Como podemos ver, toJSON es utilizado para ambos el llamado directo JSON.stringify(room) y
cuando room está anidado en otro objeto codificado.
JSON.parse
Para decodificar un string JSON, necesitamos otro método llamado JSON.parse.
La sintaxis:
strstring JSON para analizar.reviverfunction(key,value) opcional que será llamado para cada
par (propiedad, valor) y puede transformar el valor.
Por ejemplo:
numbers = JSON.parse(numbers);
alert( numbers[1] ); // 1
let userData = '{ "name": "John", "age": 35, "isAdmin": false, "friends": [0,1,2,3] }';
alert( user.friends[1] ); // 1
El JSON puede ser tan complejo como sea necesario, los objetos y arrays pueden incluir otros
objetos y arrays. Pero deben cumplir el mismo formato JSON.
Aquí algunos de los errores más comunes al escribir JSON a mano (a veces tenemos que
escribirlo por debugging):
let json = `{
name: "John", // error: nombre de propiedad sin comillas
"surname": 'Smith', // error: comillas simples en valor (debe ser doble)
'isAdmin': false // error: comillas simples en propiedad (debe ser doble)
"birthday": new Date(2000, 2, 3), // error: no se permite "new", únicamente valores simples
"friends": [0,1,2,3] // aquí todo bien
}`;
Existe otro formato llamado JSON5, que permite claves sin comillas, comentarios, etcétera.
Pero es una librería independiente, no una especificación del lenguaje.
El JSON normal es tan estricto no porque sus desarrolladores sean flojos, sino para permitir
la implementación fácil, confiable y muy rápida del algoritmo analizador.
Utilizando reactivador
Imagina esto, obtenemos un objeto meetup convertido en String desde el servidor.
Se ve así:
Resumen
JSON es un formato de datos que tiene su propio estándar independiente y librerías para la
mayoría de los lenguajes de programación.
La recursión es un patrón de programación que es útil en situaciones en las que una tarea
puede dividirse naturalmente en varias tareas del mismo tipo, pero más simples. O cuando una
tarea se puede simplificar en una acción fácil más una variante más simple de la misma tarea.
O, como veremos pronto, tratar con ciertas estructuras de datos.
Sabemos que cuando una función resuelve una tarea, en el proceso puede llamar a muchas otras
funciones. Un caso particular de esto se da cuando una función se llama a sí misma. Esto es
lo que se llama recursividad.
pow(2, 2) = 4
pow(2, 3) = 8
pow(2, 4) = 16
function pow(x, n) {
let result = 1;
alert( pow(2, 3) ); // 8
function pow(x, n) {
if (n == 1) {
return x;
} else {
return x * pow(x, n - 1);
}
}
alert( pow(2, 3) ); // 8
if n==1 = x
/
pow(x, n) =
\
else = x * pow(x, n - 1)
2. De lo contrario, podemos representar pow (x, n) como x * pow (x, n - 1) . En matemáticas, uno
escribiría x n = x * x n-1 . Esto se llama paso recursivo: transformamos la tarea en una
acción más simple (multiplicación por x ) y una llamada más simple de la misma tarea
( pow con menor n ). Los siguientes pasos lo simplifican más y más hasta que n llegue a 1 .
También podemos decir que pow se llama a sí mismo recursivamente hasta que n == 1 .
Por ejemplo, para calcular pow (2, 4) la variante recursiva realiza estos pasos:
1. pow(2, 4) = 2 * pow(2, 3)
2. pow(2, 3) = 2 * pow(2, 2)
3. pow(2, 2) = 2 * pow(2, 1)
4. pow(2, 1) = 2
Por lo tanto, la recursión reduce una llamada de función a una más simple y luego… a una más
simple, y así sucesivamente, hasta que el resultado se vuelve obvio.
El contexto de ejecución es una estructura de datos interna que contiene detalles sobre la
ejecución de una función: dónde está el flujo de control ahora, las variables actuales, el
valor de this (que no usamos aquí) y algunos otros detalles internos.
Una llamada de función tiene exactamente un contexto de ejecución asociado.
Una vez que finaliza, el antiguo contexto de ejecución se recupera de la pila y la función
externa se reanuda desde donde se pausó.
pow (2, 3)
Al comienzo de la llamada pow (2, 3) el contexto de ejecución almacenará variables: x = 2, n = 3 ,
el flujo de ejecución está en la línea 1 de la función.
Podemos esbozarlo como:
Ahí es cuando la función comienza a ejecutarse. La condición n == 1 es falsa, por lo que el
flujo continúa en la segunda rama de if :
function pow(x, n) {
if (n == 1) {
return x;
} else {
return x * pow(x, n - 1);}
}
alert( pow(2, 3) );
Las variables son las mismas, pero la línea cambia, por lo que el contexto es ahora:
Para calcular x * pow (x, n - 1) , necesitamos hacer una sub-llamada de pow con nuevos
argumentos pow (2, 2) .
pow (2, 2)
Para hacer una llamada anidada, JavaScript recuerda el contexto de ejecución actual en
la pila de contexto de ejecución.
Aquí llamamos a la misma función pow , pero no importa en absoluto. El proceso es el mismo
para todas las funciones:
En la figura usamos la palabra línea “line” porque en nuestro ejemplo hay solo una subllamada
en línea, pero generalmente una simple línea de código puede contener múltiples subllamadas,
como pow(…) + pow(…) + otraCosa(…) .
Entonces sería más preciso decir que la ejecución se reanuda “inmediatamente después de la
subllamada”.
pow(2, 1)
El proceso se repite: se realiza una nueva subllamada en la línea 5 , ahora con argumentos x =
2 , n = 1 .
La salida
Durante la ejecución de pow (2, 1) , a diferencia de antes, la condición n == 1 es verdadera, por
lo que funciona la primera rama de if :
function pow(x, n) {
if (n == 1) {
return x;} else {
return x * pow(x, n - 1);
}
}
Se reanuda la ejecución de pow (2, 2) . Tiene el resultado de la subllamada pow (2, 1) , por lo que
también puede finalizar la evaluación de x * pow (x, n - 1) , devolviendo 4 .
Tenga en cuenta los requisitos de memoria. Los contextos toman memoria. En nuestro caso,
elevar a la potencia de n realmente requiere la memoria para n contextos, para todos los
valores más bajos de n .
Un algoritmo basado en bucles ahorra más memoria:
function pow(x, n) {
let result = 1;
return result;
}
El pow iterativo utiliza un solo contexto, cambiando i y result en el proceso. Sus requisitos
de memoria son pequeños, fijos y no dependen de n .
Cualquier recursión puede reescribirse como un bucle. La variante de bucle generalmente se
puede hacer más eficaz.
La recursión puede dar un código más corto y fácil de entender y mantener. No se requiere
optimización en todo lugar, principalmente lo que nos interesa es un buen código y por eso se
usa.
Recorridos recursivos
Otra gran aplicación de la recursión es un recorrido recursivo.
Imagina que tenemos una empresa. La estructura del personal se puede presentar como un
objeto:
let company = {
sales: [{
name: 'John',
salary: 1000
}, {
name: 'Alice',
salary: 1600
}],
development: {
sites: [{
name: 'Peter',
salary: 2000
}, {
name: 'Alex',
salary: 1800
}],
Un departamento puede tener una gran variedad de personal. Por ejemplo, el departamento de
ventas sales tiene 2 empleados: John y Alice.
Ahora digamos que queremos una función para obtener la suma de todos los salarios. ¿Cómo
podemos hacer eso?
Un enfoque iterativo no es fácil, porque la estructura no es simple. La primera idea puede
ser hacer un bucle for sobre company con un sub-bucle anidado sobre departamentos de primer
nivel. Pero luego necesitamos más sub-bucles anidados para iterar sobre el personal en los
departamentos de segundo nivel como sites . …¿Y luego otro sub-bucle dentro de los de los
departamentos de tercer nivel que podrían aparecer en el futuro? ¿Deberíamos parar en el
nivel 3 o hacer 4 niveles de bucles? Si ponemos 3-4 bucles anidados en el código para
atravesar un solo objeto, se vuelve bastante feo.
Probemos la recursividad.
Como podemos ver, cuando nuestra función hace que un departamento sume, hay dos casos
posibles:
1. O bien es un departamento “simple” con una array de personas: entonces podemos sumar los
salarios en un bucle simple.
Podemos ver fácilmente el principio: para un objeto {...} se realizan subllamadas, mientras
que los Arrays [...] son las “hojas” del árbol recursivo y dan un resultado inmediato.
Tenga en cuenta que el código utiliza funciones inteligentes que hemos cubierto antes:
Bucle for (val of Object.values (obj)) para iterar sobre los valores del
objeto: Object.values devuelve una matriz de ellos.
Estructuras recursivas
Una estructura de datos recursiva (definida recursivamente) es una estructura que se replica
en partes.
Lo acabamos de ver en el ejemplo de la estructura de la empresa anterior.
Un departamento de la empresa es:
O un array de personas.
O un objeto con departamentos.
Para los desarrolladores web hay ejemplos mucho más conocidos: documentos HTML y XML.
En el documento HTML, una etiqueta HTML puede contener una lista de:
Piezas de texto.
Comentarios HTML.
Para una mejor comprensión, cubriremos una estructura recursiva más llamada “Lista enlazada”
que podría ser una mejor alternativa para las matrices en algunos casos.
Lista enlazada
Imagina que queremos almacenar una lista ordenada de objetos.
La elección natural sería un array:
…Pero hay un problema con los Arrays. Las operaciones “eliminar elemento” e “insertar
elemento” son costosas. Por ejemplo, la operación arr.unshift(obj) debe renumerar todos los
elementos para dejar espacio para un nuevo obj , y si la matriz es grande, lleva tiempo. Lo
mismo con arr.shift () .
Las únicas modificaciones estructurales que no requieren renumeración masiva son aquellas que
operan con el final del array: arr.push/pop . Por lo tanto, un array puede ser bastante lento
para grandes colas si tenemos que trabajar con el principio del mismo.
Como alternativa, si realmente necesitamos una inserción/eliminación rápida, podemos elegir
otra estructura de datos llamada lista enlazada.
El elemento de lista enlazada se define de forma recursiva como un objeto con:
value .
propiedad next que hace referencia al siguiente elemento de lista enlazado o null si ese es
el final.
Por ejemplo:
let list = {
value: 1,
next: {
value: 2,
next: {
value: 3,
next: {
value: 4,
next: null
}
}
}
};
Aquí podemos ver aún más claramente que hay varios objetos, cada uno tiene su value y
un next apuntando al vecino. La variable list es el primer objeto en la cadena, por lo que
siguiendo los punteros next de ella podemos alcanzar cualquier elemento.
La lista se puede dividir fácilmente en varias partes y luego volver a unir:
list.next.next = secondList;
Para eliminar un valor del medio, cambie el next del anterior:
list.next = list.next.next;
Hicimos que list.next salte sobre 1 al valor 2 . El valor 1 ahora está excluido de la cadena.
Si no se almacena en ningún otro lugar, se eliminará automáticamente de la memoria.
A diferencia de los arrays, no hay renumeración en masa, podemos reorganizar fácilmente los
elementos.
Naturalmente, las listas no siempre son mejores que los Arrays. De lo contrario, todos
usarían solo listas.
El principal inconveniente es que no podemos acceder fácilmente a un elemento por su número.
En un Array eso es fácil: arr[n] es una referencia directa. Pero en la lista tenemos que
comenzar desde el primer elemento e ir siguiente N veces para obtener el enésimo elemento.
… Pero no siempre necesitamos tales operaciones. Por ejemplo, cuando necesitamos una cola o
incluso un deque: la estructura ordenada que debe permitir agregar/eliminar elementos muy
rápidamente desde ambos extremos.
Podemos también agregar una variable llamada tail (cola) referenciando el último elemento
de la lista (y actualizarla cuando se agregan/remueven elementos del final).
Resumen
Glosario:
Recursion es concepto de programación que significa que una función se llama a sí misma.
Las funciones recursivas se pueden utilizar para resolver ciertas tareas de manera
elegante.
Cada vez que una función se llama a sí misma ocurre un paso de recursión. La base de la
recursividad se da cuando los argumentos de la función hacen que la tarea sea tan básica
que la función no realiza más llamadas.
Los árboles como el árbol de elementos HTML o el árbol de departamentos de este capítulo
también son naturalmente recursivos: se ramifican y cada rama puede tener otras ramas.
Las funciones recursivas se pueden usar para recorrerlas como hemos visto en el
ejemplo sumSalary .
Cualquier función recursiva puede reescribirse en una iterativa. Y eso a veces es necesario
para optimizar las cosas. Pero para muchas tareas, una solución recursiva es lo
suficientemente rápida y fácil de escribir y mantener.
Por ejemplo:
Object.assign(dest, src1, ..., srcN) – copia las propiedades de src1..N en dest .
…y otros más
En este capítulo aprenderemos como hacer lo mismo. Y, además, cómo trabajar cómodamente con
dichas funciones y arrays.
function sum(a, b) {
return a + b;
}
alert( sum(1, 2, 3, 4, 5) );
return sum;
}
alert( sumAll(1) ); // 1
alert( sumAll(1, 2) ); // 3
alert( sumAll(1, 2, 3) ); // 6
Podemos elegir obtener los primeros parámetros como variables, y juntar solo el resto.
Aquí los primeros dos argumentos van a variables y el resto va al array titles :
Los parámetros rest recogen todos los argumentos sobrantes, por lo que el siguiente código no
tiene sentido y causa un error:
La variable “arguments”
También existe un objeto símil-array especial llamado arguments que contiene todos los
argumentos indexados.
Por ejemplo:
function showName() {
alert( arguments.length );
alert( arguments[0] );
alert( arguments[1] );
// arguments es iterable
// for(let arg of arguments) alert(arg);
}
function f() {
let showArg = () => alert(arguments[0]);
showArg();
}
f(1); // 1
Como recordamos, las funciones de flecha no tienen su propio this . Ahora sabemos que tampoco
tienen el objeto especial arguments .
Sintaxis Spread
Acabamos de ver cómo obtener un array de la lista de parámetros.
alert( Math.max(3, 5, 1) ); // 5
Ahora bien, supongamos que tenemos un array [3, 5, 1] . ¿Cómo ejecutamos Math.max con él?
Pasando la variable no funcionará, porque Math.max espera una lista de argumentos numéricos, no
un único array:
Y seguramente no podremos listar manualmente los ítems en el código Math.max(arr[0], arr[1], arr[2]) ,
porque tal vez no sepamos cuántos son. A medida que nuestro script se ejecuta, podría haber
muchos elementos, o podría no haber ninguno. Y eso podría ponerse feo.
¡Operador Spread al rescate! Es similar a los parámetros rest, también usa ... , pero hace
exactamente lo opuesto.
Cuando ...arr es usado en el llamado de una función, “expande” el objeto iterable arr en una
lista de argumentos.
Para Math.max :
let merged = [0, ...arr, 2, ...arr2];alert(merged); // 0,3,5,1,2,8,9,15 (0, luego arr, después 2, después arr2)
En los ejemplos de arriba utilizamos un array para demostrar el operador spread, pero
cualquier iterable funcionará también.
Por ejemplo, aquí usamos el operador spread para convertir la cadena en un array de
caracteres:
El operador spread utiliza internamente iteradores para iterar los elementos, de la misma
manera que for..of hace.
Entonces, para una cadena for..of retorna caracteres y ...str se convierte en "H","o","l","a" . La
lista de caracteres es pasada a la inicialización del array [...str] .
Para esta tarea en particular también podríamos haber usado Array.from , ya que convierte un
iterable (como una cadena de caracteres) en un array:
Por lo tanto, para la tarea de convertir algo en un array, Array.from tiende a ser más
universal.
Nota que es posible hacer lo mismo para hacer una copia de un objeto:
let obj = { a: 1, b: 2, c: 3 };
Esta manera de copiar un objeto es mucho más corta que let objCopy = Object.assign({}, obj); o para un
array let arrCopy = Object.assign([], arr); por lo que preferimos usarla siempre que podemos.
Resumen
Cuando veamos "..." en el código, son los parámetros rest o el operador spread.
Hay una manera fácil de distinguir entre ellos:
Cuando ... se encuentra al final de los parámetros de una función, son los “parámetros
rest” y recogen el resto de la lista de argumentos en un array.
Cuando ... está en el llamado de una función o similar, se llama “operador spread” y
expande un array en una lista.
Patrones de uso:
Los parámetros rest son usados para crear funciones que acepten cualquier número de
argumentos.
El operador spread es usado para pasar un array a funciones que normalmente requieren una
lista de muchos argumentos.
avaScript es un lenguaje muy orientado a funciones. Nos da mucha libertad. Una función se
puede crear en cualquier momento, pasar como argumento a otra función y luego llamar desde un
lugar de código totalmente diferente más tarde.
Y si una función se pasa como parámetro y se llama desde otro lugar del código, ¿tendrá
acceso a las variables externas en el nuevo lugar?
Ampliemos nuestro conocimiento para comprender estos escenarios y otros más complejos.
Aquí hablaremos de variables let/const
En JavaScript, hay 3 formas de declarar una variable: let , const (las modernas) y var (más
antigua).
Las variables declaradas con const se comportan igual, por lo que este artículo también
trata sobre const .
El antiguo var tiene algunas diferencias notables que se tratarán en el artículo La vieja
"var".
Bloques de código
Si una variable se declara dentro de un bloque de código {...} , solo es visible dentro de ese
bloque.
Por ejemplo:
{
// hacer un trabajo con variables locales que no deberían verse fuera
let message = "Hello"; // solo visible en este bloque
alert(message); // Hello
}
Podemos usar esto para aislar un fragmento de código que realiza su propia tarea, con
variables que solo le pertenecen a él:
{
// ver mensaje
let message = "Hello";
alert(message);
}
{
// ver otro mensaje
let message = "Goodbye";
alert(message);
}
// ver mensaje
let message = "Hello";
alert(message);
Para if , for , while y otros, las variables declaradas dentro de {...} también son solo
visibles en su interior:
if (true) {
let phrase = "Hello!";
alert(phrase); // Hello!
}
Aquí, después de que if termine, la alerta a continuación no verá la phrase , de ahí el error.
Eso es genial, ya que nos permite crear variables locales de bloque, específicas de una
rama if .
Visualmente, let i está fuera de {...} ; pero la construcción for es especial aquí: la variable
declarada dentro de ella se considera parte del bloque.
Funciones anidadas
Una función se llama “anidada” cuando se crea dentro de otra función.
Es fácilmente posible hacer esto con JavaScript.
Podemos usarlo para organizar nuestro código:
Aquí la función anidada getFullName() se hace por conveniencia. Puede acceder a las variables
externas y, por lo tanto, puede devolver el nombre completo. Las funciones anidadas son
bastante comunes en JavaScript.
Lo que es mucho más interesante, es que puede devolverse una función anidada: ya sea como
propiedad de un nuevo objeto o como resultado en sí mismo. Luego se puede usar en otro lugar.
No importa dónde, todavía tiene acceso a las mismas variables externas.
A continuación, makeCounter crea la función “contador” que devuelve el siguiente número en cada
invocación:
function makeCounter() {
let count = 0;
return function() {
return count++;
};
}
alert( counter() ); // 0
alert( counter() ); // 1
alert( counter() ); // 2
A pesar de ser simples, variantes ligeramente modificadas de ese código tienen usos
prácticos, como por ejemplo un generador de números aleatorios para pruebas automatizadas.
¿Cómo funciona esto? Si creamos múltiples contadores, ¿serán independientes? ¿Qué está
pasando con las variables aquí?
Entender tales cosas es excelente para el conocimiento general de JavaScript y beneficioso
para escenarios más complejos. Así que vamos a profundizar un poco.
Paso 1. Variables
En JavaScript, todas las funciones en ejecución, el bloque de código {...} y el script en su
conjunto tienen un objeto interno (oculto) asociado, conocido como Alcance léxico.
El objeto del alcance léxico consta de dos partes:
1. Registro de entorno: es un objeto que almacena en sus propiedades todas las variables
locales (y alguna otra información, como el valor de this ).
Una “variable” es solo una propiedad del objeto interno especial, el Registro de entorno . “Obtener
o cambiar una variable” significa “obtener o cambiar una propiedad de ese objeto”.
En este código simple y sin funciones, solo hay un entorno léxico:
Los rectángulos en el lado derecho demuestran cómo cambia el entorno léxico global durante la
ejecución:
1. Cuando se inicia el script, el entorno léxico se rellena previamente con todas las
variables declaradas. – Inicialmente, están en el estado “No inicializado”. Ese es un
estado interno especial, significa que el motor conoce la variable, pero no se puede hacer
referencia a ella hasta que se haya declarado con let . Es casi lo mismo que si la variable
no existiera.
2. Luego aparece la definición let phrase .Todavía no hay una asignación, por lo que su valor
es undefined . Podemos usar la variable desde este punto en adelante.
Trabajar con variables es realmente trabajar con las propiedades de ese objeto.
Los motores de JavaScript también pueden optimizarlo, descartar variables que no se utilizan
para ahorrar memoria y realizar otros trucos internos, siempre que el comportamiento visible
permanezca como se describe.
Naturalmente, este comportamiento solo se aplica a las declaraciones de funciones , no a las expresiones
de funciones , donde asignamos una función a una variable, como let say = function (name) ....
Por ejemplo, para say(" John ") , se ve así (la ejecución está en la línea etiquetada con una
flecha):
function makeCounter() {
let count = 0;
Al comienzo de cada llamada a makeCounter() , se crea un nuevo objeto de entorno léxico para
almacenar variables para la ejecución makeCounter .
Entonces tenemos dos entornos léxicos anidados, como en el ejemplo anterior:
Lo que es diferente es que, durante la ejecución de makeCounter() , se crea una pequeña función
anidada de solo una línea: return count++ . Aunque no la ejecutamos, solo la creamos.
Todas las funciones recuerdan el entorno léxico en el que fueron realizadas. Técnicamente, no
hay magia aquí: todas las funciones tienen la propiedad oculta llamada [[Environment] , que
mantiene la referencia al entorno léxico donde se creó la función:
Entonces, counter.[[Environment]] tiene la referencia al Entorno léxico de {count: 0} . Así es como la
función recuerda dónde se creó, sin importar dónde se la llame. La referencia [[Environment]] se
establece una vez y para siempre en el momento de creación de la función.
Luego, cuando counter() es llamado, un nuevo Entorno Léxico es creado por la llamada, y su
referencia externa del entorno léxico se toma de counter.[[Environment]] :
Ahora cuando el código dentro de counter() busca la variable count , primero busca su propio
entorno léxico (vacío, ya que no hay variables locales allí), luego el entorno léxico del
exterior llama a makeCounter() , donde lo encuentra y lo cambia.
Closure (clausura)
Existe un término general de programación “closure” que los desarrolladores generalmente
deben conocer.
Una clausura es una función que recuerda sus variables externas y puede acceder a ellas. En
algunos lenguajes, eso no es posible, o una función debe escribirse de una manera especial
para que suceda. Pero como se explicó anteriormente, en JavaScript todas las funciones son
clausuras naturales (solo hay una excepción, que se cubrirá en La sintaxis "new Function").
Es decir: recuerdan automáticamente dónde se crearon utilizando una propiedad
oculta [[Environment]] , y luego su código puede acceder a las variables externas.
Cuando en una entrevista un desarrollador frontend recibe una pregunta sobre “¿qué es una
clausura?”, una respuesta válida sería una definición de clausura y una explicación de que
todas las funciones en JavaScript son clausuras, y tal vez algunas palabras más sobre
detalles técnicos: la propiedad [[Environment]] y cómo funcionan los entornos léxicos.
Recolector de basura
Por lo general, un entorno léxico se elimina de la memoria con todas las variables una vez
que finaliza la llamada a la función. Eso es porque ya no hay referencias a él. Como
cualquier objeto de JavaScript, solo se mantiene en la memoria mientras es accesible.
Sin embargo, si hay una función anidada a la que todavía se puede llegar después del final de
una función, entonces tiene la propiedad [[Environment]] que hace referencia al entorno léxico.
En ese caso, el entorno léxico aún es accesible incluso después de completar la función, por
lo que permanece vigente.
Por ejemplo:
function f() {
let value = 123;
return function() {
alert(value);
}
}
Tenga en cuenta que si se llama a f() muchas veces y se guardan las funciones resultantes,
todos los objetos del entorno léxico correspondientes también se conservarán en la memoria.
Veamos las 3 funciones en el siguiente ejemplo:
function f() {
let value = Math.random();
Un objeto de entorno léxico muere cuando se vuelve inalcanzable (como cualquier otro objeto).
En otras palabras, existe solo mientras haya al menos una función anidada que haga referencia
a ella.
En el siguiente código, después de eliminar la función anidada, su entorno léxico adjunto (y
por lo tanto el value ) se limpia de la memoria:
function f() {
let value = 123;
return function() {
alert(value);
}
}
function f() {
let value = Math.random();
function g() {
debugger; // en console: type alert(value); ¡No hay tal variable!
}
return g;
}
let g = f();
g();
Como puede ver, ¡no existe tal variable! En teoría, debería ser accesible, pero el motor lo
optimizó.
Eso puede conducir a problemas de depuración divertidos (si no son muy largos). Uno de ellos:
podemos ver una variable externa con el mismo nombre en lugar de la esperada:
function f() {
let value = "the closest value";
function g() {
debugger; // en la consola escriba: alert(value); Surprise!
}
return g;
}
Es bueno conocer esta característica de V8. Si está depurando con Chrome/Edge/Opera, tarde o
temprano la encontrará.
Eso no es un error en el depurador, sino más bien una característica especial de V8. Tal vez
en algún momento la cambiarán. Siempre puede verificarlo ejecutando los ejemplos en esta
página.
La vieja "var"
1. let
2. const
3. var
La declaración var es similar a let . Casi siempre podemos reemplazar let por var o viceversa
y esperar que las cosas funcionen:
Pero internamente var es una bestia diferente, originaria de muy viejas épocas. Generalmente
no se usa en código moderno, pero aún habita en el antiguo.
Si no planeas encontrarte con tal código bien puedes saltar este capítulo o posponerlo, pero
hay posibilidades de que esta bestia pueda morderte más tarde.
Por otro lado, es importante entender las diferencias cuando se migra antiguo código
de var a let para evitar extraños errores.
if (true) {
var test = true; // uso de "var" en lugar de "let"
}
Como var ignora los bloques de código, tenemos una variable global test .
Si usáramos let test en vez de var test , la variable sería visible solamente dentro del if :
if (true) {
let test = true; // uso de "let"
}
Lo mismo para los bucles: var no puede ser local en los bloques ni en los bucles:
alert(i); // 10, "i" es visible después del bucle, es una variable global
alert(one); // 1, "one" es visible después del bucle, es una variable global
Si un bloque de código está dentro de una función, var se vuelve una variable a nivel de
función:
function sayHi() {
if (true) {
var phrase = "Hello";
}
alert(phrase); // funciona
}
sayHi();
alert(phrase); // ReferenceError: phrase no está definida
Como podemos ver, var atraviesa if , for u otros bloques. Esto es porque mucho tiempo atrás
los bloques en JavaScript no tenían ambientes léxicos. Y var es un remanente de aquello.
let user;
let user; // SyntaxError: 'user' ya fue declarado
Con var podemos redeclarar una variable muchas veces. Si usamos var con una variable ya
declarada, simplemente se ignora:
var user = "John"; // este "var" no hace nada (ya estaba declarado)
// ...no dispara ningún error
alert(user); // John
Entonces el código:
function sayHi() {
phrase = "Hello";
alert(phrase);
var phrase;}
sayHi();
…es técnicamente lo mismo que esto (se movió var phrase hacia arriba):
phrase = "Hello";
alert(phrase);
}
sayHi();
function sayHi() {
phrase = "Hello"; // (*)
if (false) {
var phrase;
}alert(phrase);
}
sayHi();
Este comportamiento también se llama “hoisting” (elevamiento), porque todos los var son
“hoisted” (elevados) hacia el tope de la función.
Entonces, en el ejemplo anterior, la rama if (false) nunca se ejecuta, pero eso no tiene
importancia. El var dentro es procesado al iniciar la función, entonces al momento de (*) la
variable existe.
function sayHi() {
alert(phrase);
sayHi();
1. La declaración var
2. La asignación = .
function sayHi() {
var phrase; // la declaración se hace en el inicio...alert(phrase); // undefined
sayHi();
Como todas las declaraciones var son procesadas al inicio de la función, podemos
referenciarlas en cualquier lugar. Pero las variables serán indefinidas hasta que alcancen su
asignación.
En ambos ejemplos de arriba alert se ejecuta sin un error, porque la variable phrase existe.
Pero su valor no fue asignado aún, entonces muestra undefined .
IIFE
Como en el pasado solo existía var , y no había visibilidad a nivel de bloque, los
programadores inventaron una manera de emularla. Lo que hicieron fue el llamado "expresiones
de función inmediatamente invocadas (abreviado IIFE en inglés).
Un IIFE se ve así:
(function() {
alert(message); // Hello
})();
alert(message); // Hello
}();
Entonces, los paréntesis alrededor de la función es un truco para mostrarle a JavaScript que
la función es creada en el contexto de otra expresión, y de allí lo de “expresión de
función”, que no necesita un nombre y puede ser llamada inmediatamente.
Existen otras maneras además de los paréntesis para decirle a JavaScript que queremos una
expresión de función:
(function() {
alert("Paréntesis alrededor de la función");
})();
(function() {
alert("Paréntesis alrededor de todo");
}());
!function() {
alert("Operador 'Bitwise NOT' como comienzo de la expresión");
}();
+function() {
alert("'más unario' como comienzo de la expresión");
}();
Resumen
Hay dos diferencias principales entre var y let/const :
2. Las declaraciones var son procesadas al inicio de la función (o del script para las
globales) .
Hay otra diferencia menor relacionada al objeto global que cubriremos en el siguiente
capítulo.
Estas diferencias casi siempre hacen a var peor que let . Las variables a nivel de bloque son
mejores. Es por ello que let fue presentado en el estándar mucho tiempo atrás, y es ahora la
forma principal (junto con const ) de declarar una variable.
Objeto Global
El objeto global proporciona variables y funciones que están disponibles en cualquier lugar.
Por defecto, aquellas que están integradas en el lenguaje o el entorno.
En un navegador se denomina window , para Node.js es global , para otros entornos puede tener otro
nombre.
Recientemente, se agregó globalThis al lenguaje, como un nombre estandarizado para un objeto
global, que debería ser compatible con todos los entornos al igual que con los principales
navegadores.
Aquí usaremos window , suponiendo que nuestro entorno sea un navegador. Si su script puede
ejecutarse en otros entornos, es mejor usar globalThis en su lugar.
Se puede acceder directamente a todas las propiedades del objeto global:
alert("Hello");
// es lo mismo que
window.alert("Hello");
En un navegador, las funciones y variables globales declaradas con var (¡no con let/const !) se
convierten en propiedades del objeto global:
var gVar = 5;
¡Por favor no te fíes de eso! Este comportamiento existe por razones de compatibilidad. Los
scripts modernos hacen uso de Módulos Javascript para que tales cosas no sucedan.
Si usáramos let en su lugar, esto no sucedería:
let gLet = 5;
Si un valor es tan importante que desea que esté disponible globalmente, escríbalo
directamente como una propiedad:
// Hacer que la información actual del usuario sea global, para que todos los scripts puedan acceder a ella
window.currentUser = {
name: "John"
};// en otro lugar en el código
alert(currentUser.name); // John
if (!window.Promise) {
alert("Your browser is really old!");
}
if (!window.Promise) {
window.Promise = ... // implementación personalizada del lenguaje moderno
}
Resumen
El objeto global contiene variables que deberían estar disponibles en todas partes.
Eso incluye JavaScript incorporado, tales como Array y valores específicos del entorno, o
como window.innerHeight : la altura de la ventana en el navegador.
Deberíamos almacenar valores en el objeto global solo si son verdaderamente globales para
nuestro proyecto. Y manteniendo su uso al mínimo.
Para que nuestro código esté preparado para el futuro y sea más fácil de entender, debemos
acceder a las propiedades del objeto global directamente, como window.x .
Una buena manera de imaginar funciones es como “objetos de acción” invocables. No solo
podemos llamarlos, sino también tratarlos como objetos: agregar/eliminar propiedades, pasar
por referencia, etc.
La propiedad “name”
Las funciones como objeto contienen algunas propiedades utilizables.
function sayHi() {
alert("Hi");
alert(sayHi.name); // sayHi
f();
let user = {
sayHi() {
// ...
},
sayBye: function() {
// ...
}
alert(user.sayHi.name); // sayHi
alert(user.sayBye.name); // sayBye
Sin embargo, no hay magia. Hay casos en que no hay forma de encontrar el nombre correcto. En
ese caso, la propiedad “name” está vacía, como aquí:
La propiedad “length”
Hay una nueva propiedad “length” incorporada que devuelve el número de parámetros de una
función, por ejemplo:
function f1(a) {}
function f2(a, b) {}
function many(a, b, ...more) {}
alert(f1.length); // 1
alert(f2.length); // 2
alert(many.length); // 2
Una función de cero argumentos, que solo se llama cuando el usuario da una respuesta
positiva.
Una función con argumentos, que se llama en cualquier caso y devuelve una respuesta.
La idea es que tenemos una sintaxis de controlador simple y sin argumentos para casos
positivos (la variante más frecuente), pero también podemos admitir controladores
universales:
Propiedades personalizadas
También podemos agregar nuestras propias propiedades.
Aquí agregamos la propiedad counter para registrar el recuento total de llamadas:
function sayHi() {
alert("Hi");
sayHi(); // Hi
sayHi(); // Hi
Las propiedades de la función a veces pueden reemplazar las clausuras o closures. Por
ejemplo, podemos reescribir el ejemplo de la función de contador del capítulo Ámbito de
function makeCounter() {
// en vez de:
// let count = 0
function counter() {
return counter.count++;
};
counter.count = 0;
return counter;
}
function makeCounter() {
function counter() {
return counter.count++;
};
counter.count = 0;
return counter;
}
counter.count = 10;
alert( counter() ); // 10
Y agrégale un nombre:
¿Logramos algo aquí? ¿Cuál es el propósito de ese nombre adicional de "func" ?
Primero, tengamos en cuenta que todavía tenemos una Expresión de Función. Agregar el
nombre "func" después de function no lo convirtió en una Declaración de Función, porque todavía
se crea como parte de una expresión de asignación.
Agregar ese nombre tampoco rompió nada.
La función todavía está disponible como sayHi() :
Hay dos cosas especiales sobre el nombre func , que le hacen útil:
Por ejemplo, la función sayHi a continuación se vuelve a llamar a sí misma con "Guest" si no se
proporciona who :
¿Por qué usamos func ? ¿Quizás solo usa sayHi para la llamada anidada?
En realidad, en la mayoría de los casos podemos:
El problema con ese código es que sayHi puede cambiar en el código externo. Si la función se
asigna a otra variable, el código comenzará a dar errores:
Eso sucede porque la función toma sayHi de su entorno léxico externo. No hay sayHi local, por
lo que se utiliza la variable externa. Y en el momento de la llamada, ese sayHi externo
es nulo .
El nombre opcional que podemos poner en la Expresión de función está destinado a resolver
exactamente este tipo de problemas.
Usémoslo para arreglar nuestro código:
Ahora funciona, porque el nombre "func" es una función local. No se toma desde el exterior (y
no es visible allí). La especificación garantiza que siempre hará referencia a la función
actual.
El código externo todavía tiene su variable sayHi o welcome . Y func es el “nombre de función
interna” con el que la función puede llamarse a sí misma de manera confiable.
No existe tal cosa para la Declaración de funciones
La característica “nombre interno” descrita aquí solo está disponible para Expresiones de
funciones, no para Declaraciones de funciones. Para las declaraciones de funciones, no hay
sintaxis para agregar un nombre “interno”.
A veces necesitamos un nombre interno confiable, este es un motivo para reescribir una
Declaración de función en una Expresión de función con nombre.
Resumen
Las funciones son objetos.
Aquí cubrimos sus propiedades:
pero si no hay ninguno, JavaScript intenta adivinarlo por el contexto (por ejemplo, una
asignación).
cuentan.
Si la función se declara como una Expresión de función (no en el flujo de código principal),
y lleva el nombre, se llama Expresión de Función con Nombre (Named Function Expression). El
nombre se puede usar dentro para hacer referencia a sí mismo, para llamadas recursivas o
similares.
Además, las funciones pueden tener propiedades adicionales. Muchas bibliotecas de JavaScript
conocidas hacen un gran uso de esta función.
Crean una función “principal” y le asignan muchas otras funciones “auxiliares”. Por ejemplo,
la biblioteca jQuery crea una función llamada $ . La biblioteca lodash crea una función _ , y
luego agrega _.clone , _.keyBy y otras propiedades (mira los docs cuando quieras aprender más
sobre ello). En realidad, lo hacen para disminuir su contaminación del espacio global, de
modo que una sola biblioteca proporciona solo una variable global. Eso reduce la posibilidad
de conflictos de nombres.
Por lo tanto, una función puede hacer un trabajo útil por sí misma y también puede tener
muchas otras funcionalidades en las propiedades.
Hay una forma más de crear una función. Raramente se usa, pero a veces no hay alternativa.
Sintaxis
La sintaxis para crear una función:
La función se crea con los argumentos arg1 ... argN y el cuerpo functionBody dado.
Es más fácil entender viendo un ejemplo: Aquí tenemos una función con dos argumentos:
alert(sumar(1, 2)); // 3
diHola(); // Hola
La mayor diferencia sobre las otras maneras de crear funciones que hemos visto, es que la
función se crea desde un string y es pasada en tiempo de ejecución.
Las declaraciones anteriores nos obliga a nosotros, los programadores, a escribir el código
de la función en el script.
Pero new Function nos permite convertir cualquier string en una función. Por ejemplo, podemos
recibir una nueva función desde el servidor y ejecutarlo.
Se utilizan en casos muy específicos, como cuando recibimos código de un servidor, o compilar
dinámicamente una función a partir de una plantilla. La necesidad surge en etapas avanzadas
de desarrollo.
Closure
Normalmente, una función recuerda dónde nació en una propiedad especial llamada [[Environment]] ,
que hace referencia al entorno léxico desde dónde se creó.
Pero cuando una función es creada usando new Function , su [[Environment]] no hace referencia al
entorno léxico actual, sino al global.
Entonces, tal función no tiene acceso a las variables externas, solo a las globales.
function getFunc() {
let valor = "test";
function getFunc() {
let valor = "test";
Esta característica especial de new Function parece extraña, pero resulta muy útil en la
práctica.
Imagina que debemos crear una función a partir de una string. El código de dicha función no
se conoce al momento de escribir el script (es por eso que no usamos funciones regulares),
sino que se conocerá en el proceso de ejecución. Podemos recibirlo del servidor o de otra
fuente.
Pero si new Function pudiera acceder a las variables externas, no podría encontrar la
variable userName renombrada.
Si new Function tuviera acceso a variables externas, tendríamos problemas con los minificadores
Además, tal código sería una mala arquitectura y propensa a errores.
Para pasar algo a una función creada como new Function , debemos usar sus argumentos.
Resumen
La sintaxis:
Por razones históricas, los argumentos también pueden ser pasados como una lista separada por
comas.
Las funciones creadas con new Function , tienen [[Environment]] haciendo referencia a ambiente léxico
global, no al externo. En consecuencia no pueden usar variables externas. Pero eso es en
realidad algo bueno, porque nos previene de errores. Pasar parámetros explícitamente es mucho
mejor arquitectónicamente y no causa problemas con los minificadores.
Podemos decidir ejecutar una función no ahora, sino un determinado tiempo después. Eso se
llama “planificar una llamada”.
Hay dos métodos para ello:
setTimeout nos permite ejecutar una función una vez, pasado un intervalo de tiempo dado.
setInterval nos permite ejecutar una función repetidamente, comenzando después del intervalo
setTimeout
La sintaxis:
Parámetros:
Por razones históricas es posible pasar una cadena de código, pero no es recomendable. retraso El
retraso o delay antes de la ejecución, en milisegundos (1000 ms = 1 segundo), por defecto
0. arg1 , arg2 …Argumentos para la función
function sayHi() {
alert('Hola');
}
setTimeout(sayHi, 1000);
Con argumentos:
setTimeout("alert('Hola')", 1000);
// ¡mal!
setTimeout(sayHi(), 1000);
Eso no funciona, porque setTimeout espera una referencia a una función. Y aquí sayHi() ejecuta la
función, y el resultado de su ejecución se pasa a setTimeout . En nuestro caso, el resultado
de sayHi() es undefined (la función no devuelve nada), por lo que no habrá nada planificado.
clearTimeout(timerId);
alert(timerId); // mismo identificador (No se vuelve nulo después de cancelar)
setInterval
El método setInterval tiene la misma sintaxis que setTimeout :
Todos los argumentos tienen el mismo significado. Pero a diferencia de setTimeout , ejecuta la
función no solo una vez, sino regularmente después del intervalo de tiempo dado.
Para detener las llamadas, debemos llamar a ‘clearInterval (timerId)’.
El siguiente ejemplo mostrará el mensaje cada 2 segundos. Después de 5 segundos, la salida se
detiene:
setTimeout anidado
Hay dos formas de ejecutar algo regularmente.
Uno es setInterval . El otro es un setTimeout anidado, como este:
El setTimeout anterior planifica la siguiente llamada justo al final de la actual (*) .
El setTimeout anidado es un método más flexible que setInterval . De esta manera, la próxima
llamada se puede planificar de manera diferente, dependiendo de los resultados de la actual.
Ejemplo: necesitamos escribir un servicio que envíe una solicitud al servidor cada 5 segundos
solicitando datos, pero en caso de que el servidor esté sobrecargado, deber aumentar el
intervalo a 10, 20, 40 segundos…
Aquí está el pseudocódigo:
}, delay);
Y si las funciones que estamos planificando requieren mucha CPU, entonces podemos medir el
tiempo que tarda la ejecución y planificar la próxima llamada más tarde o más temprano.
setTimeout anidado permite establecer el retraso entre las ejecuciones con mayor precisión
que setInterval .
Comparemos dos fragmentos de código. El primero usa setInterval :
let i = 1;
setInterval(function() {
func(i++);
}, 100);
let i = 1;
setTimeout(function run() {
func(i++);
setTimeout(run, 100);
}, 100);
Eso es normal, porque el tiempo que tarda la ejecución de func “consume” una parte del
intervalo.
Es posible que la ejecución de func sea más larga de lo esperado y demore más de 100 ms.
En este caso, el motor espera a que se complete func , luego verifica el planificador y, si se
acabó el tiempo, lo ejecuta de nuevo inmediatamente.
En caso límite, si la ejecución de la función siempre demora más que los ms de retraso ,
entonces las llamadas se realizarán sin pausa alguna.
Y aquí está la imagen para el setTimeout anidado:
Esto planifica la ejecución de func lo antes posible. Pero el planificador lo invocará solo
después de que se complete el script que se está ejecutando actualmente.
Por lo tanto, la función está planificada para ejecutarse “justo después” del script actual.
Por ejemplo, esto genera “Hola”, e inmediatamente después “Mundo”:
alert("Hola");
setTimeout(function run() {
if (start + 100 < Date.now()) alert(times); // mostrar los retrasos después de 100 ms
else setTimeout(run); // de lo contrario replanificar
});
// Un ejemplo de la salida:
// 1,1,1,1,9,15,20,24,30,35,40,45,50,55,59,64,70,75,80,85,90,95,100
Para JavaScript del lado del servidor, esa limitación no existe, y existen otras formas de
planificar un trabajo asincrónico inmediato, como setImmediate para Node.js. Así que esta
nota es específica del navegador.
Resumen
Los métodos setTimeout(func, delay, ... args) y setInterval(func, delay, ... args) nos permiten
ejecutar func “una vez” y “regularmente” después del retardo delay dado en milisegundos.
Para cancelar la ejecución, debemos llamar a clearTimeout / clearInterval con el valor devuelto
por setTimeout / setInterval .
Las llamadas anidadas setTimeout son una alternativa más flexible a setInterval , lo que nos
permite establecer el tiempo entre ejecuciones con mayor precisión.
La programación de retardo cero con setTimeout(func, 0) (lo mismo que setTimeout(func) ) se usa para
programar la llamada “lo antes posible, pero después de que se complete el script actual”.
El navegador limita la demora mínima para cinco o más llamadas anidadas de setTimeout o
para setInterval (después de la quinta llamada) a 4 ms. Eso es por razones históricas.
Tenga en cuenta que todos los métodos de planificación no garantizan el retraso exacto.
Por ejemplo, el temporizador en el navegador puede ralentizarse por muchas razones:
Todo eso puede aumentar la resolución mínima del temporizador (el retraso mínimo) a 300 ms o
incluso 1000 ms dependiendo de la configuración de rendimiento del navegador y del nivel del
sistema operativo.
JavaScript ofrece una flexibilidad excepcional cuando se trata de funciones. Se pueden pasar,
usar como objetos, y ahora veremos cómo redirigir las llamadas entre ellas y decorarlas.
Caché transparente
Digamos que tenemos una función slow(x) , que es pesada para la CPU, pero cuyos resultados son
“estables”: es decir que con la misma x siempre devuelve el mismo resultado.
Si la función se llama con frecuencia, es posible que queramos almacenar en caché (recordar)
los resultados obtenidos para evitar perder tiempo en calcularlos de nuevo.
function slow(x) {
// puede haber un trabajo pesado de CPU aquí
alert(`Called with ${x}`);
return x;
}
function cachingDecorator(func) {
let cache = new Map();
return function(x) {
if (cache.has(x)) { // si hay tal propiedad en caché
return cache.get(x); // lee el resultado
}
slow = cachingDecorator(slow);
En el código anterior, cachingDecorator es un decorador: una función especial que toma otra
función y altera su comportamiento.
La idea es que podemos llamar a cachingDecorator para cualquier función, y devolver el contenedor
de almacenamiento en caché. Eso es genial, porque podemos tener muchas funciones que podrían
usar dicha función, y todo lo que tenemos que hacer es aplicarles ‘cachingDecorator’.
Al separar el caché del código de la función principal, también permite mantener el código
principal más simple.
El resultado de cachingDecorator(func) es un contenedor : function(x) que envuelve la llamada de func(x) en
la lógica de almacenamiento en caché:
Desde un código externo, la función slow envuelta sigue haciendo lo mismo. Simplemente se
agregó un aspecto de almacenamiento en caché a su comportamiento.
Para resumir, hay varios beneficios de usar un cachingDecorator separado en lugar de alterar el
código de slow en sí mismo:
slow(x) {
// una aterradora tarea muy pesada para la CPU
alert("Called with " + x);
return x * this.someMethod(); // (*)
}
};
El error ocurre en la línea (*) que intenta acceder a this.someMethod y falla. ¿Puedes ver por
qué?
La razón es que el contenedor llama a la función original como func(x) en la línea (**) . Y,
cuando se llama así, la función obtiene this = undefined .
Entonces, el contenedor pasa la llamada al método original, pero sin el contexto this . De ahí
el error.
Vamos a solucionar esto:
Ejecuta func proporcionando el primer argumento como this , y el siguiente como los argumentos.
En pocas palabras, estas dos llamadas hacen casi lo mismo:
Ambos llaman func con argumentos 1 , 2 y 3 . La única diferencia es que func.call también
establece this en obj .
Como ejemplo, en el siguiente código llamamos a sayHi en el contexto de diferentes
objetos: sayHi.call(user) ejecuta sayHi estableciendo this = user , y la siguiente línea
establece this = admin :
function sayHi() {
alert(this.name);
}
Y aquí usamos call para llamar a say con el contexto y la frase dados:
function say(phrase) {
alert(this.name + ': ' + phrase);
}
En nuestro caso, podemos usar call en el contenedor para pasar el contexto a la función
original:
let worker = {
someMethod() {
return 1;
},
slow(x) {
alert("Called with " + x);
return x * this.someMethod(); // (*)
}
};
function cachingDecorator(func) {
let cache = new Map();
return function(x) {
if (cache.has(x)) {
return cache.get(x);
}
let result = func.call(this, x); // "this" se pasa correctamente ahora
cache.set(x, result);
return result;
};
}
let worker = {
slow(min, max) {
return min + max; // una aterradora tarea muy pesada para la CPU
}
};
Anteriormente, para un solo argumento x podríamos simplemente usar cache.set(x, result) para
guardar el resultado y cache.get(x) para recuperarlo. Pero ahora necesitamos recordar el
resultado para una combinación de argumentos (min, max) . El Map nativo toma solo un valor como
clave.
Hay muchas posibles soluciones:
1. Implemente una nueva estructura de datos similar a un mapa (o use una de un tercero) que
sea más versátil y permita múltiples propiedades.
2. Use mapas anidados: cache.set(min) será un Map que almacena el par (max, result) . Así podemos
obtener result como cache.get(min).get(max) .
3. Una dos valores en uno. En nuestro caso particular, podemos usar un string "min,max" como la
propiedad de Map . Por flexibilidad, podemos permitir proporcionar un función hashing para
el decorador, que sabe hacer un valor de muchos.
let worker = {
slow(min, max) {
alert(`Called with ${min},${max}`);
return min + max;
}
};
cache.set(key, result);
return result;
};
}
function hash(args) {
return args[0] + ',' + args[1];
}
Ahora funciona con cualquier número de argumentos (aunque la función hash también necesitaría
ser ajustada para permitir cualquier número de argumentos. Una forma interesante de manejar
esto se tratará a continuación).
Hay dos cambios:
En la línea (*) llama a hash para crear una sola propiedad de arguments . Aquí usamos una
simple función de “unión” que convierte los argumentos (3, 5) en la propiedad "3,5" . Los
casos más complejos pueden requerir otras funciones hash.
Entonces (**) usa func.call(this, ...arguments) para pasar tanto el contexto como todos los
argumentos que obtuvo el contenedor (no solo el primero) a la función original.
func.apply
En vez de func.call(this, ...arguments) , podríamos usar func.apply(this, arguments) .
La sintaxis del método incorporado func.apply es:
func.apply(context, args)
Ejecuta la configuración func this = context y usa un objeto tipo array args como lista de
argumentos.
La única diferencia de sintaxis entre call y apply es que call espera una lista de argumentos,
mientras que apply lleva consigo un objeto tipo matriz.
Entonces estas dos llamadas son casi equivalentes:
func.call(context, ...args);
func.apply(context, args);
Estas hacen la misma llamada de func con el contexto y argumento dados.
La sintaxis con el operador “spread” ... – en call permite pasar una lista iterable args .
Para los objetos que son iterables y símil-array, como un array real, podemos usar cualquiera
de ellos, pero apply probablemente será más rápido porque la mayoría de los motores de
JavaScript lo optimizan mejor internamente.
Pasar todos los argumentos junto con el contexto a otra función se llama redirección de
llamadas.
Esta es la forma más simple:
Cuando un código externo llama a tal contenedor wrapper , no se puede distinguir de la llamada
de la función original func .
Préstamo de método
Ahora hagamos una pequeña mejora en la función de hash:
function hash(args) {
return args[0] + ',' + args[1];
A partir de ahora, funciona solo en dos argumentos. Sería mejor si pudiera adherir (glue)
cualquier número de args .
function hash(args) {
return args.join();
}
Por lo tanto, llamar a join en él fallará, como podemos ver a continuación:
function hash() {
alert( arguments.join() ); // Error: arguments.join is not a function}
hash(1, 2);
Aún así, hay una manera fácil de usar la unión (join) de arrays:
function hash() {
alert( [].join.call(arguments) ); // 1,2}
hash(1, 2);
1. Hacer que glue sea el primer argumento o, si no hay argumentos, entonces una coma "," .
7. Devolver result .
Entonces, técnicamente toma a this y le une this[0] , this[1] … etc. Está escrito intencionalmente
de una manera que permite cualquier tipo de array this (no es una coincidencia, muchos métodos
siguen esta práctica). Es por eso que también funciona con this = arguments
Resumen
El decorador es un contenedor alrededor de una función que altera su comportamiento. El
trabajo principal todavía lo realiza la función.
Los decoradores se pueden ver como “características” o “aspectos” que se pueden agregar a una
función. Podemos agregar uno o agregar muchos. ¡Y todo esto sin cambiar su código!
Hay muchos decoradores a tu alrededor. Verifica qué tan bien los entendiste resolviendo las
tareas de este capítulo.
Al pasar métodos de objeto como devoluciones de llamada, por ejemplo a setTimeout , se genera un
problema conocido: la "pérdida de this ".
En este capítulo veremos las formas de solucionarlo.
Pérdida de “this”
Ya hemos visto ejemplos de pérdida de this . Una vez que se pasa hacia algún lugar un método
separado de su objeto, this se pierde.
Así es como puede suceder con setTimeout :
let user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
Como podemos ver, el resultado no muestra “John” como this.firstName ¡sino undefined !
Esto se debe a que setTimeout tiene la función user.sayHi , separada del objeto. La última línea se
puede reescribir como:
El método setTimeout en el navegador es un poco especial: establece this = window para la llamada a
la función (para Node.js, this se convierte en el objeto temporizador (timer), pero realmente
no importa aquí). Entonces, en this.firstName intenta obtener window.firstName , que no existe. En
otros casos similares, this simplemente se vuelve undefined .
La tarea es bastante típica: queremos pasar un método de objeto a otro lugar (aquí, al
planificador) donde se llamará. ¿Cómo asegurarse de que se llamará en el contexto correcto?
let user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
setTimeout(function() {
user.sayHi(); // Hello, John!
}, 1000);
Ahora funciona, porque recibe a user del entorno léxico externo, y luego llama al método
normalmente.
Aquí hacemos lo mismo, pero de otra manera:
¿Qué pasa si antes de que se dispare setTimeout (¡hay un segundo retraso!) user cambia el valor?
Entonces, de repente, ¡llamará al objeto equivocado!
let user = {
firstName: "John",
sayHi() {
alert(`Hola, ${this.firstName}!`);
}
};
En otras palabras, llamar a boundFunc es como llamar a func pero con un this fijo.
Por ejemplo, aquí funcUser pasa una llamada a func con this = user :
let user = {
firstName: "John"
};
function func() {
alert(this.firstName);
}
Aquí func.bind(user) es como una “variante vinculada” de func , con this = user fijo en ella.
Todos los argumentos se pasan al func original “tal cual”, por ejemplo:
let user = {
firstName: "John"
};
function func(phrase) {
alert(phrase + ', ' + this.firstName);
}
let user = {
firstName: "John",
sayHi() {
alert(`Hello, ${this.firstName}!`);
}
};
En la línea (*) tomamos el método user.sayHi y lo vinculamos a user . sayHi es una función
“vinculada”. No importa si se llama sola o se pasa en setTimeout , el contexto será el correcto.
Aquí podemos ver que los argumentos se pasan “tal cual”, solo que this se fija mediante bind :
let user = {
firstName: "John",
say(phrase) {
alert(`${phrase}, ${this.firstName}!`);
}
};
Convenience method:bindAll
Funciones parciales
Hasta ahora solo hemos estado hablando de vincular this . Vamos un paso más allá.
Podemos vincular no solo this , sino también argumentos. Es algo que no suele hacerse, pero a
veces puede ser útil.
Sintaxis completa de bind :
function mul(a, b) {
return a * b;
}
Usemos bind para crear, en su base, una función double para duplicar:
function mul(a, b) {
return a * b;
}
La llamada a mul.bind(null, 2) crea una nueva función double que pasa las llamadas a mul ,
fijando null como contexto y 2 como primer argumento. Los demás argumentos se pasan “tal
cual”.
Esto se llama aplicación parcial: creamos una nueva función fijando algunos parámetros a la
existente.
Tenga en cuenta que aquí en realidad no usamos this . Pero bind lo requiere, por lo que debemos
poner algo como null .
function mul(a, b) {
return a * b;
}
Afortunadamente, se puede implementar fácilmente una función parcial para vincular solo
argumentos.
Como esto:
user.sayNow("Hello");
// Algo como:
// [10:00] John: Hello!
El resultado de la llamada parcial(func [, arg1, arg2 ...]) es un contenedor o “wrapper” (*) que llama
a func con:
Resumen
El método func.bind(context, ... args) devuelve una “variante vinculada” de la función func ; fijando
el contexto this y, si se proporcionan, fijando también los primeros argumentos.
Por lo general, aplicamos bind para fijar this a un método de objeto, de modo que podamos
pasarlo en otro lugar. Por ejemplo, en setTimeout .
Cuando fijamos algunos argumentos de una función existente, la función resultante (menos
universal) se llama aplicación parcial o parcial.
Los parciales son convenientes cuando no queremos repetir el mismo argumento una y otra vez.
Al igual que si tenemos una función send(from, to) , y from siempre debe ser igual para nuestra
tarea, entonces, podemos obtener un parcial y continuar la tarea con él.
arr.forEach(func) – func es ejecutado por forEach para cada elemento del array.
…y muchas más.
Está en el espíritu de JavaScript crear una función y pasarla a algún otro lugar.
Y en tales funciones, por lo general, no queremos abandonar el contexto actual. Ahí es donde
las funciones de flecha son útiles.
let group = {
title: "Our Group",
students: ["John", "Pete", "Alice"],
showList() {
this.students.forEach(
student => alert(this.title + ': ' + student)
);}
};
group.showList();
Aquí, en forEach se utiliza la función de flecha, por lo que this.title es exactamente igual que
en el método externo showList . Es decir: group.title .
Si usáramos una función “regular”, habría un error:
let group = {
title: "Our Group",
students: ["John", "Pete", "Alice"],
showList() {
this.students.forEach(function(student) {
// Error: Cannot read property 'title' of undefined
alert(this.title + ': ' + student);
});}
};
group.showList();
El error se produce porque forEach ejecuta funciones con this = undefined de forma predeterminada,
por lo que se intenta acceder a undefined.title .
Eso no afecta las funciones de flecha, porque simplemente no tienen this .
La flecha => no crea ningún enlace. La función simplemente no tiene this . La búsqueda de
‘this’ se realiza exactamente de la misma manera que una búsqueda de variable regular: en
el entorno léxico externo.
Eso es genial para los decoradores, cuando necesitamos reenviar una llamada
con this y arguments actuales.
Por ejemplo, defer (f, ms) obtiene una función y devuelve un contenedor que retrasa la llamada
en ms milisegundos:
function sayHi(who) {
alert('Hello, ' + who);
}
Aquí tuvimos que crear las variables adicionales args y ctx para que la función dentro
de setTimeout pudiera tomarlas.
Resumen
Funciones de flecha:
No tienen this
No tienen arguments
Esto se debe a que están diseñadas para piezas cortas de código que no tienen su propio
“contexto”, sino que funcionan en el actual. Y realmente brillan en ese caso de uso.
Indicadores de propiedad
writable – si es true , puede ser editado, de otra manera es de solo lectura.
enumerable – si es true , puede ser listado en bucles, de otro modo no puede serlo.
configurable – si es true , la propiedad puede ser borrada y estos atributos pueden ser
No los vimos hasta ahora porque generalmente no se muestran. Cuando creamos una propiedad “de
la forma usual”, todos ellos son true . Pero podemos cambiarlos en cualquier momento.
Primero, veamos como obtener estos indicadores.
El método Object.getOwnPropertyDescriptor permite consultar toda la información sobre una
propiedad.
La sintaxis es:
obj El objeto del que se quiere obtener la información. propertyName El nombre de la propiedad.
El valor devuelto es el objeto llamado “descriptor de propiedad”: este contiene el valor de
todos los indicadores.
Por ejemplo:
let user = {
name: "Juan"
};
obj , propertyName el objeto y la propiedad con los que se va a trabajar. descriptor descriptor de
propiedad a aplicar.
Si la propiedad existe, defineProperty actualiza sus indicadores. De otra forma, creará la
propiedad con el valor y el indicador dado; en ese caso, si el indicador no es proporcionado,
es asumido como false .
En el ejemplo a continuación, se crea una propiedad name con todos los indicadores en false :
Object.defineProperty(user, "name", {
value: "Juan"
});let descriptor = Object.getOwnPropertyDescriptor(user, 'name');
Comparado con la creada “de la forma usual” user.name : ahora todos los indicadores son false . Si
no es lo que queremos, es mejor que los establezcamos en true en el descriptor .
Ahora veamos los efectos de los indicadores con ejemplo.
Non-writable
Vamos a hacer user.name de solo lectura cambiando el indicador writable :
let user = {
name: "Juan"
};
Object.defineProperty(user, "name", {
writable: false});
Ahora nadie puede cambiar el nombre de nuestro usuario, a menos que le apliquen su
propio defineProperty para sobrescribir el nuestro.
Los errores aparecen solo en modo estricto
En el modo no estricto, no se producen errores al intentar escribir en propiedades no
grabables; pero la operación no tendrá éxito. Las acciones que infringen el indicador se
ignoran silenciosamente en el modo no estricto.
Aquí está el mismo ejemplo, pero la propiedad se crea desde cero:
let user = { };
Object.defineProperty(user, "name", {
value: "Pedro",
// para las nuevas propiedades se necesita listarlas explícitamente como true
enumerable: true,
configurable: true});
alert(user.name); // Pedro
user.name = "Alicia"; // Error
Non-enumerable
Ahora vamos a añadir un toString personalizado a user .
Normalmente, en los objetos un toString nativo es no enumerable, no se muestra en un
bucle for..in . Pero si añadimos nuestro propio toString , por defecto éste se muestra en los
bucles for..in :
let user = {
name: "Juan",
toString() {
return this.name;
}
};
let user = {
name: "Juan",
Object.defineProperty(user, "toString", {
enumerable: false});
alert(Object.keys(user)); // name
Non-configurable
El indicador “no-configurable” ( configurable:false ) a veces está preestablecido para los objetos y
propiedades nativos.
Una propiedad no configurable no puede ser eliminada, y sus atributos no pueden ser
modificados.
Por ejemplo, Math.PI es de solo lectura, no enumerable y no configurable:
Aquí user.name es “non-configurable”, pero aún puede cambiarse (por ser “writable”):
let user = {
name: "John"
};
Object.defineProperty(user, "name", {
configurable: false
});
let user = {
name: "John"
};
Object.defineProperty(user, "name", {
writable: false,
configurable: false
});
Podemos cambiar writable: true a false en una propiedad no configurable, impidiendo en más la
modificación de su valor (sumando una capa de protección). Aunque no hay vuelta atrás.
Object.defineProperties
Existe un método Object.defineProperties(obj, descriptors) que permite definir varias
propiedades de una sola vez.
La sintaxis es:
Object.defineProperties(obj, {
prop1: descriptor1,
prop2: descriptor2
// ...
});
Por ejemplo:
Object.defineProperties(user, {
name: { value: "Juan", writable: false },
surname: { value: "Perez", writable: false },
// ...
});
Object.getOwnPropertyDescriptors
Para obtener todos los descriptores al mismo tiempo, podemos usar el
método Object.getOwnPropertyDescriptors(obj).
Junto con Object.defineProperties , puede ser usado como una forma “consciente de los indicadores” de
clonar un objeto:
Normalmente, cuando clonamos un objeto, usamos una asignación para copiar las propiedades:
Otra diferencia es que for..in ignora las propiedades simbólicas y las no enumerables,
pero Object.getOwnPropertyDescriptors devuelve todos los descriptores de propiedades incluyendo
simbólicas y no enumerables.
El primer tipo son las propiedades de datos. Ya sabemos cómo trabajar con ellas. Todas las
propiedades que hemos estado usando hasta ahora eran propiedades de datos.
El segundo tipo de propiedades es algo nuevo. Son las propiedades de acceso o accessors. Son,
en esencia, funciones que se ejecutan para obtener (“get”) y asignar (“set”) un valor, pero
que para un código externo se ven como propiedades normales.
Getters y setters
Las propiedades de acceso se construyen con métodos de obtención “getter” y asignación
“setter”. En un objeto literal se denotan con get y set :
let obj = {
get propName() {
// getter, el código ejecutado para obtener obj.propName
},
set propName(value) {
// setter, el código ejecutado para asignar obj.propName = value
}
};
let user = {
name: "John",
surname: "Smith"
};
Ahora queremos añadir una propiedad de “Nombre completo” ( fullName ), que debería ser "John Smith" .
Por supuesto, no queremos copiar-pegar la información existente, así que podemos aplicarla
como una propiedad de acceso:
get fullName() {
return `${this.name} ${this.surname}`;
}};
Desde fuera, una propiedad de acceso se parece a una normal. Esa es la idea de estas
propiedades. No llamamos a user.fullName como una función, la leemos normalmente: el “getter”
corre detrás de escena.
Hasta ahora, “Nombre completo” sólo tiene un receptor. Si intentamos asignar user.fullName= ,
habrá un error.
let user = {
get fullName() {
return `...`;
}
};
let user = {
name: "John",
surname: "Smith",
get fullName() {
return `${this.name} ${this.surname}`;
},
set fullName(value) {
[this.name, this.surname] = value.split(" ");
}};
alert(user.name); // Alice
alert(user.surname); // Cooper
Como resultado, tenemos una propiedad virtual fullName que puede leerse y escribirse.
Descriptores de acceso
Los descriptores de propiedades de acceso son diferentes de aquellos para las propiedades de
datos.
Para las propiedades de acceso, no hay cosas como value y writable , sino de “get” y “set”.
Así que un descriptor de accesos puede tener:
get – una función sin argumentos, que funciona cuando se lee una propiedad,
set – una función con un argumento, que se llama cuando se establece la propiedad,
Por ejemplo, para crear un acceso fullName con defineProperty , podemos pasar un descriptor
con get y set :
let user = {
name: "John",
surname: "Smith"
};
set(value) {
[this.name, this.surname] = value.split(" ");
}});
Tenga en cuenta que una propiedad puede ser un acceso (tiene métodos get/set ) o una propiedad
de datos (tiene un value ), no ambas.
Si intentamos poner ambos, get y value , en el mismo descriptor, habrá un error:
value: 2
});
let user = {
get name() {
return this._name;
},
set name(value) {
if (value.length < 4) {
alert("El nombre es demasiado corto, necesita al menos 4 caracteres");
return;
}
this._name = value;
}
};
user.name = "Pete";
alert(user.name); // Pete
alert( john.age ); // 25
…Pero tarde o temprano, las cosas pueden cambiar. En lugar de “edad” podemos decidir
almacenar “cumpleaños”, porque es más preciso y conveniente:
Ahora, ¿qué hacer con el viejo código que todavía usa la propiedad de la “edad”?
Podemos intentar encontrar todos esos lugares y arreglarlos, pero eso lleva tiempo y puede
ser difícil de hacer si ese código está escrito por otras personas. Y además, la “edad” es
algo bueno para tener en “usuario”, ¿verdad? En algunos lugares es justo lo que queremos.
Pues mantengámoslo.
Añadiendo un getter para la “edad” resuelve el problema:
Ahora el viejo código funciona también y tenemos una buena propiedad adicional.
Prototipos y herencia
Herencia prototípica
[[Prototype]]
En JavaScript, los objetos tienen una propiedad oculta especial [[Prototype]] (como se menciona
en la especificación); que puede ser null , o hacer referencia a otro objeto llamado
“prototipo”:
let animal = {
eats: true
};
let rabbit = {
jumps: true
};
let animal = {
eats: true
};
let rabbit = {
jumps: true
};
Aquí, la línea (*) establece que animal es el prototipo de rabbit .
Luego, cuando alert intenta leer la propiedad rabbit.eats (**) , no la encuentra en rabbit , por lo
que JavaScript sigue la referencia [[Prototype]] y la encuentra en animal (busca de abajo hacia
arriba):
Aquí podemos decir que " animal es el prototipo de rabbit " o que " rabbit hereda prototípicamente
de animal ".
Entonces, si animal tiene muchas propiedades y métodos útiles, estos estarán automáticamente
disponibles en rabbit . Dichas propiedades se denominan “heredadas”.
let animal = {
eats: true,
let rabbit = {
jumps: true,
__proto__: animal
};
let animal = {
eats: true,
walk() {
alert("Animal da un paseo");
}
};
let rabbit = {
jumps: true,
__proto__: animal};
let longEar = {
earLength: 10,
__proto__: rabbit};
Ahora, si leemos algo de longEar y falta, JavaScript lo buscará en rabbit , y luego en animal .
Solo hay dos limitaciones:
2. El valor de __proto__ puede ser un objeto o null . Otros tipos son ignorados.
La propiedad __proto__ es algo vetusta. Existe por razones históricas, el JavaScript moderno
sugiere el uso de las funciones Object.getPrototypeOf/Object.setPrototypeOf en lugar de get/set del
prototipo. Estudiaremos estas funciones más adelante.
Según la especificación, solamente los navegadores debería
n dar soporte a __proto__ . Pero de hecho todos los entornos, incluyendo los del lado del
servidor, soportan __proto__ , así que es bastante seguro usarlo.
Como la notación __proto__ es más intuitiva, la usaremos en los ejemplos.
let animal = {
eats: true,
walk() {
/* este método no será utilizado por rabbit */
}
};
let rabbit = {
__proto__: animal
};
rabbit.walk = function() {
alert("¡Conejo! ¡Salta, salta!");
};
Las propiedades de acceso son una excepción, ya que la asignación es manejada por una función
setter. Por lo tanto, escribir en una propiedad de este tipo es en realidad lo mismo que
llamar a una función.
Por esa razón, admin.fullName funciona correctamente en el siguiente código:
let user = {
name: "John",
surname: "Smith",
get fullName() {
return `${this.name} ${this.surname}`;
}
};
let admin = {
__proto__: user,
isAdmin: true
};
// ¡Dispara el setter!
admin.fullName = "Alice Cooper"; // (**)
Aquí, en la línea (*) , la propiedad admin.fullName tiene un getter en el prototipo user , entonces
es llamado. Y en la línea (**) , la propiedad tiene un setter en el prototipo, por lo que es
llamado.
El valor de “this”
Puede surgir una pregunta interesante en el ejemplo anterior: ¿cuál es el valor de this dentro
de set fullName(value) ? ¿Dónde están escritas las propiedades this.name y this.surname : en user o
en admin ?
Eso es realmente algo muy importante, porque podemos tener un gran objeto con muchos métodos
y tener objetos que hereden de él. Y cuando los objetos heredados ejecutan los métodos
heredados, modificarán solo sus propios estados, no el estado del gran objeto.
Por ejemplo, aquí animal representa un “método de almacenamiento”, y rabbit lo utiliza.
let rabbit = {
name: "Conejo Blanco",
__proto__: animal
};
// modifica rabbit.isSleeping
rabbit.sleep();
alert(rabbit.isSleeping); // Verdadero
alert(animal.isSleeping); // undefined (no existe tal propiedad en el prototipo)
La imagen resultante:
Como resultado, los métodos se comparten, pero el estado del objeto no.
Bucle for…in
El bucle for..in también itera sobre las propiedades heredadas.
Por ejemplo:
let animal = {
eats: true
};
let rabbit = {
jumps: true,
__proto__: animal
};
let animal = {
eats: true
};
let rabbit = {
jumps: true,
__proto__: animal
};
if (isOwn) {
alert(`Es nuestro: ${prop}`); // Es nuestro: jumps
} else {
alert(`Es heredado: ${prop}`); // Es heredado: eats
}
}
Aquí tenemos la siguiente cadena de herencia: rabbit hereda de animal , que hereda
de Object.prototype (porque animal es un objeto {...} literal, entonces es por defecto), y
luego null encima de él:
Casi todos los demás métodos de obtención de valores/claves, como Object.keys , Object.values , etc.,
ignoran las propiedades heredadas.
Solo operan en el objeto mismo. Las propiedades del prototipo no se tienen en cuenta.
Resumen
En JavaScript, todos los objetos tienen una propiedad oculta [[Prototype]] que es: otro
objeto, o null .
Podemos usar obj.__proto__ para acceder a ella (un getter/setter histórico, también hay otras
formas que se cubrirán pronto).
Si en obj queremos leer una propiedad o llamar a un método que no existen, entonces
JavaScript intenta encontrarlos en el prototipo.
Si llamamos a obj.method() , y method se toma del prototipo, this todavía hace referencia
a obj . Por lo tanto, los métodos siempre funcionan con el objeto actual, incluso si se
heredan.
El bucle for..in itera sobre las propiedades propias y heredadas. Todos los demás métodos de
obtención de valor/clave solo operan en el objeto mismo.
F.prototype
Recuerde: se pueden crear nuevos objetos con una función constructora, como new F() .
Si F.prototype es un objeto, entonces el operador new lo usa para establecerlo
como [[Prototype]] en el nuevo objeto.
let animal = {
eats: true
};
function Rabbit(name) {
this.name = name;
}
En la imagen, "prototype" es una flecha horizontal, que significa una propiedad regular,
y [[Prototype]] es vertical, que significa la herencia de rabbit desde animal .
F.prototype solo se usa en el momento new F
La propiedad F.prototype solo se usa cuando se llama a new F : asigna [[Prototype]] del nuevo objeto.
Si, después de la creación, la propiedad F.prototype cambia ( F.prototype = <otro objeto> ), los nuevos
objetos creados por new F tendrán otro objeto como [[Prototype]] , pero los objetos ya existentes
conservarán el antiguo.
Como esto:
function Rabbit() {}
/* prototipo predeterminado
Rabbit.prototype = { constructor: Rabbit };
*/
function Rabbit() {}
// por defecto:
// Rabbit.prototype = { constructor: Rabbit }
Naturalmente, si no hacemos nada, la propiedad constructor está disponible para todos los
rabbits a través de [[Prototype]] :
function Rabbit() {}
// por defecto:
// Rabbit.prototype = { constructor: Rabbit }
Podemos usar la propiedad constructor para crear un nuevo objeto usando el constructor ya
existente.
Como aqui:
function Rabbit(name) {
this.name = name;
alert(name);
}
Eso es útil cuando tenemos un objeto, no sabemos qué constructor se usó para él (por ejemplo,
proviene de una biblioteca de terceros), y necesitamos crear otro del mismo tipo.
Pero probablemente lo más importante sobre "constructor" es que …
…JavaScript en sí mismo no garantiza el valor correcto de "constructor" .
Sí, existe en el "prototipo" predeterminado para las funciones, pero eso es todo. Lo que sucede
con eso más tarde, depende totalmente de nosotros.
En particular, si reemplazamos el prototipo predeterminado como un todo, entonces no
habrá "constructor" en él.
Por ejemplo:
Entonces, para mantener el "constructor" correcto, podemos elegir agregar/eliminar propiedades
al "prototipo" predeterminado en lugar de sobrescribirlo como un todo:
function Rabbit() {}
Rabbit.prototype = {
jumps: true,
constructor: Rabbit};
Resumen
En este capítulo describimos brevemente la forma de establecer un [[Prototype]] para los objetos
creados a través de una función de constructor. Más adelante veremos patrones de programación
más avanzados que dependen de él.
Todo es bastante simple, solo algunas notas para aclarar las cosas:
La propiedad F.prototype (no la confunda con [[Prototype]] ) establece [[Prototype]] de objetos
nuevos cuando se llama a new F() .
El valor de F.prototype debe ser: un objeto, o null . Otros valores no funcionarán.
La propiedad "prototype" solo tiene este efecto especial cuando se establece en una función
de constructor y se invoca con new .
let user = {
name: "John",
prototype: "Bla-bla" // sin magia en absoluto
};
Por defecto, todas las funciones tienen F.prototype = {constructor: F} , por lo que podemos obtener el
constructor de un objeto accediendo a su propiedad "constructor" .
Prototipos nativos
La propiedad "prototype" es ampliamente utilizada por el núcleo de JavaScript mismo. Todas las
funciones de constructor integradas lo usan.
Primero veremos los detalles, y luego cómo usarlo para agregar nuevas capacidades a los
objetos integrados.
Object.prototype
Digamos que tenemos un objeto vacío y lo mostramos:
¿Dónde está el código que genera la cadena "[objetc Objetc]" ? Ese es un método integrado toString ,
pero ¿dónde está? ¡El obj está vacío!
…Pero la notación corta obj = {} es la misma que obj = new Object() , donde Object es una función de
constructor de objeto integrado, con su propio prototype que hace referencia a un objeto enorme
con toString y otros métodos
Esto es lo que está pasando:
Cuando se llama a new Object() (o se crea un objeto literal {...} ), el [[Prototype]] se establece
en Object.prototype de acuerdo con la regla que discutimos en el capitulo anterior:
Tenga en cuenta que no hay más [[Prototype]] en la cadena sobre Object.prototype :
alert(Object.prototype.__proto__); // null
// se hereda de Array.prototype?
alert( arr.__proto__ === Array.prototype ); // verdadero
Como hemos visto antes, Object.prototype también tiene toString , pero Array.prototype está más cerca de
la cadena, por lo que se utiliza la variante de array.
Otros objetos integrados también funcionan de la misma manera. Incluso las funciones: son
objetos de un constructor Function integrado, y sus métodos ( call / apply y otros) se toman
de Function.prototype . Las funciones también tienen su propio toString .
function f() {}
Primitivos
Lo más intrincado sucede con cadenas, números y booleanos.
Como recordamos, no son objetos. Pero si tratamos de acceder a sus propiedades, se crean los
objetos contenedores temporales utilizando los constructores
integrados String , Number y Boolean , estos proporcionan los métodos y luego desaparecen.
Estos objetos se crean de manera invisible para nosotros y la mayoría de los motores los
optimizan, pero la especificación lo describe exactamente de esta manera. Los métodos de
estos objetos también residen en prototipos, disponibles
como String.prototype , Number.prototype y Boolean.prototype .
Los valores null y undefined no tienen objetos contenedores
Los valores especiales null y undefined se distinguen. No tienen objetos contenedores, por lo
que los métodos y propiedades no están disponibles para ellos. Y tampoco tienen los
prototipos correspondientes.
String.prototype.show = function() {
alert(this);
};
"BOOM!".show(); // BOOM!
Durante el proceso de desarrollo, podemos tener ideas para nuevos métodos integrados que nos
gustaría tener, y podemos sentir la tentación de agregarlos a los prototipos nativos. Pero
eso es generalmente una mala idea.
Importante:
Los prototipos son globales, por lo que es fácil generar un conflicto. Si dos bibliotecas
agregan un método String.prototype.show , entonces una de ellas sobrescribirá el método de la otra.
Por lo tanto, en general, modificar un prototipo nativo se considera una mala idea.
En la programación moderna, solo hay un caso en el que se aprueba la modificación de
prototipos nativos: haciendo un polyfill.
Cuando un método existe en la especificación de JavaScript, pero aún no está soportado por un
motor de JavaScript en particular, podemos hacer “polyfill” (polirrelleno); esto es, crear un
método sustituto.
Luego podemos implementarlo manualmente y completar el prototipo integrado con él.
Por ejemplo:
String.prototype.repeat = function(n) {
// repite la cadena n veces
Préstamo de prototipos
En el capítulo Decoradores y redirecciones, call/apply hablamos sobre el préstamo de método .
Es cuando tomamos un método de un objeto y lo copiamos en otro.
P. ej…
let obj = {
0: "Hola",
1: "mundo!",
length: 2,
};
Funciona porque el algoritmo interno del método integrado join solo se preocupa por los
índices correctos y la propiedad length . No comprueba si el objeto es realmente un arreglo.
Otra posibilidad es heredar estableciendo obj.__proto__ en Array.prototype , de modo que todos los
métodos Array estén disponibles automáticamente en obj .
Pero eso es imposible si obj ya hereda de otro objeto. Recuerde, solo podemos heredar de un
objeto a la vez.
Los métodos de préstamo son flexibles, permiten mezclar funcionalidades de diferentes objetos
si es necesario.
Resumen
Todos los objetos integrados siguen el mismo patrón:
Los prototipos integrados se pueden modificar o completar con nuevos métodos. Pero no se
recomienda cambiarlos. El único caso permitido es probablemente cuando agregamos un nuevo
estándar que aún no es soportado por el motor de JavaScript.
En el primer capítulo de esta sección mencionamos que existen métodos modernos para
configurar un prototipo.
Leer y escribir en __proto__ se considera desactualizado y algo obsoleto (fue movido al llamado
“Anexo B” del estándar JavaScript, dedicado únicamente a navegadores).
Los métodos modernos para obtener y establecer (get/set) un prototipo son:
El único uso de __proto__ que no está mal visto, es como una propiedad cuando se crea un nuevo
objeto: { __proto__: ... } .
Por ejemplo:
let animal = {
eats: true
};
El método Object.create es más potente, tiene un segundo argumento opcional: descriptores de
propiedad.
Podemos proporcionar propiedades adicionales al nuevo objeto allí, así:
let animal = {
eats: true
};
alert(rabbit.jumps); // true
Esta llamada hace una copia verdaderamente exacta de obj , que incluye todas las propiedades:
enumerables y no enumerables, propiedades de datos y setters/getters, todo, y con
el [[Prototype]] correcto.
Breve historia
Hay muchas formas de administrar [[Prototype]] . ¿Cómo pasó esto? ¿Por qué?
Más tarde, en el año 2012, apareció Object.create en el estándar. Este le dio la capacidad de
crear objetos con un prototipo dado, pero no proporcionaba la capacidad de obtenerlo ni
establecerlo. Algunos navegadores implementaron el accessor __proto__ fuera del estándar, lo
que permitía obtener/establecer un prototipo en cualquier momento, dando más flexibilidad
al desarrollador.
Más tarde, en el año 2015, Object.setPrototypeOf y Object.getPrototypeOf se agregaron al estándar para
realizar la misma funcionalidad que __proto__ daba. Como __proto__ se implementó de facto en
todas partes, fue considerado obsoleto pero logró hacerse camino al Anexo B de la norma,
es decir: opcional para entornos que no son del navegador.
Más tarde, en el año 2022, fue oficialmente permitido el uso de __proto__ en objetos
literales {...} (y movido fuera del Anexo B), pero no como getter/setter obj.__proto__ (sigue
en el Anexo B).
Esa es una pregunta interesante, que requiere que comprendamos por qué __proto__ es malo.
Y pronto llegaremos a la respuesta.
No cambie [[Prototype]] en objetos existentes si la velocidad es importante
Mira el ejemplo:
Eso no debería sorprendernos. La propiedad __proto__ es especial: debe ser un objeto o null . Una
cadena no puede convertirse en un prototipo. Es por ello que la asignación de un string
a __proto__ es ignorada.
Pero no intentamos implementar tal comportamiento, ¿verdad? Queremos almacenar pares
clave/valor, y la clave llamada "__proto__" no se guardó correctamente. Entonces, ¡eso es un
error!
Aquí las consecuencias no son terribles. Pero en otros casos podemos estar asignando objetos
en lugar de strings, y el prototipo efectivamente ser cambiado. Como resultado, la ejecución
irá mal de maneras totalmente inesperadas.
… pero la sintaxis con ‘Objeto’ es a menudo más atractiva, por ser más consisa.
Afortunadamente podemos usar objetos, porque los creadores del lenguaje pensaron en ese
problema hace mucho tiempo.
Como sabemos, __proto__ no es una propiedad de un objeto, sino una propiedad de acceso
de Object.prototype :
Como se dijo al comienzo de esta sección del tutorial: __proto__ es una forma de acceder
a [[Prototype]] , no es [[Prototype]] en sí.
Ahora, si pretendemos usar un objeto como una arreglo asociativa y no tener tales problemas,
podemos hacerlo con un pequeño truco:
Entonces, no hay getter/setter heredado para __proto__ . Ahora se procesa como una propiedad de
datos normal, por lo que el ejemplo anterior funciona correctamente.
Podemos llamar a estos objetos: objetos “muy simples” o “de diccionario puro”, porque son aún
más simples que el objeto simple normal {...} .
Una desventaja es que dichos objetos carecen de los métodos nativos que los objetos
integrados sí tienen, p.ej. toString :
Resumen
Para crear un objeto con un prototipo dado, use:
El Object.create brinda una forma fácil de hacer la copia superficial de un objeto con todos
sus descriptores:
Object.setPrototypeOf(obj, proto) – establece el [[Prototype]] de obj en proto (igual que
el setter de __proto__ ).
También hemos cubierto objetos sin prototipo, creados con Object.create(null) o {__proto__: null} .
Estos objetos son usados como diccionarios, para almacenar cualquier (posiblemente
generadas por el usuario) clave.
Clases
Sintaxis básica de `class`
En la práctica a menudo necesitamos crear muchos objetos del mismo tipo: usuarios, bienes, lo
que sea.
Como ya sabemos del capítulo Constructor, operador "new", new function puede ayudar con eso.
Pero en JavaScript moderno hay un constructor más avanzado, “class”, que introduce
características nuevas muy útiles para la programación orientada a objetos.
La sintaxis “class”
La sintaxis básica es:
class MyClass {
// métodos de clase
constructor() { ... }
method1() { ... }
method2() { ... }
method3() { ... }
...
}
Entonces usamos new MyClass() para crear un objeto nuevo con todos los métodos listados.
El método constructor() es llamado automáticamente por new , así podemos inicializar el objeto
allí.
Por ejemplo:
class User {
constructor(name) {
this.name = name;
}
sayHi() {
alert(this.name);
}
// Uso:
let user = new User("John");
user.sayHi();
La notación aquí no debe ser confundida con la sintaxis de objeto literal. Dentro de la clase
no se requieren comas.
Desvelemos la magia y veamos lo que realmente es una clase. Ayudará a entender muchos
aspectos complejos.
En JavaScript, una clase es un tipo de función.
Veamos:
class User {
constructor(name) { this.name = name; }
sayHi() { alert(this.name); }
}
1. Crea una función llamada User , la que se vuelve el resultado de la declaración de la clase.
El código de la función es tomado del método constructor (se asume vacío si no se escribe
tal método).
Después de que el objeto new User es creado, cuando llamamos a sus métodos estos son tomados
del prototipo, tal como se describe en el capítulo F.prototype. Así el objeto tiene acceso a
métodos de clase.
Podemos ilustrar el resultado de la declaración de class User como:
class User {
constructor(name) { this.name = name; }
sayHi() { alert(this.name); }
}
// Uso:
let user = new User("John");
user.sayHi();
El resultado de esta definición es el mismo. Así, efectivamente hay razones para que class sea
considerada azúcar sintáctica para definir un constructor junto con sus métodos de prototipo.
Aún así hay diferencias importantes.
1. Primero, una función creada por class es etiquetada por una propiedad interna
especial [[IsClassConstructor]]:true . Entones no es exactamente lo mismo que crearla manualmente.
El lenguaje verifica esa propiedad en varios lugares. Por ejemplo, a diferencia de las
funciones regulares, esta debe ser llamada con new :
class User {
constructor() {}
}
class User {
constructor() {}
}
3. Las clases siempre asumen use strict . Todo el código dentro del constructor de clase está
automáticamente en modo estricto.
Además la sintaxis de class brinda muchas otras características que exploraremos luego.
Expresión de clases
Al igual que las expresiones de función, las expresiones de clase pueden tener un nombre.
Si una expresión de clase tiene un nombre, este es visible solamente dentro de la clase.
function makeClass(phrase) {
// declara una clase y la devuelve
return class {
sayHi() {
alert(phrase);
}
};
}
Getters/setters
Al igual que los objetos literales, las clases pueden incluir getters/setters, propiedades
calculadas, etc.
class User {
constructor(name) {
// invoca el setter
this.name = name;
}
class User {
new User().sayHi();
class User {
name = "John";sayHi() {
alert(`Hello, ${this.name}!`);
}
}
La diferencia importante de las propiedades definidas como “campos de clase” es que estas son
establecidas en los objetos individuales, no compartidas en User.prototype :
class User {
name = "John";}
También podemos asignar valores usando expresiones más complejas y llamados a función:
class User {
name = prompt("Name, please?", "John");}
click() {
alert(this.value);
}
}
class Button {
constructor(value) {
this.value = value;
}
click = () => {
alert(this.value);
}}
Un campo de clase click = () => {...} es creado para cada objeto. Hay una función para cada
objeto Button , con this dentro referenciando ese objeto. Podemos pasar button.click a cualquier
lado y el valor de this siempre será el correcto.
Esto es especialmente práctico, en el ambiente de los navegadores, para los “event
listeners”.
Resumen
La sintaxis básica de clase se ve así:
class MyClass {
prop = value; // propiedad
constructor(...) { // constructor
// ...
}
method(...) {} // método
MyClass es técnicamente una función (la que proveemos como constructor ), mientras que los
class Animal {
constructor(name) {
this.speed = 0;
this.name = name;
}
run(speed) {
this.speed = speed;
alert(`${this.name} corre a una velocidad de ${this.speed}.`);
}
stop() {
this.speed = 0;
alert(`${this.name} se queda quieto.`);
}
}
Los objetos de la clase Rabbit tienen acceso a los métodos de Rabbit , como rabbit.hide() , y
también a los métodos Animal , como rabbit.run() .
Internamente, la palabra clave extends funciona con la buena mecánica de prototipo:
establece Rabbit.prototype.[[Prototype]] a Animal.prototype . Entonces, si no se encuentra un método
en Rabbit.prototype , JavaScript lo toma de Animal.prototype .
2. Su prototipo, que es Rabbit.prototype : tiene el método hide , pero no el método run .
3. Su prototipo, que es Animal.prototype (debido a extends ): Este finalmente tiene el método run .
Como podemos recordar del capítulo Prototipos nativos, JavaScript usa la misma herencia
prototípica para los objetos incorporados. Por ejemplo, Date.prototype.[[Prototype]] es Object.prototype .
Es por esto que “Date” tiene acceso a métodos de objeto genéricos.
Cualquier expresión está permitida después de extends
La sintaxis de clase permite especificar no solo una clase, sino cualquier expresión después
de extends .
Por ejemplo, una llamada a función que genera la clase padre:
function f(phrase) {
return class {
sayHi() { alert(phrase); }
};
}
Sobrescribir un método
Ahora avancemos y sobrescribamos un método. Por defecto, todos los métodos que no están
especificados en la clase Rabbit se toman directamente “tal cual” de la clase Animal .
Pero Si especificamos nuestro propio método stop() en Rabbit , es el que se utilizará en su
lugar:
Por ejemplo, hagamos que nuestro conejo se oculte automáticamente cuando se detenga:
class Animal {
constructor(name) {
this.speed = 0;
this.name = name;
}
run(speed) {
this.speed = speed;
alert(`${this.name} corre a una velocidad de ${this.speed}.`);
}
stop() {
this.speed = 0;
alert(`${this.name} se queda quieto.`);
}
stop() {
super.stop(); // llama el stop padre
this.hide(); // y luego hide
}}
Ahora Rabbit tiene el método stop que llama al padre super.stop() en el proceso.
El método super en la función de flecha es el mismo que en stop() , y funciona según lo
previsto. Si aquí especificáramos una función “regular”, habría un error:
// super inesperado
setTimeout(function() { super.stop() }, 1000);
Sobrescribir un constructor
Con los constructores se pone un poco complicado.
Hasta ahora, Rabbit no tenía su propio constructor .
Como podemos ver, básicamente llama al constructor padre pasándole todos los argumentos. Esto
sucede si no escribimos un constructor propio.
Ahora agreguemos un constructor personalizado a Rabbit . Especificará earLength además de name :
class Animal {
constructor(name) {
this.speed = 0;
this.name = name;
}
// ...
}
constructor(name, earLength) {
this.speed = 0;
this.name = name;
this.earLength = earLength;
}// ...
}
// No funciona!
let rabbit = new Rabbit("Conejo Blanco", 10); // Error: this no está definido.
¡Vaya! Tenemos un error. Ahora no podemos crear conejos. ¿Qué salió mal?
La respuesta corta es:
Los constructores en las clases heredadas deben llamar a super(...) , y (¡!) hacerlo antes de
usar this .
…¿Pero por qué? ¿Qué está pasando aquí? De hecho, el requisito parece extraño.
Por supuesto, hay una explicación. Vamos a entrar en detalles, para que realmente entiendas
lo que está pasando.
En JavaScript, hay una distinción entre una función constructora de una clase heredera
(llamada “constructor derivado”) y otras funciones. Un constructor derivado tiene una
propiedad interna especial [[ConstructorKind]]:"derived" . Esa es una etiqueta interna especial.
Esa etiqueta afecta su comportamiento con new .
Cuando una función regular se ejecuta con new , crea un objeto vacío y lo asigna a this .
Pero cuando se ejecuta un constructor derivado, no hace esto. Espera que el constructor
padre haga este trabajo.
Entonces un constructor derivado debe llamar a super para ejecutar su constructor padre
(base), de lo contrario no se creará el objeto para this . Y obtendremos un error.
Para que el constructor Rabbit funcione, necesita llamar a super() antes de usar this , como
aquí:
class Animal {
constructor(name) {
this.speed = 0;
this.name = name;
}
// ...
}
// ...
}
class Animal {
name = 'animal';
constructor() {
alert(this.name); // (*)
}
}
Aquí, la clase Rabbit extiende Animal y sobrescribe el campo name con un valor propio.
Rabbit no tiene su propio constructor, entonces es llamado el de Animal .
Lo interesante es que en ambos casos: new Animal() y new Rabbit() , el alert en la
línea (*) muestra animal .
En otras palabras, el constructor padre siempre usa el valor de su propio campo de clase, no
el sobrescrito.
¿Qué es lo extraño de esto?
Si esto aún no está claro, comparáralo con lo que ocurre con los métodos.
Aquí está el mismo código, pero en lugar del campo this.name llamamos el método this.showName() :
class Animal {
showName() { // en vez de this.name = 'animal'
alert('animal');
}
constructor() {
this.showName(); // en vez de alert(this.name);
}
}
En nuestro caso, Rabbit es la clase derivada. No hay constructor() en ella. Como establecimos
previamente, es lo mismo que si hubiera un constructor vacío con solamente super(...args) .
Entonces, new Rabbit() llama a super() y se ejecuta el constructor padre, y (por la regla de la
clase derivada) solamente después de que sus campos de clase sean inicializados. En el
momento de la ejecución del constructor padre, todavía no existen los campos de clase
de Rabbit , por ello los campos de Animal son los usados.
Esta sutil diferencia entre campos y métodos es particular de JavaScript
Afortunadamente este comportamiento solo se revela si los campos sobrescritos son usados en
el constructor padre. En tal caso puede ser difícil entender qué es lo que está pasando, por
ello lo explicamos aquí.
Si esto se vuelve un problema, uno puede corregirlo usando métodos o getters/setters en lugar
de campos.
Vamos a profundizar un poco más el tema de super . Veremos algunas cosas interesantes en el
camino.
En primer lugar, de todo lo que hemos aprendido hasta ahora, ¡es imposible que super funcione
en absoluto!
let animal = {
name: "Animal",
eat() {
alert(`${this.name} come.`);
}
};
En la línea (*) tomamos eat del prototipo ( animal ) y lo llamamos en el contexto del objeto
actual. Tenga en cuenta que .call(this) es importante aquí, porque un simple this.__ proto
__.eat() ejecutaría al padre eat en el contexto del prototipo, no del objeto actual.
Ahora agreguemos un objeto más a la cadena. Veremos cómo se rompen las cosas:
let animal = {
name: "Animal",
eat() {
alert(`${this.name} come.`);
}
};
let rabbit = {
__proto__: animal,
eat() {
// ...rebota al estilo de conejo y llama al método padre (animal)
this.__proto__.eat.call(this); // (*)
}
};
let longEar = {
__proto__: rabbit,
eat() {
// ...haz algo con orejas largas y llama al método padre (rabbit)
this.__proto__.eat.call(this); // (**)
}
};
2. Luego, en la línea (*) de rabbit.eat , queremos pasar la llamada aún más arriba en la cadena;
pero como this=longEar , entonces this.__ proto__.eat ¡es nuevamente rabbit.eat !
3. …Entonces rabbit.eat se llama a sí mismo en el bucle sin fin, porque no puede ascender más.
[[HomeObject]]
Para proporcionar la solución, JavaScript agrega una propiedad interna especial para las
funciones: [[HomeObject]] .
Cuando una función se especifica como un método de clase u objeto, su propiedad [[HomeObject]] se
convierte en ese objeto.
Entonces super lo usa para resolver el problema del prototipo padre y sus métodos.
let animal = {
name: "Animal",
eat() { // animal.eat.[[HomeObject]] == animal
alert(`${this.name} come.`);
}
};
let rabbit = {
__proto__: animal,
name: "Conejo",
eat() { // rabbit.eat.[[HomeObject]] == rabbit
super.eat();
}
};
let longEar = {
__proto__: rabbit,
name: "Oreja Larga",
eat() { // longEar.eat.[[HomeObject]] == longEar
super.eat();
}
};
// funciona correctamente
longEar.eat(); // Oreja Larga come.
Funciona según lo previsto, debido a la mecánica de [[HomeObject]] . Un método, como longEar.eat ,
conoce su [[HomeObject]] y toma el método padre de su prototipo. Sin el uso de this .
let plant = {
sayHi() {
alert("Soy una planta");
}
};
En la línea (*) , el método tree.sayHi se copió de rabbit . ¿Quizás solo queríamos evitar la
duplicación de código?
Su [[HomeObject]] es rabbit , ya que fue creado en rabbit . No hay forma de cambiar [[HomeObject]] .
El código de tree.sayHi() tiene dentro a super.sayHi() . Sube desde ‘rabbit’ y toma el método de
‘animal’.
let animal = {
eat: function() { // escrito así intencionalmente en lugar de eat() {...
// ...
}
};
let rabbit = {
__proto__: animal,
eat: function() {
Resumen
1. Para extender una clase: class Hijo extends Padre : – Eso significa
que Hijo.prototype.__proto__ será Padre.prototype , por lo que los métodos se heredan.
3. Al sobrescribir otro método: – Podemos usar super.method() en un método Hijo para llamar al
método Padre .
También:
Las funciones de flecha no tienen su propio this o super , por lo que se ajustan de manera
transparente al contexto circundante.
class User {
static staticMethod() {alert(this === User);
}
}
User.staticMethod(); // verdadero
Eso realmente hace lo mismo que asignarlo como una propiedad directamente:
class User { }
User.staticMethod = function() {
alert(this === User);
};
User.staticMethod(); // verdadero
El valor de this en la llamada User.staticMethod() es el mismo constructor de clase User (la regla
“objeto antes de punto”).
Por lo general, los métodos estáticos se utilizan para implementar funciones que pertenecen a
la clase como un todo, no a un objeto particular de la misma.
Por ejemplo, tenemos objetos Article y necesitamos una función para compararlos.
class Article {
constructor(title, date) {
this.title = title;
this.date = date;
}
Aquí el método Article.compare se encuentra “encima” de los artículos, como un medio para
compararlos. No es el método de un artículo sino de toda la clase.
Otro ejemplo sería un método llamado “factory”.
Digamos que necesitamos múltiples formas de crear un artículo:
La primera forma puede ser implementada por el constructor. Y para la segunda podemos hacer
un método estático de la clase.
class Article {
constructor(title, date) {
this.title = title;
this.date = date;
}
static createTodays() {
// recuerda, this = Article
return new this("Resumen de hoy", new Date());
}}
Ahora, cada vez que necesitamos crear un resumen de hoy, podemos llamar a Article.createTodays() .
Una vez más, ese no es el método de un objeto artículo, sino el método de toda la clase.
Los métodos estáticos también se utilizan en clases relacionadas con base de datos para
buscar/guardar/eliminar entradas de la misma, como esta:
Los métodos estáticos son llamados sobre las clases, no sobre los objetos individuales.
Por ejemplo, este código no funcionará:
// ...
article.createTodays(); /// Error: article.createTodays is not a function
Propiedades estáticas
Una adición reciente
Esta es una adición reciente al lenguaje. Los ejemplos funcionan en el Chrome reciente.
Las propiedades estáticas también son posibles, se ven como propiedades de clase regular,
pero precedidas por static :
class Animal {
static planet = "Tierra";
constructor(name, speed) {
this.speed = speed;
this.name = name;
}
run(speed = 0) {
this.speed += speed;
alert(`${this.name} corre a una velocidad de ${this.speed}.`);
}
// Hereda de Animal
class Rabbit extends Animal {
hide() {
alert(`${this.name} se esconde!`);
}
}
let rabbits = [
new Rabbit("Conejo Blanco", 10),
new Rabbit("Conejo Negro", 5)
];
rabbits.sort(Rabbit.compare);
alert(Rabbit.planet); // Tierra
Como resultado, la herencia funciona tanto para métodos regulares como estáticos.
Verifiquemos eso por código, aquí:
class Animal {}
class Rabbit extends Animal {}
// para la estática
alert(Rabbit.__proto__ === Animal); // verdadero
Resumen
Los métodos estáticos se utilizan en la funcionalidad propia de la clase “en su conjunto”. No
se relaciona con una instancia de clase concreta.
Por ejemplo, un método para comparar Article.compare (article1, article2) o un método de
fábrica Article.createTodays() .
class MyClass {
static property = ...;
static method() {
...
}
}
MyClass.property = ...
MyClass.method = ...
Para class B extends A el prototipo de la clase B en sí mismo apunta a A : B.[[Prototipo]] = A .
Entonces, si no se encuentra un campo en B , la búsqueda continúa en A .
Esa es una práctica “imprescindible” en el desarrollo de algo más complejo que una aplicación
“hola mundo”.
Para entender esto, alejémonos del desarrollo y volvamos nuestros ojos al mundo real…
Por lo general, los dispositivos que estamos usando son bastante complejos. Pero delimitar la
interfaz interna de la externa permite usarlas sin problemas.
Pero para ocultar detalles internos, no utilizaremos una cubierta protectora, sino una
sintaxis especial del lenguaje y las convenciones.
Interfaz interna – métodos y propiedades, accesibles desde otros métodos de la clase, pero
no desde el exterior.
Si continuamos la analogía con la máquina de café, lo que está oculto en su interior: un tubo
de caldera, un elemento calefactor, etc., es su interfaz interna.
Se utiliza una interfaz interna para que el objeto funcione, sus detalles se utilizan entre
sí. Por ejemplo, un tubo de caldera está unido al elemento calefactor.
Pero desde afuera, una máquina de café está cerrada por la cubierta protectora, para que
nadie pueda alcanzarlos. Los detalles están ocultos e inaccesibles. Podemos usar sus
funciones a través de la interfaz externa.
Entonces, todo lo que necesitamos para usar un objeto es conocer su interfaz externa. Es
posible que no seamos completamente conscientes de cómo funciona dentro, y eso es genial.
Público: accesible desde cualquier lugar. Comprenden la interfaz externa. Hasta ahora solo
estábamos usando propiedades y métodos públicos.
Privado: accesible solo desde dentro de la clase. Estos son para la interfaz interna.
Proteger “waterAmount”
Hagamos primero una clase de cafetera simple:
class CoffeeMachine {
waterAmount = 0; // la cantidad de agua adentro
constructor(power) {
this.power = power;
alert( `Se creó una máquina de café, poder: ${power}` );
}
// agregar agua
coffeeMachine.waterAmount = 200;
Cambiemos la propiedad waterAmount a protegida para tener más control sobre ella. Por ejemplo,
no queremos que nadie lo ponga por debajo de cero.
Las propiedades protegidas generalmente tienen el prefijo de subrayado _ .
Eso no se aplica a nivel de lenguaje, pero existe una convención bien conocida entre los
programadores de que no se debe acceder a tales propiedades y métodos desde el exterior.
Entonces nuestra propiedad se llamará _waterAmount :
class CoffeeMachine {
_waterAmount = 0;
set waterAmount(value) {
if (value < 0) {
value = 0;
}
this._waterAmount = value;
}
get waterAmount() {
return this._waterAmount;
}
constructor(power) {
this._power = power;
}
// agregar agua
coffeeMachine.waterAmount = -10; // _waterAmount se vuelve 0, no -10
class CoffeeMachine {
// ...
constructor(power) {
this._power = power;
}
get power() {
return this._power;
}
Funciones getter/setter
Aquí usamos la sintaxis getter/setter.
Pero la mayoría de las veces las funciones get.../set... son preferidas, como esta:
class CoffeeMachine {
_waterAmount = 0;
setWaterAmount(value) {
if (value < 0) value = 0;
this._waterAmount = value;
}
getWaterAmount() {
return this._waterAmount;
}
}
new CoffeeMachine().setWaterAmount(100);
Eso parece un poco más largo, pero las funciones son más flexibles. Pueden aceptar múltiples
argumentos (incluso si no los necesitamos en este momento).
Por otro lado, la sintaxis get/set es más corta, por lo que, en última instancia, no existe
una regla estricta, depende de usted decidir.
Los campos protegidos son heredados.
Si heredamos class MegaMachine extends CoffeeMachine , entonces nada nos impide acceder
a this._waterAmount o this._power desde los métodos de la nueva clase.
Por lo tanto, los campos protegidos son naturalmente heredables. A diferencia de los privados
que veremos a continuación.
“#waterLimit” Privada
Una adición reciente
Esta es una adición reciente al lenguaje. No es compatible con motores de JavaScript, o es
compatible parcialmente todavía, requiere polyfilling.
Los privados deberían comenzar con # . Solo son accesibles desde dentro de la clase.
Por ejemplo, aquí hay una propiedad privada #waterLimit y el método privado de control de
agua #fixWaterAmount :
class CoffeeMachine {
#waterLimit = 200; #fixWaterAmount(value) {
if (value < 0) return 0;
if (value > this.#waterLimit) return this.#waterLimit;
}setWaterAmount(value) {
this.#waterLimit = this.#fixWaterAmount(value);
}
A nivel de lenguaje, # es una señal especial de que el campo es privado. No podemos acceder
desde fuera o desde clases heredadas.
Los campos privados no entran en conflicto con los públicos. Podemos tener campos
privados #waterAmount y públicos waterAmount al mismo tiempo.
Por ejemplo, hagamos que waterAmount sea un accesorio para #waterAmount :
class CoffeeMachine {
#waterAmount = 0;
get waterAmount() {
return this.#waterAmount;
}
set waterAmount(value) {
if (value < 0) value = 0;
this.#waterAmount = value;
}
}
machine.waterAmount = 100;
alert(machine.#waterAmount); // Error
A diferencia de los protegidos, los campos privados son aplicados por el propio lenguaje. Eso
es bueno.
Pero si heredamos de CoffeeMachine , entonces no tendremos acceso directo a #waterAmount . Tendremos
que confiar en el getter/setter de waterAmount :
Con campos privados eso es imposible: this['#name'] no funciona. Esa es una limitación de
sintaxis para garantizar la privacidad.
Resumen
En términos de POO, la delimitación de la interfaz interna de la externa se
llama encapsulamiento.
Ofrece los siguientes beneficios:
Protección para los usuarios, para que no se disparen en el pieImagínese, hay un equipo de
desarrolladores que usan una máquina de café. Fue hecho por la compañía “Best CoffeeMachine”
y funciona bien, pero se quitó una cubierta protectora. Entonces la interfaz interna está
expuesta.
Todos los desarrolladores son civilizados: usan la máquina de café según lo previsto. Pero
uno de ellos, John, decidió que él era el más inteligente e hizo algunos ajustes en el
interior de la máquina de café. Entonces la máquina de café falló dos días después.
Seguramente no es culpa de John, sino de la persona que quitó la cubierta protectora y dejó
que John hiciera sus manipulaciones.
Lo mismo en programación. Si un usuario de una clase cambiará cosas que no están destinadas a
ser cambiadas desde el exterior, las consecuencias son impredecibles.SoportableLa situación
en la programación es más compleja que con una máquina de café de la vida real, porque no
solo la compramos una vez. El código se somete constantemente a desarrollo y mejora.
Si delimitamos estrictamente la interfaz interna, el desarrollador de la clase puede cambiar
libremente sus propiedades y métodos internos, incluso sin informar a los usuarios.
Si usted es un desarrollador de tal clase, es bueno saber que los métodos privados se pueden
renombrar de forma segura, sus parámetros se pueden cambiar e incluso eliminar, porque ningún
código externo depende de ellos.
Para los usuarios, cuando sale una nueva versión, puede ser una revisión total internamente,
pero aún así es simple de actualizar si la interfaz externa es la misma.Ocultando
complejidadLa gente adora usar cosas que son simples. Al menos desde afuera. Lo que hay
dentro es algo diferente.
Los programadores no son una excepción.
Siempre es conveniente cuando los detalles de implementación están ocultos, y hay disponible
una interfaz externa simple y bien documentada.
Para ocultar una interfaz interna utilizamos propiedades protegidas o privadas:
Los campos protegidos comienzan con _ . Esa es una convención bien conocida, no aplicada a
nivel de lenguaje. Los programadores solo deben acceder a un campo que comience con _ de
su clase y las clases que hereden de él.
Los campos privados comienzan con # . JavaScript se asegura de que solo podamos acceder a
los que están dentro de la clase.
En este momento, los campos privados no son compatibles entre los navegadores, pero se puede
usar “polyfill”.
Las clases integradas como Array, Map y otras también son extensibles.
Por ejemplo, aquí PowerArray hereda del Array nativo:
Tenga en cuenta una cosa muy interesante. Métodos nativos como filter , map , y otros, devuelven
los nuevos objetos exactamente del tipo heredado PowerArray . Su implementación interna utiliza
la propiedad constructor del objeto para eso.
En el ejemplo anterior,
Como puede ver, ahora .filter devuelve un Array . Por lo tanto, la funcionalidad extendida ya no
se pasa.
Otras colecciones también trabajan del mismo modo
Otras colecciones, como Map y Set , funcionan igual. También usan Symbol.species .
Como puede ver, no hay un vínculo entre Date y Object . Son independientes,
solo Date.prototype hereda de Object.prototype .
Esa es una diferencia importante de herencia entre los objetos integrados en comparación con
lo que obtenemos con 'extends`.
El operador instanceof permite verificar si un objeto pertenece a una clase determinada. También
tiene en cuenta la herencia.
Tal verificación puede ser necesaria en muchos casos. Aquí lo usaremos para construir una
función polimórfica, la que trata los argumentos de manera diferente dependiendo de su tipo.
El operador instanceof
La sintaxis es:
Devuelve true si obj pertenece a la Class o una clase que hereda de ella.
Por ejemplo:
class Rabbit {}
let rabbit = new Rabbit();
// en lugar de clase
function Rabbit() {}alert( new Rabbit() instanceof Rabbit ); // verdadero
Tenga en cuenta que arr también pertenece a la clase Object . Esto se debe a que Array hereda
prototípicamente de Object .
Normalmente, instanceof examina la cadena de prototipos para la verificación. También podemos
establecer una lógica personalizada en el método estático Symbol.hasInstance .
El algoritmo de obj instanceof Class funciona más o menos de la siguiente manera:
class Animal {
static [Symbol.hasInstance](obj) {
if (obj.canEat) return true;
}
}
class Animal {}
class Rabbit extends Animal {}
Aquí está la ilustración de lo que rabbit instanceof Animal compara con Animal.prototype :
Es divertido, ¡pero el constructor Class en sí mismo no participa en el chequeo! Solo importa
la cadena de prototipos y Class.prototype .
Eso puede llevar a consecuencias interesantes cuando se cambia una propiedad prototype después
de crear el objeto.
Como aquí:
function Rabbit() {}
let rabbit = new Rabbit();
// cambió el prototipo
Rabbit.prototype = {};
// ...ya no es un conejo!
alert( rabbit instanceof Rabbit ); // falso
Esa es su implementación de toString . Pero hay una característica oculta que hace
que toString sea mucho más poderoso que eso. Podemos usarlo como un typeof extendido y una
alternativa para instanceof .
¿Suena extraño? En efecto. Vamos a desmitificar.
Por esta especificación, el toString incorporado puede extraerse del objeto y ejecutarse en el
contexto de cualquier otro valor. Y su resultado depende de ese valor.
… etc (personalizable).
Demostremos:
let s = Object.prototype.toString;
Symbol.toStringTag
El comportamiento del objeto toString se puede personalizar utilizando una propiedad de objeto
especial Symbol.toStringTag .
Por ejemplo:
let user = {
[Symbol.toStringTag]: "User"
};
Para la mayoría de los objetos específicos del entorno, existe dicha propiedad. Aquí hay
algunos ejemplos específicos del navegador:
Como puedes ver, el resultado es exactamente Symbol.toStringTag (si existe), envuelto en [object
...] .
Al final tenemos “typeof con esteroides” que no solo funciona para tipos de datos primitivos,
sino también para objetos incorporados e incluso puede personalizarse.
Podemos usar {}.toString.call en lugar de instanceof para los objetos incorporados cuando deseamos
obtener el tipo como una cadena en lugar de solo verificar.
Resumen
Resumamos los métodos de verificación de tipos que conocemos:
Como podemos ver, {}.toString es técnicamente un typeof “más avanzado”.
Y el operador instanceof realmente brilla cuando estamos trabajando con una jerarquía de clases
y queremos verificar si la clase tiene en cuenta la herencia.
Los Mixins
En JavaScript podemos heredar de un solo objeto. Solo puede haber un [[Prototype]] para un
objeto. Y una clase puede extender únicamente otra clase.
Pero a veces eso se siente restrictivo. Por ejemplo, tenemos una clase StreetSweeper y una
clase Bicycle , y queremos hacer su combinación: un StreetSweepingBicycle .
O tenemos una clase User y una clase EventEmitter que implementa la generación de eventos, y nos
gustaría agregar la funcionalidad de EventEmitter a User , para que nuestros usuarios puedan
emitir eventos.
Un ejemplo de mixin
La forma más sencilla de implementar un mixin en JavaScript es hacer un objeto con métodos
útiles, para que podamos combinarlos fácilmente en un prototipo de cualquier clase.
Por ejemplo, aquí el mixin sayHiMixin se usa para agregar algo de “diálogo” a User :
// mixinlet sayHiMixin = {
sayHi() {
alert(`Hola ${this.name}`);
},
sayBye() {
alert(`Adiós ${this.name}`);
}
};
// uso:class User {
constructor(name) {
this.name = name;
}
}
No hay herencia, sino un simple método de copia. Entonces, User puede heredar de otra clase y
también incluir el mixin para “mezclar” los métodos adicionales, como este:
Object.assign(User.prototype, sayHiMixin);
let sayHiMixin = {
__proto__: sayMixin, // (o podríamos usar Object.setPrototypeOf para configurar el prototype aquí)
sayHi() {
// llama al método padresuper.say(`Hola ${this.name}`); // (*)
},
sayBye() {
super.say(`Adios ${this.name}`); // (*)
}
};
class User {
constructor(name) {
this.name = name;
}
}
Ten en cuenta que la llamada al método padre super.say() de sayHiMixin (en las líneas etiquetadas
con (*) ) busca el método en el prototipo de ese mixin, no en la clase.
Esto se debe a que los métodos sayHi y sayBye se crearon inicialmente en sayHiMixin . Entonces, a
pesar de que se copiaron, su propiedad interna [[[HomeObject]] hace referencia a sayHiMixin , como
se muestra en la imagen de arriba.
Como super busca los métodos padres en [[HomeObject]].[[Prototype]] , esto significa que busca sayHiMixin.
[[Prototype]] .
EventMixin
Ahora hagamos un mixin para la vida real.
Una característica importante de muchos objetos del navegador (por ejemplo) es que pueden
generar eventos. Los eventos son una excelente manera de “transmitir información” a
cualquiera que lo desee. Así que hagamos un mixin que nos permita agregar fácilmente
funciones relacionadas con eventos a cualquier clase/objeto.
Después de agregar el mixin, un objeto user podrá generar un evento "login" cuando el visitante
inicie sesión. Y otro objeto, por ejemplo, calendar puede querer escuchar dichos eventos para
cargar el calendario para el persona registrada.
O bien, un menu puede generar el evento "seleccionar" cuando se selecciona un elemento del menú,
y otros objetos pueden asignar controladores para reaccionar ante ese evento. Y así.
Aquí está el código:
let eventMixin = {
/**
* Suscribe al evento, uso:
* menu.on('select', function(item) { ... }
*/
on(eventName, handler) {
if (!this._eventHandlers) this._eventHandlers = {};
if (!this._eventHandlers[eventName]) {
this._eventHandlers[eventName] = [];
}
this._eventHandlers[eventName].push(handler);
},
/**
* Cancelar la suscripción, uso:
* menu.off('select', handler)
*/
off(eventName, handler) {
let handlers = this._eventHandlers?.[eventName];
if (!handlers) return;
for (let i = 0; i < handlers.length; i++) {
if (handlers[i] === handler) {
handlers.splice(i--, 1);
}
}
},
/**
* Generar un evento con el nombre y los datos
* this.trigger('select', data1, data2);
*/
trigger(eventName, ...args) {
if (!this._eventHandlers?.[eventName]) {
return; // no hay controladores para ese nombre de evento
}
// Llama al controlador
this._eventHandlers[eventName].forEach(handler => handler.apply(this, args));
}
};
.on(eventName, handler) : asigna la función handler para que se ejecute cuando se produce el evento
con ese nombre. Técnicamente, hay una propiedad _eventHandlers que almacena una matriz de
controladores para cada nombre de evento, y simplemente la agrega a la lista.
Uso:
Ahora, si queremos que el código reaccione a una selección de menú, podemos escucharlo
con menu.on(...) .
Y el mixin de eventMixin hace que sea fácil agregar ese comportamiento a tantas clases como
queramos, sin interferir con la cadena de herencia.
Resumen
Mixin – es un término genérico de programación orientado a objetos: una clase que contiene
métodos para otras clases.
Manejo de errores
Manejo de errores, "try...catch"
No importa lo buenos que seamos en la programación, a veces nuestros scripts tienen errores.
Pueden ocurrir debido a nuestros descuidos, una entrada inesperada del usuario, una respuesta
errónea del servidor y por otras razones más.
Por lo general, un script “muere” (se detiene inmediatamente) en caso de error, imprimiéndolo
en la consola.
Pero hay una construcción sintáctica try...catch que nos permite “atrapar” errores para que el
script pueda, en lugar de morir, hacer algo más razonable.
La sintaxis “try…catch”
La construcción try...catch tiene dos bloques principales: try , y luego catch :
try {
// código...
} catch (err) {
// manipulación de error
Funciona así:
2. Si no hubo errores, se ignora catch (err) : la ejecución llega al final de try y continúa,
omitiendo catch .
3. Si se produce un error, la ejecución de try se detiene y el control fluye al comienzo
de catch (err) . La variable err (podemos usar cualquier nombre para ella) contendrá un
objeto de error con detalles sobre lo que sucedió.
try {
try {
alert('Inicio de ejecuciones try'); // (1) <-- lalala; // error, variable no está definida!alert('Fin de try (nunca alcanzado)
} catch (err) {
try {
{{{{{{{{{{{{
} catch(err) {
alert("El motor no puede entender este código, no es válido.");
}
El motor de JavaScript primero lee el código y luego lo ejecuta. Los errores que ocurren en
la fase de lectura se denominan errores de “tiempo de análisis” y son irrecuperables (desde
dentro de ese código). Eso es porque el motor no puede entender el código.
try {
setTimeout(function() {
noSuchVariable; // el script morirá aquí
}, 1000);
} catch (err) {
alert( "no funcionará" );
}
Esto se debe a que la función en sí misma se ejecuta más tarde, cuando el motor ya ha
abandonado la construcción try...catch .
Para detectar una excepción dentro de una función programada, try...catch debe estar dentro de
esa función:
setTimeout(function() {
try {
noSuchVariable; // try...catch maneja el error!
} catch {
alert( "El error se detecta aquí!" );
}
}, 1000);
Objeto Error
Cuando se produce un error, JavaScript genera un objeto que contiene los detalles al
respecto. El objeto se pasa como argumento para catch :
try {
// ...
} catch(err) { // <-- el "objeto error", podría usar otra palabra en lugar de err
// ...
}
Para todos los errores integrados, el objeto error tiene dos propiedades principales:
name Nombre de error. Por ejemplo, para una variable indefinida que
Hay otras propiedades no estándar disponibles en la mayoría de los entornos. Uno de los más
utilizados y compatibles es:
stack Pila de llamadas actual: una cadena con información sobre la secuencia de llamadas
Por ejemplo:
try {
lalala; // error, la variable no está definida!} catch (err) {
alert(err.name); // ReferenceError
alert(err.message); // lalala no está definida!
alert(err.stack); // ReferenceError: lalala no está definida en (...call stack)
try {
// ...
} catch { // <-- sin (err)
// ...
}
Usando “try…catch”
Exploremos un caso de uso de la vida real de try...catch .
Como ya sabemos, JavaScript admite el método JSON.parse(str) para leer valores codificados
con JSON.
Por lo general, se utiliza para decodificar datos recibidos a través de la red, desde el
servidor u otra fuente.
Lo recibimos y llamamos a JSON.parse así:
let user = JSON.parse(json); // convierte la representación de texto a objeto JS// ahora user es un objeto con propiedades de la caden
alert( user.name ); // John
alert( user.age ); // 30
Puede encontrar información más detallada sobre JSON en el capítulo Métodos JSON, toJSON.
Si json está mal formado, JSON.parse genera un error, por lo que el script “muere”.
¿Deberíamos estar satisfechos con eso? ¡Por supuesto no!
De esta manera, si algo anda mal con los datos, el visitante nunca lo sabrá (a menos que abra
la consola del desarrollador). Y a la gente realmente no le gusta cuando algo “simplemente
muere” sin ningún mensaje de error.
Usemos try...catch para manejar el error:
try {
let user = JSON.parse(json); // <-- cuando ocurre un error ...alert( user.name ); // no funciona
} catch (err) {
// ...la ejecución salta aquí
alert( "Nuestras disculpas, los datos tienen errores, intentaremos solicitarlos una vez más." );
alert( err.name );
alert( err.message );}
Aquí usamos el bloque catch solo para mostrar el mensaje, pero podemos hacer mucho más: enviar
una nueva solicitud de red, sugerir una alternativa al visitante, enviar información sobre el
error a una instalación de registro, …. Todo mucho mejor que solo morir.
try {
Aquí JSON.parse se ejecuta normalmente, pero la ausencia de name es en realidad un error
nuestro.
Para unificar el manejo de errores, usaremos el operador throw .
El operador “throw”
El operador throw genera un error.
La sintaxis es:
Técnicamente, podemos usar cualquier cosa como un objeto error. Eso puede ser incluso un
primitivo, como un número o una cadena, pero es mejor usar objetos, preferiblemente con
propiedades name y message (para mantenerse algo compatible con los errores incorporados).
JavaScript tiene muchos constructores integrados para manejar errores
estándar: Error , SyntaxError , ReferenceError , TypeError y otros. Podemos usarlos para crear objetos de
error también.
Su sintaxis es:
Para errores incorporados (no para cualquier objeto, solo para errores), la propiedad name es
exactamente el nombre del constructor. Y mensaje se toma del argumento.
Por ejemplo:
alert(error.name); // Error
alert(error.message); // Estas cosas pasan... o_O
try {
JSON.parse("{ json malo o_O }");
} catch (err) {
alert(err.name); // SyntaxErroralert(err.message); // Token b inesperado en JSON en la posición 2
}
try {
if (!user.name) {
throw new SyntaxError("dato incompleto: sin nombre"); // (*)}
alert( user.name );
} catch (err) {
En la línea (*) , el operador throw genera un SyntaxError con el message dado, de la misma manera
que JavaScript lo generaría él mismo. La ejecución de try se detiene inmediatamente y el
flujo de control salta a catch .
Ahora catch se convirtió en un lugar único para todo el manejo de errores: tanto
para JSON.parse como para otros casos.
Relanzando (rethrowing)
En el ejemplo anterior usamos try...catch para manejar datos incorrectos. Pero, ¿es posible
que ocurra otro error inesperado dentro del bloque try{...} ? Como un error de programación (la
variable no está definida) o algo más, no solo “datos incorrectos”.
Por ejemplo:
try {
user = JSON.parse(json); // <-- olvidé poner "let" antes del usuario
// ...
} catch (err) {
alert("Error en JSON: " + err); // Error en JSON: ReferenceError: user no está definido
// (no es error JSON)
}
¡Por supuesto, todo es posible! Los programadores cometen errores. Incluso en las utilidades
de código abierto utilizadas por millones durante décadas, de repente se puede descubrir un
error que conduce a hacks terribles.
En nuestro caso, try...catch está destinado a detectar errores de “datos incorrectos”. Pero por
su naturaleza, catch obtiene todos los errores de try . Aquí recibe un error inesperado, pero
aún muestra el mismo mensaje de “Error en JSON”. Eso está mal y también hace que el código
sea más difícil de depurar.
Para evitar tales problemas, podemos emplear la técnica de “rethrowing”. La regla es simple:
Catch solo debe procesar los errores que conoce y “volver a lanzar” (rethrow) a todos los
demás.
try {
user = { /*...*/ };
} catch (err) {
if (err instanceof ReferenceError) {alert('ReferenceError'); // "ReferenceError" para acceder a una variable indefinida
}
}
También podemos obtener el nombre de la clase error con la propiedad err.name . Todos los
errores nativos lo tienen. Otra opción es leer err.constructor.name .
En el siguiente código, usamos el rethrowing para que catch solo maneje SyntaxError :
} catch (err) {
El error lanzado en la línea (*) desde el interior del bloque catch cae desde try...catch y
puede ser atrapado por una construcción externa try...catch (si existe), o mata al script.
Por lo tanto, el bloque catch en realidad maneja solo los errores con los que sabe cómo lidiar
y “omite” todos los demás.
El siguiente ejemplo demuestra cómo dichos errores pueden ser detectados por un nivel más
de try...catch :
function readData() {
let json = '{ "age": 30 }';
try {
// ...
blabla(); // error!} catch (err) {
// ...
if (!(err instanceof SyntaxError)) {
throw err; // rethrow (no sé cómo lidiar con eso)}
}
}
try {
readData();
} catch (err) {
alert( "La captura externa tiene: " + err ); // capturado!}
Aquí readData solo sabe cómo manejar SyntaxError , mientras que el try...catch externo sabe cómo
manejar todo.
try…catch…finally
Espera, eso no es todo.
La construcción try...catch puede tener una cláusula de código más: finally .
Si existe, se ejecuta en todos los casos:
try {
... intenta ejecutar el código ...
} catch (err) {
... manejar errores ...
} finally {
... ejecutar siempre ...
}
try {
alert( 'intenta (try)' );
if (confirm('¿Cometer un error?')) BAD_CODE();
} catch (err) {
alert( 'atrapa (catch)' );
} finally {
1. Si responde “Sí” a “¿Cometer un error?”, Entonces try -> catch -> finally .
La cláusula finally a menudo se usa cuando comenzamos a hacer algo y queremos finalizarlo en
cualquier resultado.
Por ejemplo, queremos medir el tiempo que tarda una función de números de Fibonacci fib(n) .
Naturalmente, podemos comenzar a medir antes de que se ejecute y terminar después. ¿Pero qué
pasa si hay un error durante la llamada a la función? En particular, la implementación
de fib(n) en el código siguiente devuelve un error para números negativos o no enteros.
La cláusula finally es un excelente lugar para terminar las mediciones, pase lo que pase.
Aquí finally garantiza que el tiempo se medirá correctamente en ambas situaciones, en caso de
una ejecución exitosa de fib y en caso de error:
function fib(n) {
if (n < 0 || Math.trunc(n) != n) {
throw new Error("Debe ser un número positivo y entero.");
}
return n <= 1 ? n : fib(n - 1) + fib(n - 2);
}
try {
result = fib(num);
} catch (err) {
result = 0;
} finally {
diff = Date.now() - start;
}alert(result || "error ocurrido");
Tenga en cuenta que las variables result y diff en el código anterior se declaran antes
de try..catch .
De lo contrario, si declaramos let en el bloque try , solo sería visible dentro de él.
finally y return
function func() {
try {
return 1;} catch (err) {
/* ... */
} finally {
alert( 'finally' );}
}
function func() {
// comenzar a hacer algo que necesita ser completado (como mediciones)
try {
// ...
} finally {
// completar esto si todo muere
}
}
En el código anterior, siempre se produce un error dentro de try , porque no hay catch .
Pero finally funciona antes de que el flujo de ejecución abandone la función.
Captura global
Específico del entorno
La información de esta sección no es parte del núcleo de JavaScript.
Imaginemos que tenemos un error fatal fuera de try...catch , y el script murió. Como un error de
programación o alguna otra cosa terrible.
¿Hay alguna manera de reaccionar ante tales ocurrencias? Es posible que queramos registrar el
error, mostrarle algo al usuario (normalmente no ve mensajes de error), etc.
La sintaxis:
message Mensaje de error. url URL del script donde ocurrió el error. line , col Números de línea y
<script>
window.onerror = function(message, url, line, col, error) {
alert(`${message}\n At ${line}:${col} of ${url}`);
};function readData() {
badFunc(); // ¡Vaya, algo salió mal!
}
readData();
</script>
4. Podemos iniciar sesión en la interfaz web del servicio y ver los errores registrados.
Resumen
La construcción try...catch permite manejar errores de tiempo de ejecución. Literalmente permite
“intentar (try)” ejecutar el código y “atrapar (catch)” errores que pueden ocurrir en él.
La sintaxis es:
try {
// ejecuta este código
} catch (err) {
// si ocurrió un error, entonces salta aquí
// err es el objeto error
} finally {
// hacer en cualquier caso después de try/catch
}
Puede que no haya una sección catch o finally , por lo que las construcciones más
cortas try...catch y try...finally también son válidas.
Los objetos Error tienen las siguientes propiedades:
name – la cadena con el nombre del error (nombre del constructor de error).
stack (No estándar, pero bien soportado) – la pila en el momento de la creación del error.
Si no se necesita un objeto error, podemos omitirlo usando catch { en lugar de catch (err) { .
También podemos generar nuestros propios errores utilizando el operador throw . Técnicamente,
el argumento de throw puede ser cualquier cosa, pero generalmente es un objeto error heredado
de la clase incorporada Error . Más sobre la extensión de errores en el próximo capítulo.
Relanzado (rethrowing) es un patrón muy importante de manejo de errores: un
bloque catch generalmente espera y sabe cómo manejar el tipo de error en particular, por lo
que debería relanzar errores que no conoce.
Cuando desarrollamos algo, a menudo necesitamos nuestras propias clases de error para
reflejar cosas específicas que pueden salir mal en nuestras tareas. Para errores en las
operaciones de red, podemos necesitar HttpError , para las operaciones de la base de
datos DbError , para las operaciones de búsqueda NotFoundError , etc.
Nuestros errores deben admitir propiedades de error básicas como message , name y,
preferiblemente, stack . Pero también pueden tener otras propiedades propias, por ejemplo, los
objetos HttpError pueden tener una propiedad statusCode con un valor como 404 o 403 o 500 .
JavaScript permite usar throw con cualquier argumento, por lo que técnicamente nuestras clases
de error personalizadas no necesitan heredarse de Error . Pero si heredamos, entonces es
posible usar obj instanceof Error para identificar objetos error. Entonces es mejor heredar de él.
A medida que la aplicación crece, nuestros propios errores forman naturalmente una jerarquía.
Por ejemplo, HttpTimeoutError puede heredar de HttpError , y así sucesivamente.
Internamente, usaremos JSON.parse . Si recibe json mal formado, entonces arroja SyntaxError . Pero
incluso si json es sintácticamente correcto, eso no significa que sea un usuario válido,
¿verdad? Puede perder los datos necesarios. Por ejemplo, puede no tener propiedades de nombre
y edad que son esenciales para nuestros usuarios.
Nuestra función readUser(json) no solo leerá JSON, sino que verificará (“validará”) los datos. Si
no hay campos obligatorios, o el formato es incorrecto, entonces es un error. Y eso no es un
“SyntaxError”, porque los datos son sintácticamente correctos, sino otro tipo de error. Lo
llamaremos ValidationError y crearemos una clase para ello. Un error de ese tipo también debe
llevar la información sobre el campo infractor.
Nuestra clase ValidationError debería heredar de la clase incorporada Error .
Esa clase está incorporada, pero aquí está su código aproximado para que podamos entender lo
que estamos extendiendo:
function test() {
throw new ValidationError("Vaya!");
}
try {
test();
} catch(err) {
alert(err.message); // Vaya!
alert(err.name); // ValidationError
alert(err.stack); // una lista de llamadas anidadas con números de línea para cada una
}
Tenga en cuenta: en la línea (1) llamamos al constructor padre. JavaScript requiere que
llamemos super en el constructor hijo, por lo que es obligatorio. El constructor padre
establece la propiedad message .
El constructor principal también establece la propiedad name en "Error" , por lo que en la
línea (2) la restablecemos al valor correcto.
Intentemos usarlo en readUser(json) :
if (!user.age) {
throw new ValidationError("Sin campo: age");
}
if (!user.name) {
throw new ValidationError("Sin campo: name");
}
return user;
}
try {
let user = readUser('{ "age": 25 }');
} catch (err) {
if (err instanceof ValidationError) {
alert("Dato inválido: " + err.message); // Dato inválido: sin campo: nombre} else if (err instanceof SyntaxError) { // (*)
alert("Error de sintaxis JSON: " + err.message);
} else {
throw err; // error desconocido, vuelva a lanzarlo (**)
}
}
El bloque try..catch en el código anterior maneja tanto nuestro ValidationError como
el SyntaxError incorporado de JSON.parse .
Observe cómo usamos instanceof para verificar el tipo de error específico en la línea (*) .
// ...
// en lugar de (err instanceof SyntaxError)
} else if (err.name == "SyntaxError") { // (*)
// ...
La versión instanceof es mucho mejor, porque en el futuro vamos a extender ValidationError , haremos
subtipos de ella, como PropertyRequiredError . Y el control instanceof continuará funcionando para las
nuevas clases heredadas. Entonces eso es a prueba de futuro.
También es importante que si catch encuentra un error desconocido, entonces lo vuelva a lanzar
en la línea (**) . El bloque catch solo sabe cómo manejar los errores de validación y sintaxis,
otros tipos de error (como los tipográficos en el código u otros desconocidos) deben “pasar a
través” y ser relanzados.
Herencia adicional
La clase ValidationError es demasiado genérica. Son muchas las cosas que pueden salir mal. La
propiedad podría estar ausente, o puede estar en un formato incorrecto (como un valor de
cadena para age en lugar de un número). Hagamos una clase más
concreta PropertyRequiredError específicamente para propiedades ausentes. Esta clase llevará
información adicional sobre la propiedad que falta.
if (!user.age) {
return user;
}
try {
let user = readUser('{ "age": 25 }');
} catch (err) {
if (err instanceof ValidationError) {
alert("Dato inválido: " + err.message); // Dato inválido: Sin propiedad: name
alert(err.name); // PropertyRequiredError
alert(err.property); // name} else if (err instanceof SyntaxError) {
alert("Error de sintaxis JSON: " + err.message);
} else {
throw err; // error desconocido, vuelva a lanzarlo
}
}
La nueva clase PropertyRequiredError es fácil de usar: solo necesitamos pasar el nombre de la
propiedad: new PropertyRequiredError(property) . El message legible para humanos es generado por el
constructor.
Tenga en cuenta que this.name en el constructor PropertyRequiredError se asigna de nuevo manualmente.
Eso puede volverse un poco tedioso: asignar this.name = <class name> en cada clase de error
personalizada. Podemos evitarlo haciendo nuestra propia clase “error básico” que
asigna this.name = this.constructor.name . Y luego herede todos nuestros errores personalizados.
Llamémosla MyError .
Aquí está el código con MyError y otras clases error personalizadas, simplificadas:
// name es incorrecto
alert( new PropertyRequiredError("campo").name ); // PropertyRequiredError
Ahora los errores personalizados son mucho más cortos, especialmente ValidationError , ya que
eliminamos la línea "this.name = ..." en el constructor.
Empacado de Excepciones
El propósito de la función readUser en el código anterior es “leer los datos del usuario”.
Puede haber diferentes tipos de errores en el proceso. En este momento
tenemos SyntaxError y ValidationError , pero en el futuro la función readUser puede crecer y
probablemente generar otros tipos de errores.
El código que llama a readUser debe manejar estos errores. En este momento utiliza
múltiples if en el bloque catch , que verifican la clase y manejan los errores conocidos y
vuelven a arrojar los desconocidos.
El esquema es así:
try {
...
readUser() // la fuente potencial de error
En el código anterior podemos ver dos tipos de errores, pero puede haber más.
Si la función readUser genera varios tipos de errores, entonces debemos preguntarnos:
¿realmente queremos verificar todos los tipos de error uno por uno cada vez?
A menudo, la respuesta es “No”: nos gustaría estar “un nivel por encima de todo eso”. Solo
queremos saber si hubo un “error de lectura de datos”: el por qué ocurrió exactamente es a
menudo irrelevante (el mensaje de error lo describe). O, mejor aún, nos gustaría tener una
forma de obtener los detalles del error, pero solo si es necesario.
1. Crearemos una nueva clase ReadError para representar un error genérico de “lectura de
datos”.
2. La función readUser detectará los errores de lectura de datos que ocurren dentro de ella,
como ValidationError y SyntaxError , y generará un ReadError en su lugar.
Entonces, el código que llama a readUser solo tendrá que verificar ReadError , no todos los tipos
de errores de lectura de datos. Y si necesita más detalles de un error, puede verificar su
propiedad cause .
Aquí está el código que define ReadError y demuestra su uso en readUser y try..catch :
function validateUser(user) {
if (!user.age) {
throw new PropertyRequiredError("age");
}
if (!user.name) {
throw new PropertyRequiredError("name");
}
}
function readUser(json) {
let user;
try {
user = JSON.parse(json);
} catch (err) {
if (err instanceof SyntaxError) {
throw new ReadError("Error de sintaxis", err);
} else {
throw err;
}}
try {
validateUser(user);
} catch (err) {
if (err instanceof ValidationError) {
throw new ReadError("Error de validación", err);
} else {
throw err;
}}
try {
readUser('{json malo}');
} catch (e) {
if (e instanceof ReadError) {
alert(e);
// Error original: SyntaxError: inesperado token b en JSON en la posición 1
alert("Error original: " + e.cause);} else {
throw e;
}
}
En el código anterior, readUser funciona exactamente como se describe: detecta los errores de
sintaxis y validación y arroja los errores ReadError en su lugar (los errores desconocidos se
vuelven a generar como de costumbre).
Entonces, el código externo verifica instanceof ReadError y eso es todo. No es necesario enumerar
todos los tipos de error posibles.
El enfoque se llama “empacado de excepciones”, porque tomamos excepciones de “bajo nivel” y
las “ajustamos” en ReadError que es más abstracto. Es ampliamente utilizado en la programación
orientada a objetos.
Resumen
Podemos heredar de Error y otras clases de error incorporadas normalmente. Solo necesitamos
cuidar la propiedad name y no olvidemos llamar super .
Podemos usar instanceof para verificar errores particulares. También funciona con herencia.
Pero a veces tenemos un objeto error que proviene de una biblioteca de terceros y no hay
una manera fácil de obtener su clase. Entonces la propiedad name puede usarse para tales
controles.
Promesas y async/await
Introducción: callbacks
Muchas funciones son proporcionadas por el entorno de host de Javascript que permiten
programar acciones asíncronas. En otras palabras, acciones que iniciamos ahora, pero que
terminan más tarde.
function loadScript(src) {
// crea una etiqueta <script> y la agrega a la página
// esto hace que el script dado: src comience a cargarse y ejecutarse cuando se complete
let script = document.createElement('script');
script.src = src;
document.head.append(script);
}
Esto inserta en el documento una etiqueta nueva, creada dinámicamente, <script src =" ... "> con el
código src dado. El navegador comienza a cargarlo automáticamente y lo ejecuta cuando la
carga se completa.
Esta función la podemos usar así:
loadScript('/my/script.js');
// el código debajo de loadScript
// no espera a que finalice la carga del script
// ...
Digamos que necesitamos usar el nuevo script tan pronto como se cargue. Declara nuevas
funciones, y queremos ejecutarlas.
Si hacemos eso inmediatamente después de llamar a loadScript (...) , no funcionará:
document.head.append(script);
}
loadScript('/my/script.js', function() {
// la callback se ejecuta luego que se carga el script
newFunction(); // ahora funciona
...
});
Esa es la idea: el segundo argumento es una función (generalmente anónima) que se ejecuta
cuando se completa la acción.
Aquí un ejemplo ejecutable con un script real:
loadScript('/my/script.js', function(script) {
loadScript('/my/script2.js', function(script) {
alert(`Genial, el segundo script está cargado`);
});});
Una vez que se completa el loadScript externo, la callback inicia el interno.
¿Qué pasa si queremos un script más …?
loadScript('/my/script.js', function(script) {
loadScript('/my/script2.js', function(script) {
loadScript('/my/script3.js', function(script) {
// ...continua después que se han cargado todos los scripts
});});
});
Entonces, cada nueva acción está dentro de una callback. Eso está bien para algunas acciones,
pero no es bueno para todas, así que pronto veremos otras variantes.
Manejo de errores
En los ejemplos anteriores no consideramos los errores. ¿Qué pasa si falla la carga del
script? Nuestra callback debería poder reaccionar ante eso.
Aquí una versión mejorada de loadScript que rastrea los errores de carga:
document.head.append(script);
}
Así usamos una única función de ‘callback’ tanto para informar errores como para transferir
resultados.
Pirámide infernal
A primera vista, es una forma viable de codificación asincrónica. Y de hecho lo es. Para una
o quizás dos llamadas anidadas, se ve bien.
Pero para múltiples acciones asincrónicas que van una tras otra, tendremos un código como
este:
if (error) {
handleError(error);
} else {
// ...
loadScript('2.js', function(error, script) {
if (error) {
handleError(error);
} else {
// ...
loadScript('3.js', function(error, script) {
if (error) {
handleError(error);
} else {
// ...continua después de que se han cargado todos los script (*)}
});
}
});
}
});
En el código de arriba:
3. Cargamos 3.js , entonces, si no hay ningún error: haga otra cosa (*) .
A medida que las llamadas se anidan más, el código se vuelve más profundo y difícil de
administrar, especialmente si tenemos un código real en lugar de ‘…’ que puede incluir más
bucles, declaraciones condicionales, etc.
A esto se le llama “infierno de callbacks” o “pirámide infernal” (“callback hell”, “pyramid
of doom”).
loadScript('1.js', step1);
¿Lo Ves? Hace lo mismo, y ahora no hay anidamiento profundo porque convertimos cada acción en
una función de nivel superior separada.
Funciona, pero el código parece una hoja de cálculo desgarrada. Es difícil de leer, y habrás
notado que hay que saltar de un lado a otro mientras lees. Es un inconveniente, especialmente
si el lector no está familiarizado con el código y no sabe dónde dirigir la mirada.
Además, las funciones llamadas step* son de un solo uso, son para evitar la “Pirámide de
callbacks”. Nadie los reutilizará fuera de la cadena de acción. Así que hay muchos nombres
abarrotados aquí.
Nos gustaría tener algo mejor.
Afortunadamente, hay otras formas de evitar tales pirámides. Una de las mejores formas es
usando “promesas”, descritas en el próximo capítulo.
Promesa
Esta es una analogía de la vida real para las cosas que a menudo tenemos en la programación:
1. Un “código productor” que hace algo y toma tiempo. Por ejemplo, algún código que carga los
datos a través de una red. Eso es un “cantante”.
2. Un “código consumidor” que quiere el resultado del “código productor” una vez que está
listo. Muchas funciones pueden necesitar ese resultado. Estos son los “fans”.
La analogía no es terriblemente precisa, porque las promesas de JavaScript son más complejas
que una simple lista de suscripción: tienen características y limitaciones adicionales. Pero
está bien para empezar.
La sintaxis del constructor para un objeto promesa es:
La función pasada a new Promise se llama ejecutor. Cuando se crea new Promise , el ejecutor corre
automáticamente. Este contiene el código productor que a la larga debería producir el
resultado. En términos de la analogía anterior: el ejecutor es el “cantante”.
Sus argumentos resolve y reject son callbacks proporcionadas por el propio JavaScript. Nuestro
código solo está dentro del ejecutor.
Cuando el ejecutor, más tarde o más temprano, eso no importa, obtiene el resultado, debe
llamar a una de estos callbacks:
Para resumir: el ejecutor corre automáticamente e intenta realizar una tarea. Cuando termina
con el intento, llama a resolve si fue exitoso o reject si hubo un error.
El objeto promise devuelto por el constructor new Promise tiene estas propiedades internas:
Entonces el ejecutor, en algún momento, pasa la promise a uno de estos estados:
Aquí hay un ejemplo de un constructor de promesas y una función ejecutora simple con “código
productor” que toma tiempo (a través de setTimeout ):
// después de 1 segundo, indica que la tarea está hecha con el resultado "hecho"
setTimeout(() => resolve("hecho"), 1000);
});
2. El ejecutor recibe dos argumentos: resolve y reject . Estas funciones están predefinidas por
el motor de JavaScript, por lo que no necesitamos crearlas. Solo debemos llamar a uno de
ellos cuando esté listo.
Después de un segundo de “procesamiento”, el ejecutor llama a resolve("hecho") para producir
el resultado. Esto cambia el estado del objeto promise :
La idea es que una tarea realizada por el ejecutor puede tener solo un resultado o un error.
Además, resolve / reject espera solo un argumento (o ninguno) e ignorará argumentos adicionales.
Por ejemplo, esto puede suceder cuando comenzamos una tarea, pero luego vemos que todo ya se
ha completado y almacenado en caché.
Está bien. Inmediatamente tenemos una promesa resuelta.
then
El más importante y fundamental es .then .
La sintaxis es:
promise.then(
function(result) { /* manejar un resultado exitoso */ },
function(error) { /* manejar un error */ }
);
El primer argumento de .then es una función que se ejecuta cuando se resuelve la promesa y
recibe el resultado.
Si solo nos interesan las terminaciones exitosas, entonces podemos proporcionar solo un
argumento de función para .then :
catch
Si solo nos interesan los errores, entonces podemos usar null como primer argumento: .then(null,
errorHandlingFunction) . O podemos usar .catch(errorHandlingFunction) , que es exactamente lo mismo:
La llamada .catch(f) es un análogo completo de .then(null, f) , es solo una abreviatura.
Limpieza: finally
Al igual que hay una cláusula finally en un try {...} catch {...} normal, hay un finally en las
promesas.
La llamada .finally(f) es similar a .then(f, f) en el sentido de que f siempre se ejecuta cuando
se resuelve la promesa: ya sea que se resuelva o rechace.
La idea de finally es establecer un manejador para realizar la limpieza y finalización después
de que las operaciones se hubieran completado.
Por ejemplo, detener indicadores de carga, cerrar conexiones que ya no son necesarias, etc.
Puedes pensarlo como el finalizador de la fiesta. No importa si la fiesta fue buena o mala ni
cuántos invitados hubo, aún necesitamos (o al menos deberíamos) hacer la limpieza después.
El código puede verse como esto:
1. Un manejador finally no tiene argumentos. En finally no sabemos si la promesa es exitosa o
no. Eso está bien, ya que usualmente nuestra tarea es realizar procedimientos de
finalización “generales”.
Por favor observe el ejemplo anterior: como puede ver, el manejador de finally no tiene
argumentos, y lo que sale de la promesa es manejado en el siguiente manejador.
2. Resultados y errores pasan “a través” del manejador de finally . Estos pasan al siguiente
manejador que se adecúe.
Por ejemplo, aquí el resultado se pasa a través de finally a then :
Como puede ver, el “valor” devuelto por la primera promesa es pasado a través de finally al
siguiente then .
Esto es muy conveniente, porque finally no está destinado a procesar el resultado de una
promesa. Como dijimos antes, es el lugar para hacer la limpieza general sin importar cuál
haya sido el resultado.
Y aquí, el ejemplo de un error para que veamos cómo se pasa, a través de finally , a catch :
3. Un manejador de finally tampoco debería devolver nada. Y si lo hace, el valor devuelto es
ignorado silenciosamente.
La única excepción a esta regla se da cuando el manejador mismo de finally dispara un error.
En ese caso, este error pasa al siguiente manejador de error en lugar del resultado previo
al finally.
Para summarizar:
Un manejador finally no obtiene lo que resultó del manejador previo (no tiene argumentos).
Ese resultado es pasado a través de él al siguiente manejador.
Cuando es finally el que dispara el error, la ejecución pasa al manejador de error más
cercano.
Estas características son de ayuda y hacen que las cosas funcionen tal como corresponde si
“finalizamos” con finally como se supone: con procedimientos de limpieza genéricos.
Ten en cuenta que esto es diferente y más poderoso que el escenario de la “lista de
suscripción” de la vida real. Si el cantante ya lanzó su canción y luego una persona se
registra en la lista de suscripción, probablemente no recibirá esa canción. Las suscripciones
en la vida real deben hacerse antes del evento.
Las promesas son más flexibles. Podemos agregar manejadores en cualquier momento: si el
resultado ya está allí, nuestros manejadores lo obtienen de inmediato.
Ejemplo: loadScript
A continuación, veamos ejemplos más prácticos de cómo las promesas pueden ayudarnos a
escribir código asincrónico.
Tenemos, del capítulo anterior, la función loadScript para cargar un script.
Aquí está la variante basada callback, solo para recordarnos:
document.head.append(script);
}
La nueva función loadScript no requerirá una callback. En su lugar, creará y devolverá un objeto
Promise que se resuelve cuando se completa la carga. El código externo puede agregar
manejadores (funciones de suscripción) usando .then :
function loadScript(src) {
return new Promise(function(resolve, reject) {
let script = document.createElement('script');
script.src = src;
document.head.append(script);
});
}
Uso:
promise.then(
script => alert(`${script.src} está cargado!`),
error => alert(`Error: ${error.message}`)
);
Promesas Callbacks
Las promesas nos permiten hacer las cosas en el orden Debemos tener una función callback a nuestra
natural. Primero, ejecutamos loadScript (script) , disposición al llamar a ‘loadScript(script,
Entonces, las promesas nos dan un mejor flujo de código y flexibilidad. Pero hay más. Lo
veremos en los próximos capítulos.
Encadenamiento de promesas
Se ve así:
}).then(function(result) { // (**)
alert(result); // 1
return result * 2;
}).then(function(result) { // (***)
alert(result); // 2
return result * 2;
}).then(function(result) {
alert(result); // 4
return result * 2;
});
2. Entonces se llama el manejador .then (**) , que a su vez crea una nueva promesa (resuelta
con el valor 2 ).
3. El siguiente .then (***) obtiene el resultado del anterior, lo procesa (duplica) y lo pasa
al siguiente manejador.
4. …y así sucesivamente.
A medida que el resultado se pasa a lo largo de la cadena de controladores, podemos ver una
secuencia de llamadas de alerta: 1 → 2 → 4 .
promise.then(function(result) {
alert(result); // 1
return result * 2;
});
promise.then(function(result) {
alert(result); // 1
return result * 2;
});
promise.then(function(result) {
alert(result); // 1
return result * 2;
});
Lo que hicimos aquí fue varios controladores para una sola promesa. No se pasan el resultado
el uno al otro; en su lugar lo procesan de forma independiente.
Aquí está la imagen (compárala con el encadenamiento anterior):
Todos los ‘.then’ en la misma promesa obtienen el mismo resultado: el resultado de esa
promesa. Entonces, en el código sobre todo alert muestra lo mismo: 1 .
En la práctica, rara vez necesitamos múltiples manejadores para una promesa. El
encadenamiento se usa mucho más a menudo.
Devolviendo promesas
}).then(function(result) {
alert(result); // 1
alert(result); // 2
}).then(function(result) {
alert(result); // 4
});
En este código el primer .then muestra 1 y devuelve new Promise(...) en la línea (*) . Después de
un segundo, se resuelve, y el resultado (el argumento de resolve , aquí es result * 2 ) se pasa al
controlador del segundo .then . Ese controlador está en la línea (**) , muestra 2 y hace lo
mismo.
Por lo tanto, la salida es la misma que en el ejemplo anterior: 1 → 2 → 4, pero ahora con 1
segundo de retraso entre las llamadas de alerta.
El ejemplo: loadScript
Usemos esta función con el loadScript promisificado, definido en el capítulo anterior, para
cargar los scripts uno por uno, en secuencia:
loadScript("/article/promise-chaining/one.js")
.then(function(script) {
return loadScript("/article/promise-chaining/two.js");
})
.then(function(script) {
return loadScript("/article/promise-chaining/three.js");
})
.then(function(script) {
// usamos las funciones declaradas en los scripts
// para demostrar que efectivamente se cargaron
one();
two();
three();
});
loadScript("/article/promise-chaining/one.js")
.then(script => loadScript("/article/promise-chaining/two.js"))
.then(script => loadScript("/article/promise-chaining/three.js"))
.then(script => {
// los scripts se cargaron, podemos usar las funciones declaradas en ellos
one();
two();
three();
});
loadScript("/article/promise-chaining/one.js").then(script1 => {
loadScript("/article/promise-chaining/two.js").then(script2 => {
loadScript("/article/promise-chaining/three.js").then(script3 => {
// esta función tiene acceso a las variables script1, script2 y script3
one();
two();
three();
});
});
});
Este código hace lo mismo: carga 3 scripts en secuencia. Pero “crece hacia la derecha”.
Entonces tenemos el mismo problema que con los callbacks.
Quienes comienzan a usar promesas pueden desconocer el encadenamiento, y por ello escribirlo
de esta manera. En general, se prefiere el encadenamiento.
A veces es aceptable escribir .then directamente, porque la función anidada tiene acceso al
ámbito externo. En el ejemplo anterior, el callback más anidado tiene acceso a todas las
variables script1 , script2 , script3 . Pero eso es una excepción más que una regla.
Objetos Thenables
Para ser precisos, un controlador puede devolver no exactamente una promesa, sino un objeto
llamado “thenable”, un objeto arbitrario que tiene un método .then . Será tratado de la misma
manera que una promesa.
La idea es que las librerías de terceros puedan implementar sus propios objetos “compatibles
con la promesa”. Pueden tener un conjunto extendido de métodos, pero también ser compatibles
con las promesas nativas, porque implementan .then .
class Thenable {
constructor(num) {
this.num = num;
}
then(resolve, reject) {
alert(resolve); // function() { código nativo }
// resolve con this.num*2 después de 1 segundo
setTimeout(() => resolve(this.num * 2), 1000); // (**)
}
}
JavaScript comprueba el objeto devuelto por el controlador .then en la línea (*) : si tiene un
método invocable llamado then , entonces llama a ese método que proporciona funciones
nativas resolve , accept como argumentos (similar a un ejecutor) y espera hasta que se llame a
uno de ellos. En el ejemplo anterior, se llama a resolve(2) después de 1 segundo (**) . Luego, el
resultado se pasa más abajo en la cadena.
Esta característica nos permite integrar objetos personalizados con cadenas de promesa sin
tener que heredar de Promise .
Esto hace una solicitud de red a la url y devuelve una promesa. La promesa se resuelve con un
objeto ‘response’ cuando el servidor remoto responde con encabezados, pero antes de que se
descargue la respuesta completa.
Para leer la respuesta completa, debemos llamar al método response.text() : devuelve una promesa
que se resuelve cuando se descarga el texto completo del servidor remoto, con ese texto como
resultado.
El siguiente código hace una solicitud a user.json y carga su texto desde el servidor:
fetch('/article/promise-chaining/user.json')
// .a continuación, se ejecuta cuando el servidor remoto responde
.then(function(response) {
// response.text() devuelve una nueva promesa que se resuelve con el texto de respuesta completo
// cuando se carga
return response.text();
})
.then(function(text) {
// ...y aquí está el contenido del archivo remoto
alert(text); // {"name": "iliakan", isAdmin: true}
});
El objeto response devuelto por fetch también incluye el método response.json() que lee los datos
remotos y los analiza como JSON. En nuestro caso, eso es aún más conveniente, así que pasemos
a ello.
También usaremos las funciones de flecha por brevedad:
// igual que el anterior, pero response.json() analiza el contenido remoto como JSON
fetch('/article/promise-chaining/user.json')
.then(response => response.json())
.then(user => alert(user.name)); // iliakan, tengo nombre de usuario
El código funciona; ver comentarios sobre los detalles. Sin embargo, hay un problema
potencial, un error típico para aquellos que comienzan a usar promesas.
Para que la cadena sea extensible, debemos devolver una promesa que se resuelva cuando el
avatar termine de mostrarse.
Como esto:
fetch('/article/promise-chaining/user.json')
.then(response => response.json())
.then(user => fetch(`https://api.github.com/users/${user.name}`))
.then(response => response.json())
.then(githubUser => new Promise(function(resolve, reject) { // (*)let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
setTimeout(() => {
img.remove();
resolve(githubUser); // (**)}, 3000);
}))
// se dispara después de 3 segundos
.then(githubUser => alert(`Terminado de mostrar ${githubUser.name}`));
Es decir, el controlador .then en la línea (*) ahora devuelve new Promise , que se resuelve solo
después de la llamada de resolve(githubUser) en setTimeout (**) . El siguiente ‘.then’ en la cadena
esperará eso.
Como buena práctica, una acción asincrónica siempre debe devolver una promesa. Eso hace
posible planificar acciones posteriores; incluso si no planeamos extender la cadena ahora, es
posible que la necesitemos más adelante.
function loadJson(url) {
return fetch(url)
.then(response => response.json());
}
function loadGithubUser(name) {
return loadJson(`https://api.github.com/users/${name}`);
}
function showAvatar(githubUser) {
return new Promise(function(resolve, reject) {
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
setTimeout(() => {
img.remove();
resolve(githubUser);
}, 3000);
});
}
// Úsalos:
loadJson('/article/promise-chaining/user.json')
.then(user => loadGithubUser(user.name))
.then(showAvatar)
.then(githubUser => alert(`Finished showing ${githubUser.name}`));
// ...
Resumen
Si un controlador .then (o catch/finally , no importa) devuelve una promesa, el resto de la cadena
espera hasta que se asiente. Cuando lo hace, su resultado (o error) se pasa más allá.
Aquí hay una imagen completa:
Las promesas encadenadas son excelentes manejando los errores. Cuando una promesa es
rechazada, el control salta al manejador de rechazos más cercano. Esto es muy conveniente en
la práctica.
Por ejemplo, en el código de abajo, la URL a la cual se le hace fetch es incorrecta (no existe
el sitio) y al ser rechazada .catch maneja el error:
Como puedes ver, el .catch no tiene que escribirse inmediatamente después de la promesa. Este
puede aparecer después de uno o quizás varios .then .
O, puede ocurrir, que todo en el sitio se encuentre bien, pero la respuesta no es un JSON
válido. La forma más fácil de detectar todos los errores es agregando .catch al final de la
cadena de promesas:
fetch('/article/promise-chaining/user.json')
.then(response => response.json())
.then(user => fetch(`https://api.github.com/users/${user.name}`))
.then(response => response.json())
.then(githubUser => new Promise((resolve, reject) => {
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
setTimeout(() => {
img.remove();
resolve(githubUser);
}, 3000);
}))
.catch(error => alert(error.message));
Lo normal es que tal .catch no se dispare en absoluto. Pero si alguna de las promesas
anteriores es rechazada (por un error de red, un JSON inválido or cualquier otra razón),
entonces el error es capturado.
try…catch implícito
El código de un ejecutor de promesas y de manejadores de promesas tiene embebido un
" try..catch invisible". Si ocurre una excepción, esta es atrapada y es tratada como un rechazo.
Esto sucede con todos los errores, no solo los causados por la sentencia de excepción throw .
Por ejemplo, un error de programación:
El .catch del final no solo detecta rechazos explícitos, sino también los errores accidentales
en los manejadores anteriores.
En un try..catch normal, podemos analizar el error y quizá volver a lanzarlo si no se puede
manejar. Lo mismo podemos hacer con las promesas.
Si hacemos throw dentro de .catch , el control pasa a otro manejador de errores más cercano. Y,
si manejamos el error y terminamos de forma correcta, entonces se continúa con el siguiente
manejador .then exitoso.
}).catch(function(error) {
Aquí el .catch termina de forma correcta. Entonces se ejecuta el siguiente manejador
exitoso .then .
En el siguiente ejemplo podemos ver otra situación con .catch . El manejador (*) detecta el
error y simplemente no puede manejarlo (en el ejemplo solo sabe que hacer con un URIError ), por
}).catch(function(error) { // (*)
throw error; // Lanza este error u otro error que salte en el siguiente catch.}
}).then(function() {
/* Esto no se ejecuta */
}).catch(error => { // (**)
});
La ejecución salta del primer .catch (*) al siguiente (**) en la cadena.
Rechazos no manejados
¿Que sucede cuanto un error no es manejado? Por ejemplo, si olvidamos agregar .catch al final
de una cadena de promesas, como aquí:
new Promise(function() {
noSuchFunction(); // Aquí hay un error (no existe la función)
})
.then(() => {
// manejador de una o más promesas exitosas
}); // sin .catch al final!
window.addEventListener('unhandledrejection', function(event) {
// el objeto event tiene dos propiedades especiales:
alert(event.promise); // [objeto Promesa] - La promesa que fue rechazada
alert(event.reason); // Error: Whoops! - Motivo por el cual se rechaza la promesa
});new Promise(function() {
throw new Error("Whoops!");
}); // No hay un .catch final para manejar el error
Resumen
.catch maneja errores de todo tipo: ya sea una llamada a reject() , o un error que arroja un
manejador.
.then también atrapa los errores de la misma manera si se le da el segundo argumento (que
es el manejador de error).
Debemos colocar .catch exactamente en los lugares donde queremos manejar los errores y
saber cómo manejarlos. El manejador debe analizar los errores (los errores personalizados
ayudan), y relanzar los errores desconocidos (tal vez sean errores de programación).
Promise API
Hay 6 métodos estáticos en la clase Promise . Veremos sus casos de uso aquí.
Promise.all
Digamos que queremos que muchas promesas se ejecuten en paralelo y esperar hasta que todas
ellas estén listas.
Por ejemplo, descargar varias URLs en paralelo y procesar su contenido en cuanto todas ellas
finalicen.
Para ello es Promise.all .
La sintaxis es:
Promise.all toma un iterable (usualmente un array de promesas) y devuelve una nueva promesa.
Esta nueva promesa es resuelta en cuanto todas las promesas listadas se resuelven, y el array
de aquellos resultados se vuelve su resultado.
Por ejemplo, el Promise.all debajo se resuelve después de 3 segundos, y su resultado es un
array [1, 2, 3] :
Promise.all([
new Promise(resolve => setTimeout(() => resolve(1), 3000)), // 1
new Promise(resolve => setTimeout(() => resolve(2), 2000)), // 2
new Promise(resolve => setTimeout(() => resolve(3), 1000)) // 3
]).then(alert); // 1,2,3 cuando las promesas están listas: cada promesa constituye un miembro del array
Ten en cuenta que el orden de los miembros del array es el mismo que el de las promesas que
los originan. Aunque la primera promesa es la que toma más tiempo en resolverse, es aún la
primera en el array de resultados.
let urls = [
'https://api.github.com/users/iliakan',
'https://api.github.com/users/remy',
Un mayor ejemplo con fetch: la búsqueda de información de usuario para un array de usuarios
de GitHub por sus nombres (o podríamos buscar un array de bienes por sus “id”, la lógica es
idéntica):
Promise.all(requests)
.then(responses => {
// todas las respuestas son resueltas satisfactoriamente
for(let response of responses) {
alert(`${response.url}: ${response.status}`); // muestra 200 por cada url
}
return responses;
})
// mapea el array de resultados dentro de un array de response.json() para leer sus contenidos
.then(responses => Promise.all(responses.map(r => r.json())))
// todas las respuestas JSON son analizadas: "users" es el array de ellas
.then(users => users.forEach(user => alert(user.name)));
Promise.all([
new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)),new Promise((resolve, reject) => setTimeout((
]).catch(alert); // Error: Whoops!
Aquí la segunda promesa se rechaza en dos segundos. Esto lleva a un rechazo inmediato
de Promise.all , entonces .catch se ejecuta: el error del rechazo se vuelve la salida
del Promise.all entero.
En caso de error, las demás promesas son ignoradas
Si una promesa se rechaza, Promise.all se rechaza inmediatamente, olvidando completamente las
otras de la lista. Aquellos resultados son ignorados.
Por ejemplo: si hay múltiples llamados fetch , como en el ejemplo arriba, y uno falla, los
demás aún continuarán en ejecución, pero Promise.all no las observará más. Ellas probablemente
respondan, pero sus resultados serán ignorados.
Promise.all no hace nada para cancelarlas, no existe un concepto de “cancelación” en las
promesas. En otro capítulo veremos AbortController , que puede ayudar con ello pero no es parte de
la API de las promesas.
Promise.all(iterable) permite valores “comunes” que no sean promesas en iterable
Promise.all([
new Promise((resolve, reject) => {
setTimeout(() => resolve(1), 1000)
Promise.allSettled
Una adición reciente
Esta es una adición reciente al lenguaje. Los navegadores antiguos pueden necesitar
polyfills.
Promise.all rechaza como un todo si cualquiera de sus promesas es rechazada. Esto es bueno para
los casos de “todo o nada”, cuando necesitamos que todos los resultados sean exitosos para
proceder:
Promise.all([
fetch('/template.html'),
fetch('/style.css'),
fetch('/data.json')
]).then(render); // el método render necesita los resultados de todos los fetch
Promise.allSettled solo espera que todas las promesas se resuelvan sin importar sus resultados. El
let urls = [
'https://api.github.com/users/iliakan',
'https://api.github.com/users/remy',
'https://no-such-url'
];
[
{status: 'fulfilled', value: ...response...},
{status: 'fulfilled', value: ...response...},
{status: 'rejected', reason: ...error object...}
]
Polyfill
Si el browser no soporta Promise.allSettled , es fácil implementarlo:
if (!Promise.allSettled) {
const rejectHandler = reason => ({ status: 'rejected', reason });
En este código, promises.map toma los valores de entrada, los transforma en promesas (por si no
lo eran) con p => Promise.resolve(p) , entonces agrega un manejador .then a cada una.
Este manejador (“handler”) transforma un resultado exitoso value en {status:'fulfilled', value} , y un
error reason en {status:'rejected', reason} . Ese es exactamente el formato de Promise.allSettled .
Ahora podemos usar Promise.allSettled para obtener el resultado de todas las promesas dadas
incluso si algunas son rechazadas.
Promise.race
Similar a Promise.all , pero espera solamente por la primera respuesta y obtiene su resultado (o
error).
Su sintaxis es:
Promise.race([
new Promise((resolve, reject) => setTimeout(() => resolve(1), 1000)),
new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 2000)),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).then(alert); // 1
La primera promesa fue la más rápida, por lo que se vuelve resultado. En cuanto una promesa
responde, “gana la carrera”, y todos los resultados o errores posteriores son ignorados.
Promise.any
Es similar a Promise.race , pero espera solamente por la primera promesa cumplida y obtiene su
resultado. Si todas la promesas fueron rechazadas, entonces la promesa que devuelve es
rechazada con AggregateError , un error especial que almacena los errores de todas las promesas en
su propiedad errors .
La sintaxis es:
Promise.any([
new Promise((resolve, reject) => setTimeout(() => reject(new Error("Whoops!")), 1000)),
new Promise((resolve, reject) => setTimeout(() => resolve(1), 2000)),
new Promise((resolve, reject) => setTimeout(() => resolve(3), 3000))
]).then(alert); // 1
La primera promesa fue la más rápida, pero fue rechazada entonces devuelve el resultado de la
segunda. Una vez que la primera promesa cumplida “gana la carrera”, los demás resultados
serán ignorados.
Aquí hay un ejemplo donde todas la promesas fallan:
Como puedes ver, los objetos de error de las promesas que fallaron están disponibles en la
propiedad errors del objeto AggregateError .
Promise.resolve/reject
Los métodos Promise.resolve y Promise.reject son raramente necesitados en código moderno porque la
sintaxis async/await (que veremos luego) las hace algo obsoletas.
Las tratamos aquí para completar la cobertura y por aquellos casos que por algún motivo no
puedan usar async/await .
Promise.resolve
Promise.resolve(value) crea una promesa resuelta con el resultado value .
Tal como:
El método es usado por compatibilidad, cuando se espera que una función devuelva una promesa.
Por ejemplo, la función loadCached abajo busca una URL y recuerda (en caché) su contenido.
Futuros llamados con la misma URL devolverá el contenido de caché, pero usa Promise.resolve para
hacer una promesa de él y así el valor devuelto es siempre una promesa:
function loadCached(url) {
if (cache.has(url)) {
return Promise.resolve(cache.get(url)); // (*)}
return fetch(url)
.then(response => response.text())
.then(text => {
cache.set(url,text);
return text;
});
}
Podemos escribir loadCached(url).then(…) porque se garantiza que la función devuelve una promesa.
Siempre podremos usar .then después de loadCached . Ese es el propósito de Promise.resolve en la
línea (*) .
Promise.reject
Promise.reject(error) crea una promesa rechazada con error .
Tal como:
Resumen
Existen 6 métodos estáticos de la clase Promise :
cumpla y devuelve su resultado. Si todas las promesas son rechazadas, AggregateError se
vuelve el error de Promise.any .
Promisificación
Veamos un ejemplo.
Aquí tenemos loadScript(src, callback) del artículo Introducción: callbacks.
document.head.append(script);
}
// uso:
// loadScript('path/script.js', (err, script) => {...})
La función carga un script con el src dado, y llama a callback(err) en caso de error
o callback(null, script) en caso de carga exitosa. Esto está ampliamente acordado en el uso de
callbacks, lo hemos visto antes.
Vamos a promisificarla.
Haremos una función nueva loadScriptPromise(src) que va a hacer lo mismo (carga el script), pero
devuelve una promesa en vez de usar callbacks.
Es decir: pasamos solamente src (sin callback ) y obtenemos una promesa de vuelta, que resuelve
con script cuando la carga fue exitosa y rechaza con error en caso contrario.
Aquí está:
// uso:
// loadScriptPromise('path/script.js').then(...)
function promisify(f) {
return function (...args) { // devuelve una función contenedora (*)
return new Promise((resolve, reject) => {
function callback(err, result) { // nuestro callback personalizado para f (**)
if (err) {
reject(err);
} else {
resolve(result);
}
}
// uso:
let loadScriptPromise = promisify(loadScript);
loadScriptPromise(...).then(...);
El código puede verse complicado, pero es esencialmente lo mismo que escribimos arriba al
promisificar la función loadScript .
Una llamada a promisify(f) devuelve una función contenedora que envuelve a f (*) . Este
contenedor devuelve una promesa y redirige el llamado a la f original, siguiendo el resultado
en el callback personalizado (**) .
Aquí promisify asume que la función original espera un callback con dos argumentos (err, result) .
Eso es lo que usualmente encontramos. Entonces nuestro callback personalizado está
exactamente en el formato correcto, y promisify funciona muy bien para tal caso.
¿Y si la f original espera un callback con más argumentos callback(err, res1, res2) ?
Podemos mejorar el ayudante. Hagamos una versión de promisify más avanzada.
Cuando la llamamos como promisify(f) , debe funcionar igual que en la versión previa.
Cuando la llamamos como promisify(f, true) , debe devolver una promesa que resuelve con el array
de resultados del callback. Esto es para callbacks con muchos argumentos.
args.push(callback);
f.call(this, ...args);
});
};
}
// Uso:
Como puedes ver es esencialmente lo mismo de antes, pero resolve es llamado con solo uno o con
todos los argumentos dependiendo del valor de manyArgs .
Para formatos más exóticos de callback, como aquellos sin err en absoluto: callback(result) ,
podemos promisificarlos manualmente sin usar el ayudante.
También hay módulos con funciones de promisificación un poco más flexibles, ej. es6-
promisify. En Node.js, hay una función integrada util.promisify para ello.
Por favor tome nota:
Así que la promisificación está solo pensada para funciones que llaman al callback una vez.
Las llamadas adicionales serán ignoradas.
Microtareas (Microtasks)
La ejecución de una tarea se inicia sólo cuando no se está ejecutando nada más.
¿y si el orden es importante para nosotros? ¿Cómo podemos hacer que código finalizado se ejecute
después de ¡promesa realizada! ?
Fácil, solo ponlo en la cola con .then :
Promise.resolve()
.then(() => alert("promesa realizada!"))
.then(() => alert("código finalizado"));
Rechazo no controlado
Recuerdas el evento unhandledrejection del artículo Manejo de errores con promesas?
Ahora podemos ver exactamente cómo Javascript descubre que hubo un rechazo no controlado
o unhandled rejection
Pero si olvidas añadir el .catch , entonces, después de que la cola de microtareas esté vacía,
el motor activa el evento:
// Promesa fallida!
window.addEventListener('unhandledrejection', event => alert(event.reason));
Resumen
El control de promesas siempre es asíncrono, ya que todas las acciones de promesa pasan por
la cola interna de “PromiseJobs”, también llamada “cola de microtareas” (término de V8).
Entonces, los controladores .then/catch/finally siempre se llaman después de que el código actual
ha finalizado.
Async/await
Existe una sintaxis especial para trabajar con promesas de una forma más confortable, llamada
“async/await”. Es sorprendentemente fácil de entender y usar.
Funciones async
Comencemos con la palabra clave async . Puede ser ubicada delante de una función como aquí:
La palabra “async” ante una función significa solamente una cosa: que la función siempre
devolverá una promesa. Otros valores serán envueltos y resueltos en una promesa
automáticamente.
Por ejemplo, esta función devuelve una promesa resuelta con el resultado de 1 ; Probémosla:
f().then(alert); // 1
f().then(alert); // 1
Entonces, async se asegura de que la función devuelva una promesa, o envuelve las no promesas
y las transforma en una. Bastante simple, ¿correcto? Pero no solo eso. Hay otra
palabra, await , que solo trabaja dentro de funciones async y es muy interesante.
Await
La sintaxis:
await hace que JavaScript espere hasta que la promesa responda y devuelve su resultado.
Aquí hay un ejemplo con una promesa que resuelve en 1 segundo:
let result = await promise; // espera hasta que la promesa se resuelva (*)alert(result); // "¡Hecho!"
}
f();
Si tratamos de usar await en una función no async, tendremos un error de sintaxis:
function f() {
let promise = Promise.resolve(1);
let result = await promise; // Syntax error}
Es posible que obtengamos este error si olvidamos poner async antes de una función. Como se
dijo, “await” solo funciona dentro de una función async .
2. También debemos hacer que la función sea async para que aquellos funcionen.
// muestra el avatar
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
// espera 3 segundos
await new Promise((resolve, reject) => setTimeout(resolve, 3000));
img.remove();
return githubUser;
}
showAvatar();
En los navegadores modernos, await de nivel superior funciona, siempre que estamos dentro de
un módulo. Cubriremos módulos en el artículo Módulos, introducción.
Por ejemplo:
console.log(user);
(async () => {
let response = await fetch('/article/promise-chaining/user.json');
let user = await response.json();
...
})();
await acepta “thenables”
Tal como promise.then , await nos permite el uso de objetos “thenable” (aquellos con el
método then ). La idea es que un objeto de terceras partes pueda no ser una promesa, sino
compatible con una: si soporta .then , es suficiente para el uso con await .
Aquí hay una demostración de la clase Thenable ; el await debajo acepta sus instancias:
class Thenable {
constructor(num) {
this.num = num;
}
then(resolve, reject) {
alert(resolve);
// resuelve con this.num*2 después de 1000ms
setTimeout(() => resolve(this.num * 2), 1000); // (*)
}
}
f();
Si await obtiene un objeto no-promesa con .then , llama tal método proveyéndole con las
funciones incorporadas resolve y reject como argumentos (exactamente como lo hace con
ejecutores Promise regulares). Entonce await espera hasta que une de ellos es llamado (en el
ejemplo previo esto pasa en la línea (*) ) y entonces procede con el resultado.
Métodos de clase Async
class Waiter {
async wait() {return await Promise.resolve(1);
}
}
new Waiter()
.wait()
.then(alert); // 1 (lo mismo que (result => alert(result)))
Manejo de Error
Si una promesa se resuelve normalmente, entonces await promise devuelve el resultado. Pero en
caso de rechazo, dispara un error, tal como si hubiera una instrucción throw en aquella línea.
Este código:
En situaciones reales, la promesa tomará algún tiempo antes del rechazo. En tal caso habrá un
retardo antes de que await dispare un error.
Podemos atrapar tal error usando try..catch , de la misma manera que con un throw normal:
try {
let response = await fetch('http://no-such-url');
} catch(err) {
alert(err); // TypeError: failed to fetch}
}
f();
En el caso de un error, el control salta al bloque catch . Podemos también envolver múltiples
líneas:
try {
let response = await fetch('/no-user-here');
let user = await response.json();
} catch(err) {
// atrapa errores tanto en fetch como en response.json
alert(err);
}
}
f();
Si no tenemos try..catch , entonces la promesa generada por el llamado de la función async f() se
vuelve rechazada. Podemos añadir .catch para manejarlo:
Cuando necesitamos esperar por múltiples promesas, podemos envolverlas en un Promise.all y
luego await :
En caso de error, se propaga como es usual, desde la promesa que falla a Promise.all , y entonces
se vuelve una excepción que podemos atrapar usando try..catch alrededor del llamado.
Resumen
El comando async antes de una función tiene dos efectos:
El comando await antes de una promesa hace que JavaScript espere hasta que la promesa
responda. Entonces:
1. Si es un error, la excepción es generada — lo mismo que si throw error fuera llamado en ese
mismo lugar.
Juntos proveen un excelente marco para escribir código asincrónico que es fácil de leer y
escribir.
Con async/await raramente necesitamos escribir promise.then/catch , pero aún no deberíamos olvidar
que están basados en promesas porque a veces (ej. como en el nivel superior de código)
tenemos que usar esos métodos. También Promise.all es adecuado cuando esperamos por varias
tareas simultáneas.
Funciones Generadoras
Para crear un generador, necesitamos una construcción de sintaxis especial: function* , la
llamada “función generadora”.
Se parece a esto:
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
Ahora el generador está listo. Deberíamos verlo desde done: true y procesar value: 3 como el
resultado final.
Las nuevas llamadas a generator.next() ya no tienen sentido. Si las hacemos, devuelven el mismo
objeto: {done: true} .
¿ function* f(…) o function *f(…) ?
Ambas sintaxis son correctas.
Pero generalmente se prefiere la primera sintaxis, ya que la estrella * denota que es una
función generadora, describe el tipo, no el nombre, por lo que debería seguir a la palabra
clave function .
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
Es porque la iteración for..of ignora el último value , cuando done: true . Entonces, si queremos
que todos los resultados se muestren con for..of , debemos devolverlos con yield :
Como los generadores son iterables, podemos llamar a todas las funciones relacionadas, p. Ej.
la sintaxis de propagación ... :
function* generateSequence() {
yield 1;
yield 2;
yield 3;
}
alert(sequence); // 0, 1, 2, 3
let range = {
from: 1,
to: 5,
Podemos utilizar una función generadora para la iteración proporcionándola como Symbol.iterator .
Este es el mismo range , pero mucho más compacto:
let range = {
from: 1,
to: 5,
Eso no es una coincidencia, por supuesto. Los generadores se agregaron al lenguaje JavaScript
con iteradores en mente, para implementarlos fácilmente.
La variante con un generador es mucho más concisa que el código iterable original de range y
mantiene la misma funcionalidad.
Eso seguramente requeriría un break (o return ) en for..of sobre dicho generador. De lo
contrario, el bucle se repetiría para siempre y se colgaría.
Ahora nos gustaría reutilizarlo para generar una secuencia más compleja:
Podemos usar esta secuencia, p. Ej. para crear contraseñas seleccionando caracteres de él
(también podría agregar caracteres de sintaxis), pero vamos a generarlo primero.
En una función regular, para combinar los resultados de muchas otras funciones, las llamamos,
almacenamos los resultados y luego nos unimos al final.
Para los generadores, hay una sintaxis especial yield* para “incrustar” (componer) un
generador en otro.
El generador compuesto:
function* generatePasswordCodes() {
// 0..9
yield* generateSequence(48, 57);
// A..Z
yield* generateSequence(65, 90);
// a..z
yield* generateSequence(97, 122);}
alert(str); // 0..9A..Za..z
La directiva yield* delega la ejecución a otro generador. Este término significa que yield*
itera sobre el generador gen y reenvía de forma transparente sus yields al exterior. Como
gen
function* generateAlphaNum() {
alert(str); // 0..9A..Za..z
Para hacerlo, deberíamos llamar a generator.next (arg) , con un argumento. Ese argumento se
convierte en el resultado de yield .
Veamos un ejemplo:
function* gen() {
// Pasar una pregunta al código externo y esperar una respuesta
let result = yield "2 + 2 = ?"; // (*)alert(result);
}
Tenga en cuenta que el código externo no tiene que llamar inmediatamente a next(4) . Puede que
lleve algún tiempo. Eso no es un problema: el generador esperará.
Por ejemplo:
function* gen() {
let ask1 = yield "2 + 2 = ?";
alert(ask1); // 4
alert(ask2); // 9
}
Imagen de la ejecución:
3. El segundo .next(4) pasa 4 de nuevo al generador como resultado del primer yield y reanuda
la ejecución.
5. El tercer next(9) pasa 9 al generador como resultado del segundo yield y reanuda la
ejecución que llega al final de la función, así que done: true .
generator.throw
Como observamos en los ejemplos anteriores, el código externo puede pasar un valor al
generador, como resultado de yield .
…Pero también puede iniciar (lanzar) un error allí. Eso es natural, ya que un error es una
especie de resultado.
Para pasar un error a un yield , deberíamos llamar a generator.throw(err) . En ese caso, el err se
coloca en la línea con ese yield .
Por ejemplo, aquí el yield de "2 + 2 = ?" conduce a un error:
function* gen() {
try {
let result = yield "2 + 2 = ?"; // (1)
function* generate() {
let result = yield "2 + 2 = ?"; // Error en esta linea
}
try {
generator.throw(new Error("La respuesta no se encuentra en mi base de datos"));
} catch(e) {
alert(e); // mostrar el error
}
generator.return
function* gen() {
yield 1;
yield 2;
yield 3;
}
const g = gen();
Si volvemos a usar generator.return() en un generator finalizado, devolverá ese valor nuevamente
(MDN).
No lo usamos a menudo, ya que la mayor parte del tiempo queremos todos los valores, pero
puede ser útil cuando queremos detener el generador en una condición específica.
Resumen
Los generadores son creados por funciones generadoras function* f(…) {…} .
En JavaScript moderno, los generadores rara vez se utilizan. Pero a veces son útiles, porque
la capacidad de una función para intercambiar datos con el código de llamada durante la
ejecución es bastante única. Y, seguramente, son geniales para hacer objetos iterables.
Además, en el próximo capítulo aprenderemos los generadores asíncronos, que se utilizan para
leer flujos de datos generados asincrónicamente (por ejemplo, recuperaciones paginadas a
través de una red) en bucles for await ... of .
En la programación web, a menudo trabajamos con datos transmitidos, por lo que ese es otro
caso de uso muy importante.
Los iteradores asíncronos nos permiten iterar sobre los datos que vienen de forma asíncrona,
en una petición. Como, por ejemplo, cuando descargamos algo por partes a través de una red. Y
los generadores asíncronos lo hacen aún más conveniente.
Veamos primero un ejemplo simple, para comprender la sintaxis y luego revisar un caso de uso
de la vida real.
Repaso de iterables
Repasemos el tópico acerca de iterables.
let range = {
from: 1,
to: 5
};
…Y queremos usar un bucle for..of en él, tal como for(value of range) , para obtener valores
desde 1 hasta 5 .
En otras palabras, queremos agregar la habilidad de iteración al objeto.
Eso puede ser implementado usando un método especial con el nombre Symbol.iterator :
Para cada iteración, el método next() es invocado para el siguiente valor.
El next() debe devolver un valor en el formato {done: true/false, value:<loop value>} ,
donde done:true significa el fin del bucle.
let range = {
from: 1,
to: 5,
next() { // llamado en cada iteración, para obtener el siguiente valorif (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
}
};
Iteradores asíncronos
La iteración asincrónica es necesaria cuando los valores vienen asincrónicamente: después
de setTimeout u otra clase de retraso.
El caso más común es un objeto que necesita hacer un pedido sobre la red para enviar el
siguiente valor, veremos un ejemplo de la vida real algo más adelante.
Para hacer un objeto iterable asincrónicamente:
2. El método next() debe devolver una promesa (a ser cumplida con el siguiente valor).
La palabra clave async lo maneja, nosotros simplemente hacemos async next() .
3. Para iterar sobre tal objeto, debemos usar un bucle for await (let item of iterable) .
Como ejemplo inicial, hagamos iterable un objeto range object, similar al anterior, pero ahora
devolverá valores asincrónicamente, uno por segundo.
Todo lo que necesitamos hacer es algunos reemplazos en el código de abajo:
let range = {
from: 1,
to: 5,
[Symbol.asyncIterator]() { // (1)return {
current: this.from,
last: this.to,
async next() { // (2) // nota: podemos usar "await" dentro de el async next:
await new Promise(resolve => setTimeout(resolve, 1000)); // (3)if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
(async () => {
2. Este método debe devolver el objeto con el método next() retornando una promesa (2) .
3. El método next() no tiene que ser async , puede ser un método normal que devuelva una
promesa, pero async nos permite usar await , entonces, es más conveniente. Aquí solo nos
demoramos un segundo. (3) .
4. Para iterar, nosotros usamos for await(let value of range) (4) , es decir, agregar “await” y
después “for”. Llama range[Symbol.asyncIterator]() una vez, y luego next() para los valores.
Repaso de generators
Ahora repasemos generators, que permiten una iteración mucho más corta. La mayoría de las
veces, cuando queramos hacer un iterable, usaremos generators.
Para simplicidad, omitiendo cosas importantes, son “funciones que generan (yield) valores”.
Son explicados en detalle en el capítulo Generadores.
Los generators son etiquetados con function* (nota el asterisco) y usa yield para generar un
valor, entonces podemos usar el bucle for..of en ellos.
Este ejemplo genera una secuencia de valores desde start hasta end :
let range = {
from: 1,
to: 5,
[Symbol.iterator]() {
Una práctica común para Symbol.iterator es devolver un generador, este hace el código más corto
como puedes ver:
let range = {
from: 1,
to: 5,
La sintaxis es simple: anteponga async a function* . Esto hace al generador asincrónico.
Entonce usamos for await (...) para iterarlo, como esto:
(async () => {
})();
Como el generador es asincrónico, podemos usar await dentro de él, contar con promesas, hacer
solicitudes de red y así.
Diferencia bajo la capa
Técnicamente, si eres un lector avanzado que recuerda los detalles de los generadores, hay
una diferencia interna.
En los generadores asincrónicos, el método generator.next() es asincrónico, devuelve promesas.
En un generador normal usaríamos result = generator.next() para obtener valores. En un generador
asíncrono debemos agregar await , así:
let range = {
from: 1,
to: 5,
yield value;
}
}
};
(async () => {
})();
Esto responde con un JSON de 30 commits, y también proporciona un enlace a la siguiente
página en la cabecera.
Entonces podemos usar ese enlace para la próxima solicitud, para obtener más commits, y
así sucesivamente.
Para nuestro código querríamos una manera más simple de obtener commits.
Hagamos una función fetchCommits(repo) que tome commits por nosotros, haciendo solicitudes cuando
sea necesario. Y dejar que se preocupe por todas las cosas de paginación. Para nosotros un
simple for await..of .
Su uso será como esto:
while (url) {
const response = await fetch(url, { // (1)
headers: {'User-Agent': 'Our script'}, // github requiere encabezado de user-agent
});
url = nextPage;
for(let commit of body) { // (4) concede commits uno por uno, hasta que termine la página
yield commit;
}
}
}
4. Luego entregamos uno por uno todos los “commit” recibidos y, cuando finalizan, se activará
la siguiente iteración while(url) haciendo una solicitud más.
(async () => {
let count = 0;
console.log(commit.author.login);
})();
// Nota: Si ejecutas este código en una caja de pruebas externa, necesitas copiar aquí la función fetchCommits descrita más arriba
next() el valor de retorno es {value:…, done: true/false} Promise que resuelve como {value:…, done: true/false}
next() el valor de retorno es {value:…, done: true/false} Promise que resuelve como {value:…, done: true/false}
En el desarrollo web, a menudo nos encontramos con flujos de datos que fluyen trozo a trozo.
Por ejemplo, descargar o cargar un archivo grande.
Podemos usar generadores asíncronos para procesar dichos datos. También es digno de mencionar
que en algunos entornos, como en los navegadores, también hay otra API llamada Streams, que
proporciona interfaces especiales para trabajar con tales flujos, para transformar los datos
y pasarlos de un flujo a otro (por ejemplo, descargar de un lugar e inmediatamente enviar a
otra parte).
Módulos
Módulos, introducción.
A medida que nuestra aplicación crece, queremos dividirla en múltiples archivos, llamados
“módulos”. Un módulo puede contener una clase o una biblioteca de funciones para un propósito
específico.
Durante mucho tiempo, JavaScript existió sin una sintaxis de módulo a nivel de lenguaje. Eso
no fue un problema, porque inicialmente los scripts eran pequeños y simples, por lo que no
era necesario.
Pero con el tiempo los scripts se volvieron cada vez más complejos, por lo que la comunidad
inventó una variedad de formas de organizar el código en módulos, bibliotecas especiales para
cargar módulos a pedido.
Para nombrar algunos (por razones históricas):
AMD – uno de los sistemas de módulos más antiguos, implementado inicialmente por la
biblioteca require.js.
UMD – un sistema de módulos más, sugerido como universal, compatible con AMD y CommonJS.
Ahora, todo esto se convierte lentamente en una parte de la historia, pero aún podemos
encontrarlos en viejos scripts.
El sistema de módulos a nivel de idioma apareció en el estándar en 2015, evolucionó
gradualmente desde entonces y ahora es compatible con todos los principales navegadores y en
Node.js. Así que estudiaremos los módulos modernos de Javascript de ahora en adelante.
Qué es un módulo?
Un módulo es solo un archivo. Un script es un módulo. Tan sencillo como eso.
La palabra clave export etiqueta las variables y funciones que deberían ser accesibles
desde fuera del módulo actual.
// 📁 sayHi.js
export function sayHi(user) {
alert(`Hello, ${user}!`);
}
// 📁 main.js
import {sayHi} from './sayHi.js';
alert(sayHi); // function...
sayHi('John'); // Hello, John!
La directiva import carga el módulo por la ruta ./sayHi.js relativo con el archivo actual, y
asigna la función exportada sayHi a la variable correspondiente.
Ejecutemos el ejemplo en el navegador.
Como los módulos admiten palabras clave y características especiales, debemos decirle al
navegador que un script debe tratarse como un módulo, utilizando el atributo <script type =" module
"> .
Asi:
Resultado
say.js
index.html
document.body.innerHTML = sayHi('John');
</script>
Hay características principales, válidas tanto para el navegador como para JavaScript del
lado del servidor.
index.html
<!doctype html><script type="module" src="user.js"></script><script type="module" src="hello.js"></script>
Los módulos deben hacer export a lo que ellos quieren que esté accesible desde afuera y
hacer import de lo que necesiten.
Aquí hay dos scripts en la misma página, ambos type="module" . No ven entre sí sus variables de
nivel superior:
<script type="module">
// La variable sólo es visible en éste script de módulo
let user = "John";
</script><script type="module">
alert(user); // Error: user no está definido</script>
//📁 alert.js
alert("Módulo es evaluado!");
// Importar el mismo módulo desde archivos distintos
//📁 1.js
import `./alert.js`; // Módulo es evaluado!
//📁 2.js
import `./alert.js`; // (no muestra nada)
//📁 admin.js
export let admin = {
name: "John"
};
Si este módulo se importa desde varios archivos, el módulo solo se evalúa la primera vez, se
crea el objeto admin y luego se pasa a todos los importadores adicionales.
//📁 1.js
import {admin} from './admin.js';
admin.name = "Pete";
//📁 2.js
import {admin} from './admin.js';
alert(admin.name); // Pete
Como puedes ver, cuando 1.js cambia la propiedad name en el admin importado,
entonces 2.js puede ver el nuevo admin.name .
Esto es porque el modulo se ejecuta solo una vez. Los exports son generados y luego
compartidos entre importadores, entonces si algo cambia en el objeto admin , otros importadores
lo verán.
Tal comportamiento es en verdad muy conveniente, porque nos permite configurar módulos.
En otras palabras, un módulo puede brindar una funcionalidad genérica que necesite ser
configurada. Por ejemplo, la autenticación necesita credenciales. Entonces se puede exportar
un objeto de configuración esperando que el código externo se lo asigne.
Aquí está el patrón clásico:
//📁 admin.js
export let config = { };
Aquí admin.js exporta el objeto config (inicialmente vacío, pero podemos tener propiedades por
defecto también).
Entonces en init.js , el primer script de nuestra app, importamos config de él y
establecemos config.user :
//📁 init.js
import {config} from './admin.js';
config.user = "Pete";
//📁 another.js
import {sayHi} from './admin.js';
import.meta
El objeto import.meta contiene la información sobre el módulo actual.
Su contenido depende del entorno. En el navegador, contiene la URL del script, o la URL de
una página web actual si está dentro de HTML:
<script type="module">
alert(import.meta.url); // script URL
// para un script inline es la URL de la página HTML actual
</script>
Compárelo con scripts que no sean módulos, donde this es un objeto global:
<script>
alert(this); // window
</script><script type="module">
alert(this); // undefined
</script>
descargar módulos externo <script type="module" src="..."> no bloquea el procesamiento de HTML, se
cargan en paralelo con otros recursos.
los módulos esperan hasta que el documento HTML esté completamente listo (incluso si son
pequeños y cargan más rápido que HTML), y luego lo ejecuta.
se mantiene el orden relativo de los scripts: los scripts que van primero en el documento,
se ejecutan primero.
Como efecto secundario, los módulos siempre “ven” la página HTML completamente cargada,
incluidos los elementos HTML debajo de ellos.
Por ejemplo:
<script type="module">
alert(typeof button); // objeto: el script puede 'ver' el botón de abajo// debido que los módulos son diferidos, el script se ejecut
</script>
<script>
alert(typeof button); // button es indefinido, el script no puede ver los elementos de abajo// los scripts normales corren inmediata
</script><button id="button">Button</button>
Note que: ¡el segundo script se ejecuta antes que el primero! Entonces vemos primero undefined ,
y después object .
Esto se debe a que los módulos están diferidos, por lo que esperamos a que se procese el
documento. El script normal se ejecuta inmediatamente, por lo que vemos su salida primero.
Al usar módulos, debemos tener en cuenta que la página HTML se muestra a medida que se carga,
y los módulos JavaScript se ejecutan después de eso, por lo que el usuario puede ver la
página antes de que la aplicación JavaScript esté lista. Es posible que algunas funciones aún
no funcionen. Deberíamos poner “indicadores de carga”, o asegurarnos de que el visitante no
se confunda con eso.
counter.count();
</script>
Scripts externos
Los scripts externos que tengan type="module" son diferentes en dos aspectos:
1. Los scripts externos con el mismo src sólo se ejecutan una vez:
2. Los scripts externos que se buscan desde otro origen (p.ej. otra sitio web) require
encabezados CORS, como se describe en el capítulo Fetch: Cross-Origin Requests. En otras
palabras, si un script de módulo es extraído desde otro origen, el servidor remoto debe
proporcionar un encabezado Access-Control-Allow-Origin permitiendo la búsqueda.
Ciertos entornos, como Node.js o herramientas de paquete permiten módulos simples sin ninguna
ruta, ya que tienen sus propias formas de encontrar módulos y hooks para ajustarlos. Pero los
navegadores aún no admiten módulos sueltos.
Compatibilidad, “nomodule”
Los navegadores antiguos no entienden type = "module" . Los scripts de un tipo desconocido
simplemente se ignoran. Para ellos, es posible proporcionar un respaldo utilizando el
atributo nomodule :
<script type="module">
alert("Ejecuta en navegadores modernos");
</script><script nomodule>
alert("Los navegadores modernos conocen tanto type=module como nomodule, así que omita esto")
alert("Los navegadores antiguos ignoran la secuencia de comandos con type=module desconocido, pero ejecutan esto.");
</script>
Herramientas de Ensamblaje
En la vida real, los módulos de navegador rara vez se usan en su forma “pura”. Por lo
general, los agrupamos con una herramienta especial como Webpack y los implementamos en el
servidor de producción.
Uno de los beneficios de usar empaquetadores – dan más control sobre cómo se resuelven los
módulos, permitiendo módulos simples y mucho más, como los módulos CSS/HTML.
Las herramientas de compilación hacen lo siguiente:
1. Toman un módulo “principal”, el que se pretende colocar en <script type="module"> en HTML.
3. Compila un único archivo con todos los módulos (o múltiples archivos, eso es ajustable),
reemplazando los llamados nativos de import con funciones del empaquetador para que
funcione. Los módulos de tipo “Especial” como módulos HTML/CSS también son supported.
La sintaxis JavaScript moderna puede transformarse en una sintaxis más antigua con una
funcionalidad similar utilizando Babel.
<!-- Asumiendo que obtenemos bundle.js desde una herramienta como Webpack -->
<script src="bundle.js"></script>
Dicho esto, los módulos nativos también se pueden utilizar. Por lo tanto no estaremos
utilizando Webpack aquí: tú lo podrás configurar más adelante.
Resumen
Para resumir, los conceptos centrales son:
1. Un módulo es un archivo. Para que funcione import/export , los navegadores necesitan <script
type="module"> . Los módulos tienen varias diferencias:
2. Los módulos tienen su propio alcance local de alto nivel y funcionalidad de intercambio a
través de ‘import/export’.
4. El código del módulo se ejecuta solo una vez. Las exportaciones se crean una vez y se
comparten entre los importadores.
Export e Import
// exportar un array
export let months = ['Jan', 'Feb', 'Mar','Apr', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
Tenga en cuenta que export antes de una clase o una función no la hace una expresión de
función. Sigue siendo una declaración de función, aunque exportada.
La mayoría de las guías de estilos JavaScript no recomiendan los punto y comas después de
declarar funciones y clases.
Es por esto que no hay necesidad de un punto y coma al final de export class y export
function:
export function sayHi(user) {
alert(`Hello, ${user}!`);
} // no ; at the end
//📁 say.js
function sayHi(user) {
alert(`Hello, ${user}!`);
}
function sayBye(user) {
alert(`Bye, ${user}!`);
}
Import *
Generalmente, colocamos una lista de lo que queremos importar en llaves import {...} , de esta
manera:
//📁 main.js
import {sayHi, sayBye} from './say.js';sayHi('John'); // Hello, John!
sayBye('John'); // Bye, John!
Pero si hay mucho para importar, podemos importar todo como un objeto utilizando import * as
//📁 main.js
import * as say from './say.js';
say.sayHi('John');
say.sayBye('John');
A primera vista, “importar todo” parece algo tan genial, corto de escribir, por qué
deberíamos listar explícitamente lo que necesitamos importar?
Pues hay algunas razones.
2. La lista explícita de importaciones ofrece una mejor visión general de la estructura del
código: qué se usa y dónde. Facilita el soporte de código y la refactorización.
Las herramientas de empaquetado modernas, como webpack y otras, construyen los módulos juntos
y optimizan la velocidad de carga. También eliminan las importaciones no usadas.
Por ejemplo, si importas import * as library desde una librería de código enorme, y usas solo unos
pocos métodos, los que no se usen no son incluidos en el paquete optimizado.
Importar “as”
También podemos utilizar as para importar bajo nombres diferentes.
Por ejemplo, importemos sayHi en la variable local hi para brevedad, e
importar sayBye como bye :
//📁 main.js
import {sayHi as hi, sayBye as bye} from './say.js';hi('John'); // Hello, John!
bye('John'); // Bye, John!
Exportar “as”
Existe un sintaxis similar para export .
Exportemos funciones como hi y bye :
//📁 say.js
...
export {sayHi as hi, sayBye as bye};
Ahora hi y bye son los nombres oficiales para desconocidos, a ser utilizados en
importaciones:
//📁 main.js
import * as say from './say.js';
Export default
En la práctica, existen principalmente dos tipos de módulos.
1. Módulos que contienen una librería, paquete de funciones, como say.js de arriba.
2. Módulos que declaran una entidad simple, por ejemplo un módulo user.js exporta
únicamente class User .
Principalmente, se prefiere el segundo enfoque, de modo que cada “cosa” reside en su propio
módulo.
Naturalmente, eso requiere muchos archivos, ya que todo quiere su propio módulo, pero eso no
es un problema en absoluto. En realidad, la navegación de código se vuelve más fácil si los
archivos están bien nombrados y estructurados en carpetas.
Los módulos proporcionan una sintaxis especial ‘export default’ (“la exportación
predeterminada”) para que la forma de “una cosa por módulo” se vea mejor.
Poner export default antes de la entidad a exportar:
//📁 user.js
export default class User { // sólo agregar "default"
// 📁 main.js
import User from './user.js'; // no {User}, sólo User
new User('John');
Las importaciones sin llaves se ven mejor. Un error común al comenzar a usar módulos es
olvidarse de las llaves. Entonces, recuerde, import necesita llaves para las exportaciones con
nombre y no las necesita para la predeterminada.
No dar un nombre está bien, porque solo hay un “export default” por archivo, por lo que
“import” sin llaves sabe qué importar.
Sin default , dicha exportación daría un error:
El nombre “default”
En algunas situaciones, la palabra clave default se usa para hacer referencia a la exportación
predeterminada.
Por ejemplo, para exportar una función por separado de su definición:
function sayHi(user) {
alert(`Hello, ${user}!`);
}
//📁 user.js
export default class User {
constructor(name) {
this.name = name;
}
}
Aquí la manera de importar la exportación predeterminada junto con la exportación con nombre:
//📁 main.js
import {default as User, sayHi} from './user.js';
new User('John');
Y por último, si importamos todo * como un objeto, entonce la propiedad default es exactamente
la exportación predeterminada:
//📁 main.js
import * as user from './user.js';
Las exportaciones con nombre nos obligan a usar exactamente el nombre correcto para importar:
…Mientras que para una exportación predeterminada siempre elegimos el nombre al importar:
Por lo tanto, los miembros del equipo pueden usar diferentes nombres para importar lo mismo,
y eso no es bueno.
Por lo general, para evitar eso y mantener el código consistente, existe una regla que
establece que las variables importadas deben corresponder a los nombres de los archivos, por
ejemplo:
auth/
index.js
user.js
helpers.js
tests/
login.js
providers/
github.js
facebook.js
...
Nos gustaría exponer la funcionalidad del paquete a través de un único punto de entrada.
En otras palabras, una persona que quiera usar nuestro paquete, debería importar solamente el
archivo principal auth/index.js .
Como esto:
// 📁 auth/index.js
// importar login/logout e inmediatamente exportarlas
import {login, logout} from './helpers.js';
export {login, logout};
Ahora los usuarios de nuestro paquete pueden hacer esto import {login} from "auth/index.js" .
La sintaxis export ... from ... es solo una notación más corta para tal importación-exportación:
// 📁auth/index.js
// re-exportar login/logout
export {login, logout} from './helpers.js';
La diferencia notable de export ... from comparado a import/export es que los módulos re-exportados
no están disponibles en el archivo actual. Entonces en el ejemplo anterior de auth/index.js no
podemos usar las funciones re-exportadas login/logout .
// 📁 user.js
export default class User {
// ...
}
1. export User from './user.js' no funcionará. Nos dará un error de sintaxis.
Para reexportar la exportación predeterminada, tenemos que escribir export {default as User} , tal
como en el ejemplo de arriba.
exportación predeterminada.
Si queremos reexportar tanto la exportación con nombre como la predeterminada, se
necesitan dos declaraciones:
Tales rarezas de reexportar la exportación predeterminada son una de las razones por las que
a algunos desarrolladores no les gustan las exportaciones predeterminadas y prefieren
exportaciones con nombre.
Resumen
Aquí están todos los tipos de ‘exportación’ que cubrimos en este y en artículos anteriores.
Puede comprobarlo al leerlos y recordar lo que significan:
Export independiente:
Reexportar:
Importa todo:
Importa el módulo (su código se ejecuta), pero no asigna ninguna de las exportaciones a
variables:
import "module"
Podemos poner las declaraciones import/export en la parte superior o inferior de un script, eso
no importa.
sayHi();
// ...
if (something) {
import {sayHi} from "./say.js"; // Error: import debe estar en nivel superior
}
Importaciones dinámicas
La ruta del módulo debe ser una cadena primitiva, no puede ser una llamada de función. Esto
no funcionará:
if(...) {
import ...; // ¡Error, no permitido!
}
{
import ...; // Error, no podemos poner importación en ningún bloque.
}
Esto se debe a que import / export proporcionan una columna vertebral para la estructura del
código. Eso es algo bueno, ya que la estructura del código se puede analizar, los módulos se
pueden reunir y agrupar en un archivo mediante herramientas especiales, las exportaciones no
utilizadas se pueden eliminar (“tree-shaken”). Eso es posible solo porque la estructura de
las importaciones/exportaciones es simple y fija.
Pero, ¿cómo podemos importar un módulo dinámicamente, a petición?
La expresión import()
La expresión import(module) carga el módulo y devuelve una promesa que se resuelve en un objeto
de módulo que contiene todas sus exportaciones. Se puede llamar desde cualquier lugar del
código.
Podemos usarlo dinámicamente en cualquier lugar del código, por ejemplo:
import(modulePath)
.then(obj => <module object>)
.catch(err => <loading error, e.g. if no such module>)
O, podríamos usar let module = await import(modulePath) si está dentro de una función asíncrona.
Por ejemplo, si tenemos el siguiente módulo say.js :
// 📁 say.js
export function hi() {
alert(`Hola`);
}
hi();
bye();
// 📁 say.js
export default function() {
alert("Módulo cargado (export default)!");
}
…Luego, para acceder a él, podemos usar la propiedad default del objeto del módulo:
<!doctype html><script>
async function load() {
let say = await import('./say.js');
say.hi(); // ¡Hola!
say.bye(); // ¡Adiós!
say.default(); // Módulo cargado (export default)!
}
</script><button onclick="load()">Click me</button>
Temas diversos
Proxy y Reflect
Un objeto Proxy envuelve (es un “wrapper”: envoltura, contenedor) a otro objeto e intercepta
sus operaciones (como leer y escribir propiedades, entre otras). El proxy puede manejar estas
operaciones él mismo o, en forma transparente permitirle manejarlas al objeto envuelto.
Los proxys son usados en muchas librerías y en algunos frameworks de navegador. En este
artículo veremos muchas aplicaciones prácticas.
Proxy
La sintaxis:
let proxy = new Proxy(target, handler)
operaciones. Ejemplos, la trampa get para leer una propiedad de target , la trampa set para
escribir una propiedad en target , entre otras.
Cuando hay una operación sobre proxy , este verifica si hay una trampa correspondiente
en handler . Si la trampa existe se ejecuta y el proxy tiene la oportunidad de manejarla, de
otro modo la operación es ejecutada por target .
Como ejemplo para comenzar, creemos un proxy sin ninguna trampa:
Como no hay trampas, todas las operaciones sobre proxy son redirigidas a target .
Como podemos ver, sin ninguna trampa, proxy es un envoltorio transparente alrededor de target .
Proxy es un “objeto exótico” especial. No tiene propiedades propias. Con un manejador
Invariantes
JavaScript impone algunas invariantes: condiciones que deben ser satisfechas por métodos
internos y trampas.
La mayor parte de ellos son para devolver valores:
[[Set]] debe devolver true si el valor fue escrito correctamente, de otro modo false .
[[Delete]] debe devolver true si el valor fue borrado correctamente, de otro modo false .
[[GetPrototypeOf]] , aplicado al proxy, debe devolver el mismo valor que [[GetPrototypeOf]] aplicado
al “target” del proxy. En otras palabras, leer el prototipo de un proxy debe devolver
siempre el prototipo de su objeto target.
Las trampas pueden interceptar estas operaciones, pero deben seguir estas reglas.
Las invariantes aseguran un comportamiento correcto y consistente de características de
lenguaje. La lista completa de invariantes está en la especificación. Probablemente no las
infringirás si no estás haciendo algo retorcido.
Veamos cómo funciona en ejemplos prácticos.
Para interceptar una lectura, el handler debe tener un método get(target, property, receiver) .
Se dispara cuando una propiedad es leída, con los siguientes argumentos:
receiver – si la propiedad objetivo es un getter, el receiver es el objeto que va a ser usado
como this en su llamado. Usualmente es el objeto proxy mismo (o un objeto que hereda de él,
si heredamos desde proxy). No necesitamos este argumento ahora mismo, así que se verá en
más detalle luego.
alert( numbers[1] ); // 1
alert( numbers[123] ); // 0 (porque no existe tal ítem)
Como podemos ver, es muy fácil de hacer con una trampa get .
Podemos usar Proxy para implementar cualquier lógica para valores “por defecto”.
Supongamos que tenemos un diccionario con frases y sus traducciones:
let dictionary = {
'Hello': 'Hola',
'Bye': 'Adiós'
};
Por ahora, si no existe la frase, la lectura de dictionary devuelve undefined . Pero en la
práctica dejar la frase sin traducir es mejor que undefined . Así que hagamos que devuelva la
frase sin traducir en vez de undefined .
let dictionary = {
'Hello': 'Hola',
'Bye': 'Adiós'
};
El proxy debe reemplazar completamente al objeto “target” que envolvió: nadie debe jamás
hacer referencia al objeto target saltando tal envoltura. De otro modo sería fácil
desbaratarlo.
La trampa set debe devolver true si la escritura fue exitosa, y false en caso contrario
(dispara TypeError ).
Usémoslo para validar valores nuevos:
numbers.push("test"); // TypeError ('set' en el proxy devolvió false)alert("Esta linea nunca es alcanzada (error en la línea de arriba
No necesitamos sobrescribir métodos de valor añadido como push , unshift y demás para agregar
los chequeos allí, porque internamente ellos usan la operación [[Set]] que es interceptada por
el proxy.
Entonces el código es limpio y conciso.
No olvides devolver true
Como dijimos antes, hay invariantes que se deben mantener.
Para set , debe devolver true si la escritura fue correcta.
for..in itera sobre claves no symbol con el indicador enumerable , y también claves
prototípicas.
let user = {
name: "John",
age: 30,
_password: "***"
};
let user = { };
let user = { };
});
alert( Object.keys(user) ); // a, b, c
Tomemos nota de nuevo: solamente necesitamos interceptar [[GetOwnProperty]] si la propiedad está
ausente en el objeto.
let user = {
name: "John",
_password: "secreto"
};
alert(user._password); // secreto
Usemos proxy para prevenir cualquier acceso a propiedades que comienzan con _ .
Necesitaremos las trampas:
ownKeys para excluir propiedades que comienzan con _ de for..in y métodos como Object.keys .
Aquí el código:
let user = {
name: "John",
_password: "***"
};
get(target, prop) {
// ...
let value = target[prop];
return (typeof value === 'function') ? value.bind(target) : value; // (*)}
La razón es que los métodos de objeto, como user.checkPassword() , deben ser capaces de acceder
a _password :
user = {
// ...
checkPassword(value) {
// método de objeto debe poder leer _password
return value === this._password;
}
}
Un llamado a user.checkPassword() hace que el objeto target user sea this (el objeto antes del
punto se vuelve this ), entonces cuando trata de acceder a this._password , la trampa get se activa
(se dispara en cualquier lectura de propiedad) y arroja un error.
Entonces vinculamos (bind) el contexto de los métodos al objeto original, target , en la
línea (*) . Así futuros llamados usarán target como this , sin trampas.
Esta solución usualmente funciona, pero no es ideal, porque un método podría pasar el objeto
original hacia algún otro lado y lo habremos arruinado: ¿dónde está el objeto original, y
dónde el apoderado?
Además, un objeto puede ser envuelto por proxys muchas veces (proxys múltiples pueden agregar
diferentes ajustes al objeto), y si pasamos un objeto no envuelto por proxy a un método,
puede haber consecuencias inesperadas.
Por lo tanto, tal proxy no debería usarse en todos lados.
let range = {
start: 1,
end: 10
};
Queremos usar el operador in para verificar que un número está en el rango, range .
La trampa has intercepta la llamada in .
has(target, property)
Aquí el demo:
let range = {
start: 1,
end: 10
};
En ese artículo lo hicimos sin proxy. Un llamado a delay(f, ms) devolvía una función que
redirigía todos los llamados a f después de ms milisegundos.
Aquí la version previa, implementación basada en función:
// después de esta envoltura, los llamados a sayHi serán demorados por 3 segundos
sayHi = delay(sayHi, 3000);
Como ya hemos visto, esto mayormente funciona. La función envoltorio (*) ejecuta el llamado
después del lapso.
Pero una simple función envoltura (wrapper) no redirige operaciones de lectura y escritura ni
ninguna otra cosa. Una vez envuelta, el acceso a las propiedades de la función original
( name , length ) se pierde:
function sayHi(user) {
alert(`Hello, ${user}!`);
}
El Proxy es mucho más poderoso, porque redirige todo lo que no maneja al objeto envuelto
“target”.
Usemos Proxy en lugar de una función envoltura:
function sayHi(user) {
alert(`Hello, ${user}!`);
}
alert(sayHi.length); // 1 (*) el proxy redirige la operación "get length" al objeto targetsayHi("John"); // Hello, John! (después de 3
El resultado es el mismo, pero ahora no solo las llamadas sino todas las operaciones son
redirigidas a la función original. Así sayHi.length se devuelve correctamente luego de la
envoltura en la línea (*) .
Obtuvimos una envoltura “enriquecida”.
Existen otras trampas. La lista completa está en el principio de este artículo. Su patrón de
uso es similar al de arriba.
Reflect
Reflect es un objeto nativo que simplifica la creación de Proxy .
Se dijo previamente que los métodos internos como [[Get]] , [[Set]] son únicamente para la
especificación, que no pueden ser llamados directamente.
El objeto Reflect hace de alguna manera esto posible. Sus métodos son envoltorios mínimos
alrededor del método interno.
Aquí hay ejemplos de operaciones y llamados a Reflect que hacen lo mismo:
… … …
Por ejemplo:
alert(user.name); // John
En particular, Reflect nos permite llamar a los operadores ( new , delete , …) como funciones
( Reflect.construct , Reflect.deleteProperty , …). Esta es una capacidad interesante, pero hay otra cosa
importante.
Para cada método interno atrapable por Proxy , hay un método correspondiente en Reflect con el
mismo nombre y argumentos que la trampa Proxy .
Entonces podemos usar Reflect para redirigir una operación al objeto original.
En este ejemplo, ambas trampas get y set transparentemente (como si no existieran) reenvían
las operaciones de lectura y escritura al objeto, mostrando un mensaje:
let user = {
name: "John",
};
Aquí:
Reflect.set escribe una propiedad de objeto y devuelve true si fue exitosa, false si no lo
fue.
Eso es todo, así de simple: si una trampa quiere dirigir el llamado al objeto, es suficiente
con el llamado a Reflect.<method> con los mismos argumentos.
En la mayoría de los casos podemos hacerlo sin Reflect , por ejemplo, leer una
propiedad Reflect.get(target, prop, receiver) puede ser reemplazado por target[prop] . Aunque hay
importantes distinciones.
Proxy en un getter
Veamos un ejemplo que demuestra por qué Reflect.get es mejor. Y veremos también por
qué get/set tiene el tercer argumento receiver que no usamos antes.
let user = {
_name: "Guest",
get name() {
return this._name;
La trampa get es “transparente” aquí, devuelve la propiedad original, y no hace nada más.
Esto es suficiente para nuestro ejemplo.
Todo se ve bien. Pero hagamos el ejemplo un poco más complejo.
Después de heredar otro objeto admin desde user , podemos observar el comportamiento
incorrecto:
let user = {
_name: "Guest",
get name() {
return this._name;
}
};
let admin = {
__proto__: userProxy,
_name: "Admin"
};
// Esperado: Admin
alert(admin.name); // salida: Guest (?!?)
1. Cuando leemos admin.name , como el objeto admin no tiene su propia propiedad, la búsqueda va a
su prototipo.
3. Cuando se lee la propiedad name del proxy, se dispara su trampa get y devuelve desde el
objeto original como target[prop] en la línea (*) .
Para arreglar estas situaciones, necesitamos receiver , el tercer argumento de la trampa get .
Este mantiene el this correcto para pasarlo al getter. Que en nuestro caso es admin .
¿Cómo pasar el contexto para un getter? Para una función regular podemos usar call/apply , pero
es un getter, no es “llamado”, solamente accedido.
Reflect.get hace eso. Todo funcionará bien si lo usamos.
Aquí la variante corregida:
let user = {
_name: "Guest",
get name() {
return this._name;
}
};
let admin = {
__proto__: userProxy,
_name: "Admin"
};
alert(admin.name); // Admin
Ahora receiver , que mantiene una referencia al this correcto (que es admin ), es pasado al getter
usando Reflect.get en la línea (*) .
Podemos reescribir la trampa aún más corta:
Los llamados de Reflect fueron nombrados exactamente igual a las trampas y aceptan los mismos
argumentos. Fueron específicamente diseñados así.
Entonces, return Reflect... brinda una forma segura y “no cerebral” de redirigir la operación y
asegurarse de que no olvidamos nada relacionado a ello.
Bueno, hay un problema. Cuando se envuelve un objeto nativo el proxy no tiene acceso a estos
slots internos, entonces los métodos nativos fallan.
Por ejemplo:
Internamente, un Map almacena todos los datos en su slot interno [[MapData]] . El proxy no tiene
tal slot. El método nativo Map.prototype.set trata de acceder a la propiedad interna this.[[MapData]] ,
pero como this=proxy , no puede encontrarlo en proxy y simplemente falla.
Afortunadamente, hay una forma de arreglarlo:
proxy.set('test', 1);
alert(proxy.get('test')); // 1 (¡Funciona!)
Campos privados
Algo similar ocurre con los “campos privados” usados en las clases.
Por ejemplo, el método getName() accede a la propiedad privada #name y falla cuando lo
proxificamos:
class User {
#name = "Guest";
getName() {
return this.#name;
}
}
alert(user.getName()); // Error
La razón es que los campos privados son implementados usando slots internos. JavaScript no
usa [[Get]]/[[Set]] cuando accede a ellos.
En la llamada a getName() , el valor de this es el proxy user que no tiene el slot con campos
privados.
class User {
#name = "Guest";
getName() {
return this.#name;
}
}
alert(user.getName()); // Guest
Dicho esto, la solución tiene su contra, explicada previamente: expone el objeto original al
método, potencialmente permite ser pasado más allá y dañar otra funcionalidad del proxy.
Proxy != target
El proxy y el objeto original son objetos diferentes. Es natural, ¿cierto?
Así que si usamos el objeto original como clave y luego lo hacemos proxy, entonces el proxy
no puede ser hallado:
class User {
constructor(name) {
this.name = name;
allUsers.add(this);
}
}
alert(allUsers.has(user)); // true
alert(allUsers.has(user)); // false
Como podemos ver, después del proxy no podemos hallar user en el set allUsers porque el proxy
es un objeto diferente.
El proxy no puede interceptar un test de igualdad estricta ===
Los proxys pueden interceptar muchos operadores; tales
como new (con construct ), in (con has ), delete (con deleteProperty ) y otros.
Pero no hay forma de interceptar un test de igualdad estricta entre objetos. Un objeto es
estrictamente igual únicamente a sí mismo y a ningún otro valor.
Por lo tanto todas las operaciones y clases nativas que hacen una comparación estricta de
objetos diferenciarán entre el objeto original y su proxy. No hay reemplazo transparente
aquí…
Proxy revocable
Un proxy revocable es uno que puede ser deshabilitado.
Digamos que tenemos un recurso al que quisiéramos poder cerrar en cualquier momento.
Podemos envolverlo en un proxy revocable sin trampas. Tal proxy dirigirá todas las
operaciones al objeto, y podemos deshabilitarlo en cualquier momento.
La sintaxis es:
La llamada devuelve un objeto con el proxy y la función revoke para deshabilitarlo.
Aquí hay un ejemplo:
let object = {
data: "datos valiosos"
};
La llamada a revoke() quita al proxy todas las referencias internas hacia el objeto target, ya
no estarán conectados.
En principio revoke está separado de proxy , así que podemos pasar proxy alrededor mientras
mantenemos revoke en la vista actual.
También podemos vincular el método revoke al proxy asignándolo como propiedad: proxy.revoke =
revoke .
revokes.set(proxy, revoke);
Usamos WeakMap en lugar de Map aquí porque no bloqueará la recolección de basura. Si el objeto
proxy se vuelve inalcanzable (es decir, ya ninguna variable hace referencia a
él), WeakMap permite eliminarlo junto con su revoke que no necesitaremos más.
References
Specification: Proxy.
MDN: Proxy.
Resumen
Proxy es un envoltorio (wrapper) alrededor de un objeto que redirige las operaciones en el
hacia el objeto, opcionalmente atrapando algunas de ellas para manejarlas por su cuenta.
…Entonces deberíamos usar proxy en todos lados en lugar de target . Un proxy no tiene sus
propias propiedades o métodos. Atrapa una operación si la trampa correspondiente le es
provista, de otro modo la reenvía al objeto target .
Podemos atrapar:
Muchas otras operaciones (la lista completa al principio del artículo y en docs).
Esto nos permite crear propiedades y métodos “virtuales”, implementar valores por defecto,
objetos observables, decoradores de función y mucho más.
También podemos atrapar un objeto múltiples veces en proxys diferentes, decorándolos con
varios aspectos de funcionalidad.
La API de Reflect está diseñada para complementar Proxy. Para cada trampa de Proxy hay una
llamada Reflect con los mismos argumentos. Deberíamos usarlas para redirigir llamadas hacia los
objetos target.
Los proxys tienen algunas limitaciones:
Los objetos nativos tienen “slots internos” a los que el proxy no tiene acceso. Ver la
forma de sortear el problema más arriba.
Performance: los tests de velocidad dependen del motor, pero generalmente acceder a una
propiedad usando el proxy más simple el tiempo se multiplica unas veces. Aunque en la
práctica esto solo es importante para los objetos que son los “cuello de botella” de una
aplicación.
Por ejemplo:
Una cadena de código puede ser larga, contener cortes de línea, declaración de funciones,
variables y así.
El código evaluado es ejecutado en el entorno léxico presente, entonces podemos ver sus
variables externas:
let a = 1;
function f() {
let a = 2;
eval('alert(a)'); // 2}
f();
let x = 5;
eval("x = 10");
alert(x); // 10, valor modificado
En modo estricto, eval tiene su propio entorno léxico. Entonces funciones y variables
declaradas dentro de eval no son visibles fuera:
// recordatorio: 'use strict' está habilitado en los ejemplos ejecutables por defecto
Sin use strict , eval no tiene su propio entorno léxico, entonces podemos ver x y f afuera.
Usando “eval”
En programación moderna eval es usado muy ocasionalmente. Se suele decir que “eval is evil” –
juego de palabras en inglés que significa en español: “eval es malvado”.
La razón es simple: largo, largo tiempo atrás JavaScript era un lenguaje mucho más débil,
muchas cosas podían ser concretadas solamente con eval . Pero aquel tiempo pasó hace una
década.
Ahora casi no hay razones para usar eval . Si alguien lo está usando, hay buena chance de que
pueda ser reemplazado con una construcción moderna del lenguaje o un Módulo JavaScript.
Por favor ten en cuenta que su habilidad para acceder a variables externas tiene efectos
colaterales.
Los Code minifiers (minimizadores de código, herramientas usadas antes de poner JS en
producción para comprimirlo) renombran las variables locales acortándolas (como a , b etc)
para achicar el código. Usualmente esto es seguro, pero no si eval es usado porque las
variables locales pueden ser accedidas desde la cadena de código evaluada. Por ello los
minimizadores no hacen tal renombrado en todas las variables potencialmente visibles por eval .
Esto afecta negativamente en el índice de compresión.
El uso de variables locales dentro de eval es también considerado una mala práctica de
programación, porque hace el mantenimiento de código más difícil.
Hay dos maneras de estar asegurado frente a tales problemas.
Si el código evaluado no usa variables externas, por favor llama eval como window.eval(...) :
let x = 1;
{
let x = 5;
window.eval('alert(x)'); // 1 (variable global)
}
Si el código evaluado necesita variables locales, cambia eval por new Function y pásalas como
argumentos:
f(5); // 5
La construcción new Function es explicada en el capítulo La sintaxis "new Function". Esta crea
una función desde una cadena, también en el entorno global, y así no puede ver las variables
locales. Pero es mucho más claro pasarlas explícitamente como argumentos como en el ejemplo
de arriba.
Resumen
Un llamado a eval(code) ejecuta la cadena de código y devuelve el resultado de la última
sentencia.
Puede acceder a variables locales externas. Esto es considerado una mala práctica.
Currificación
alert( curriedSum(1)(2) ); // 3
function sum(a, b) {
return a + b;
}
¡Pongámosle curry!
Ahora podemos hacer fácilmente una función conveniente para los registros actuales:
// uso
logNow("INFO", "message"); // [HH: mm] mensaje INFO
Ahora logNow es log con un primer argumento fijo, en otras palabras, “función parcialmente
aplicada” o “parcial” para abreviar.
Podemos ir más allá y hacer una función conveniente para los registros de depuración
actuales:
Entonces:
1. No perdimos nada después del curry: log todavía se puede llamar normalmente.
function curry(func) {
Ejemplos de uso:
function sum(a, b, c) {
return a + b + c;
}
1. Si el recuento de args pasado es el mismo que tiene la función original en su definición
( func.length ), entonces simplemente páselo usando func.apply .
Luego, en una nueva llamada, nuevamente obtendremos un nuevo parcial (si no hay suficientes
argumentos) o, finalmente, el resultado.
Solo funciones de longitud fija
El currying requiere que la función tenga un número fijo de argumentos.
Una función que utiliza múltiples parámetros, como f(...args) , no se puede currificar.
Un poco más que curry
Por definición, el curry debería convertir sum(a, b, c) en sum(a)(b)(c) .
Resumen
Currificación es una transformación que hace que f(a, b, c) sea invocable como f(a)(b)(c) . Las
implementaciones de JavaScript generalmente mantienen la función invocable normalmente y
devuelven el parcial si el conteo de argumentos no es suficiente.
La currificación nos permite obtener parciales fácilmente. Como hemos visto en el ejemplo de
registro, después de aplicar currificación a la función universal de tres argumentos log(fecha,
importancia, mensaje) nos da parciales cuando se llama con un argumento (como log(fecha) ) o dos
Tipo de Referencia
let user = {
name: "John",
hi() { alert(this.name); },
bye() { alert("Bye"); }
};
user.hi(); // Funciona
Como puedes ver, la llamada resulta en un error porque el valor de "this" dentro de la llamada
se convierte en undefined .
Esto funciona (objeto, punto, método):
user.hi();
¿Por qué? Si queremos entender por qué pasa esto vayamos bajo la tapa de cómo funciona la
llamada obj.method() .
Entonces ¿cómo es trasladada la información de this de la primera parte a la segunda?
Si ponemos estas operaciones en líneas separadas, entonces this se perderá con seguridad:
let user = {
name: "John",
hi() { alert(this.name); }
};
Aquí hi = user.hi coloca la función dentro de una variable y luego la última linea es
completamente independiente, por lo tanto no hay this .
Para hacer que la llamada user.hi() funcione, JavaScript usa un truco: el punto '.' no devuelve
una función, sino un valor especial del Tipo de referencia.
El Tipo de Referencia es un “tipo de especificación”. No podemos usarla explícitamente, pero
es usada internamente por el lenguaje.
El valor del Tipo de Referencia es una combinación de triple valor (base, name, strict) , donde:
El resultado de un acceso a la propiedad user.hi no es una función, sino un valor de Tipo de
Referencia. Para user.hi en modo estricto esto es:
Cuando son llamados los paréntesis () en el tipo de referencia, reciben la información
completa sobre el objeto y su método, y pueden establecer el this correcto ( user en este
caso).
Resumen
El Tipo de Referencia es un tipo interno del lenguaje.
Leer una propiedad como las que tienen un punto . en obj.method() no devuelve exactamente el
valor de la propiedad, sino un valor especial de “tipo de referencia” que almacena tanto el
valor de la propiedad como el objeto del que se tomó.
Eso se hace para la llamada () al siguiente método para obtener el objeto y
establecer this en él.
Para todas las demás operaciones, el tipo de referencia se convierte automáticamente en el
valor de la propiedad (una función en nuestro caso).
Toda la mecánica está oculta a nuestros ojos. Solo importa en casos sutiles, como cuando un
método se obtiene dinámicamente del objeto, usando una expresión.
BigInt
BigInt es un tipo numérico especial que provee soporte a enteros de tamaño arbitrario.
Un bigint se crea agregando n al final del literal entero o llamando a la función BigInt que
crea bigints desde cadenas, números, etc.
Operadores matemáticos
BigInt puede ser usado mayormente como un número regular, por ejemplo:
alert(1n + 2n); // 3
alert(5n / 2n); // 2
Por favor, ten en cuenta: la división 5/2 devuelve el resultado redondeado a cero, sin la
parte decimal. Todas las operaciones sobre bigints devuelven bigints.
No podemos mezclar bigints con números regulares:
Podemos convertirlos explícitamente cuando es necesario: usando BigInt() o Number() como aquí:
// De number a bigint
alert(bigint + BigInt(number)); // 3
// De bigint a number
alert(Number(bigint) + number); // 3
Comparaciones
Comparaciones tales como < , > funcionan bien entre bigints y numbers:
Por favor, nota que como number y bigint pertenecen a diferentes tipos, ellos pueden ser
iguales == , pero no estrictamente iguales === :
alert( 1 == 1n ); // true
Operaciones booleanas
Cuando están dentro de un if u otra operación booleana, los bigints se comportan como
numbers.
Por ejemplo, en if , el bigint 0n es falso, los otros valores son verdaderos:
if (0n) {
// nunca se ejecuta
}
Los operadores booleanos, tales como || , && y otros, también trabajan con bigints en forma
similar a los number:
Polyfills
Hacer Polyfill con bigints es trabajoso. La razón es que muchos operadores JavaScript
como + , - y otros se comportan de diferente manera comparados con los números regulares.
Por ejemplo, la división de bigints siempre devuelve un bigint (redondeado cuando es
necesario).
Para emular tal comportamiento, un polyfill necesitaría analizar el código y reemplazar todos
los operadores con sus funciones. Pero hacerlo es engorroso y tendría mucho costo en
performance.
Por lo que no se conoce un buen polyfill.
Suma c = a + b c = JSBI.add(a, b)
Resta c = a - b c = JSBI.subtract(a, b)
… … …
…Y entonces usar polyfill (plugin Babel) para convertir las llamadas de JSBI en bigints
nativos para aquellos navegadores que los soporten.
En otras palabras, este enfoque sugiere que escribamos código en JSBI en lugar de bigints
nativos. Pero JSBI trabaja internamente tanto con numbers como con bigints, los emula
siguiendo de cerca la especificación, entonces el código será “bigint-ready” (preparado para
bigint).
Podemos usar tal código JSBI “tal como está” en motores que no soportan bigints, y para
aquellos que sí lo soportan – el polyfill convertirá las llamadas en bigints nativos.
Como ya mencionamos, los strings de JavaScript están basados en Unicode: cada carácter está
representado por una secuencia de entre 1 y 4 bytes.
JavaScript nos permite insertar un carácter en un string por medio de su código hexadecimal
Unicode, usando estas tres notaciones:
\xXX
XX deben ser dos dígitos hexadecimales con un valor entre 00 y FF . Entonces, \xXX es el
carácter cuyo código Unicode es XX .
Como la notación \xXX admite solo dos dígitos hexadecimales, puede representar solamente
los primeros 256 caracteres Unicode.
Estos primeros 256 caracteres incluyen el alfabeto latino, la mayoría de caracteres de
sintaxis básicos, y algunos otros. Por ejemplo, "\x7A" es lo mismo que "z" (Unicode U+007A ).
alert( "\x7A" ); // z
alert( "\xA9" ); // ©, el símbolo de copyright
\uXXXX XXXX deben ser exactamente 4 dígitos hexadecimales con un valor entre 0000 y FFFF .
\u{X…XXXXXX}
X…XXXXXX debe ser un valor hexadecimal de 1 a 6 bytes entre 0 y 10FFFF (el mayor punto de
código definido por Unicode). Esta notación nos permite fácilmente representar todos los
caracteres Unicode existentes.
Esto es porque los pares sustitutos no existían cuando JavaScript fue creado, por ello no es
procesado correctamente por el lenguaje.
En realidad tenemos un solo símbolo en cada línea de los string de arriba, pero la
propiedad length los muestra con un largo de 2 .
Obtener un símbolo puede ser intrincado, porque la mayoría de las características del
lenguaje trata a los pares sustitutos como de 2 caracteres.
Por ejemplo, aquí vemos dos caracteres extraños en la salida:
Las 2 partes del par sustituto no tienen significado el uno sin el otro. Entonces las alertas
del ejemplo en realidad muestran basura.
Técnicamente, los pares sustitutos son también detectables por su propio código: si un
carácter tiene código en el intervalo de 0xd800..0xdbff , entonces es la primera parte de un par
sustituto. El siguiente carácter (segunda parte) debe tener el código en el
intervalo 0xdc00..0xdfff . Estos intervalos son reservados exclusivamente para pares sustitutos
por el estándar.
Los métodos String.fromCodePoint y str.codePointAt fueron añadidos en JavaScript para manejar
los pares sustitutos.
Dicho esto, si tomamos desde la posición 1 (y hacerlo es incorrecto aquí), ambas funciones
devolverán solo la segunda parte del par:
Aquí podemos ver basura (la primera mitad del par sustituto de la sonrisa) en la salida.
Simplemente sé consciente de esto si quieres trabajar con confianza con los pares sustitutos.
Puede que no sea un gran problema, pero al menos deberías entender lo que pasa.
alert('S\u0307'); // Ṡ
Si necesitamos una marca adicional sobre la letra (o debajo de ella), no hay problema,
simplemente se agrega el carácter de marca necesario.
Por ejemplo, si agregamos un carácter “punto debajo” (código \u0323 ), entonces tendremos" S
con puntos arriba y abajo ": Ṩ .
Ejemplo:
alert( 'S\u0307\u0323' ); // Ṩ
Esto proporciona una gran flexibilidad, pero también un problema interesante: dos caracteres
pueden ser visualmente iguales, pero estar representados con diferentes composiciones
Unicode.
Por ejemplo:
Para resolver esto, existe un algoritmo de “normalización Unicode” que lleva cada cadena a la
forma “normal”.
Este es implementado por str.normalize().
alert( "S\u0307\u0323".normalize().length ); // 1
En realidad, este no es siempre el caso. La razón es que el símbolo Ṩ es “bastante común”,
por lo que los creadores de Unicode lo incluyeron en la tabla principal y le dieron el
código.
Si desea obtener más información sobre las reglas y variantes de normalización, se describen
en el apéndice del estándar: Unicode, pero para la mayoría de los propósitos prácticos, la
información de esta sección es suficiente.