Cadenas
En resumen: Una cadena en JavaScript no cambia: cada += crea un texto nuevo, por eso muchos trozos se juntan con un array y join, y localeCompare compara en orden alfabético.
Problema
Una cadena nueva en cada +=
Ya sabes que title.toUpperCase(); por sí solo no hace nada: una cadena es inmutable, y el método devuelve una cadena nueva. Esto tiene un coste, y se ve en un bucle.
El programa de la tienda en línea construye el informe del día línea por línea:
let report = "";for (let i = 1; i <= 3; i++) { report += `Pedido ${i}: 12.50 €\n`;}process.stdout.write(report);Pedido 1: 12.50 €Pedido 2: 12.50 €Pedido 3: 12.50 €process.stdout.write imprime el texto sin añadir un salto de línea al final: el informe ya termina con \n.
Según las reglas del lenguaje, cada report += ... no añade nada a la cadena vieja. Crea una cadena nueva: todo el informe hasta ahora más la línea nueva. Si el motor de verdad copiara todo en cada vuelta, hacia el final de 50 000 pedidos cada vuelta copiaría más de un millón de caracteres. Eso es lo que pasa en Java (unos 2,5 segundos) y en Python (unos 6,5 segundos).
En Node el mismo bucle con 50 000 pedidos tarda unos 8 milisegundos. Lo medimos en la máquina donde revisamos el código de las lecciones. V8, el motor que ejecuta JavaScript, hace trampa: con += no copia. Recuerda los dos trozos uno al lado del otro y los pega todos de una vez cuando necesitas el texto. Es un truco del motor, no una regla del lenguaje. La cadena sigue siendo inmutable.
La idea
Un array de trozos y join
JavaScript no tiene una cadena que cambie en su sitio. Por eso juntas los trozos en un array, que sí cambia en su sitio. Al final, join los pega en una sola cadena y copia cada carácter una sola vez:
const parts = [];for (let i = 1; i <= 3; i++) { parts.push(`Pedido ${i}: 12.50 €`);}const report = parts.join("\n");console.log(report);Pedido 1: 12.50 €Pedido 2: 12.50 €Pedido 3: 12.50 €const parts = []es un array vacío.pushañade un trozo al final sin tocar los demás.parts.join("\n")crea una sola cadena con todos los trozos, con el separador"\n"entre ellos. Para pegar sin separador, escribesparts.join("").- El salto de línea entre los trozos viene del separador, por eso aquí los trozos no llevan
\nal final.
En Node las dos formas son rápidas: con 50 000 pedidos join tarda unos 5 milisegundos y += unos 8. Un array y join son la forma habitual cuando hay muchos trozos o necesitas un separador entre ellos, y no dependen del truco del motor.
Esto es lo que dicen las reglas del lenguaje sobre cada forma. Con += V8 aplaza la copia, pero la cadena nueva sigue siendo una cadena nueva:
let report = "";for (let i = 1; i <= 3; i++) { report += `Pedido ${i}: 12.50 €\n`;}const parts = [];for (let i = 1; i <= 3; i++) { parts.push(`Pedido ${i}: 12.50 €\n`);}const joined = parts.join("");console.log(report.length, joined.length);console.log(report === joined);Paso a paso, en palabras
- report apunta a una cadena vacía.
- El primer += crea una cadena nueva con una línea: 18 caracteres.
- Según las reglas del lenguaje, el segundo += da otra cadena: la línea vieja (18 caracteres) y la nueva. La cadena vieja ya no le sirve a nadie.
- El tercero da una cadena con 36 caracteres viejos y la línea nueva. Java y Python los copian cada vez. V8 aplaza la copia: guarda los trozos y los pega una sola vez.
- parts es un array vacío: en él irán los trozos.
- push mete cada trozo en el array en su sitio. Los trozos viejos no se copian, y el array sigue siendo el mismo.
- join pega los trozos una sola vez: 54 caracteres. El texto es el mismo. Según las reglas, += copia otros 54 caracteres viejos, y join ni uno solo.
Más precisamente, para curiosos
- V8 llama «cuerda» (rope) a los trozos pegados. Cuando lees un carácter de una cadena así, el motor la pega en una sola cadena, una vez.
- Una sola plantilla literal como
`Pedido ${i}: ${price}`es una sola cadena nueva. Cuánto copia+=en un bucle depende del motor, mientras quejoines predecible en todas partes.
Míralo
Cambiar con slice
Una cadena no cambia en su sitio, así que cada cambio es una cadena nueva, construida con partes de la vieja. Aquí tienes un número de tarjeta para imprimirlo en un recibo:
let card = "4532015112830366";card = card.slice(0, 12) + " " + card.slice(12);card = card.slice(0, 8) + " " + card.slice(8);card = card.slice(0, 4) + " " + card.slice(4);console.log(card);card = "**** **** ****" + card.slice(14);console.log(card);
let phone = "0888-123-456";phone = phone.slice(0, 8) + phone.slice(9);phone = phone.slice(0, 4) + phone.slice(5);console.log(phone);console.log([..."roma"].reverse().join(""));4532 0151 1283 0366**** **** **** 03660888123456amorcard.slice(0, 12) son los caracteres desde el principio hasta el índice 12, sin él. card.slice(12) es desde el índice 12 hasta el final.
| Quieres | Escribes |
|---|---|
insertar x antes del índice i |
s.slice(0, i) + x + s.slice(i) |
reemplazar los caracteres desde a hasta b (sin b) por x |
s.slice(0, a) + x + s.slice(b) |
quitar el carácter con índice i |
s.slice(0, i) + s.slice(i + 1) |
quitar los caracteres desde a hasta b (sin b) |
s.slice(0, a) + s.slice(b) |
| dar la vuelta al texto, de atrás hacia delante | [...s].reverse().join("") |
| la longitud, un carácter | s.length, s[i] |
[...s] crea un array con los caracteres de la cadena. El array tiene reverse(), y join("") lo vuelve a pegar.
Los espacios del número de tarjeta se insertan de atrás hacia delante: primero en 12, después en 8, después en 4. Si empiezas por 4, el espacio insertado desplaza todo lo que hay detrás, y 8 ya no es el lugar que quieres. Por la misma razón, los guiones del teléfono se quitan primero por detrás.
Pruébalo
Dos nombres, un objeto
¿Qué imprimirá el programa?
const note = ["Mesa 4"];const copy = note;copy.push(", 2 cafés");console.log(note.join(""));
const text = "Mesa 4";let other = text;other += ", 2 cafés";console.log(text);copy = note copia la flecha, las dos variables apuntan al mismo array, y push lo cambia en su sitio. Con una cadena es distinto: other += ... crea una cadena nueva y mueve solo other hacia ella. text sigue apuntando a la vieja.La idea
En orden alfabético: localeCompare
=== solo te dice «¿son iguales?». Para ordenar necesitas más: qué cadena va antes que la otra. Eso es lo que te dice localeCompare:
console.log("Alonso".localeCompare("Blanco", "es"));console.log("Blanco".localeCompare("Blanco", "es"));console.log("Alonso" < "Blanco");console.log("iglesias" < "Iglesias");-10truefalse- un número negativo: la primera va antes que la segunda;
- 0: son iguales;
- un número positivo: la primera va después de la segunda.
"es" indica que se compare según las reglas del español. También puedes comparar cadenas con < y >, pero entonces se compara por el código de cada carácter: las mayúsculas van antes que las minúsculas, por eso "iglesias" < "Iglesias" es false.
Lo que importa es el signo, no el número en sí. Con él ordenas una lista de nombres, por ejemplo con el ordenamiento de burbuja de la lección sobre arrays:
const names = ["Torres", "Alonso", "Iglesias", "Blanco"];for (let i = 0; i < names.length - 1; i++) { for (let j = 0; j < names.length - 1 - i; j++) { if (names[j].localeCompare(names[j + 1], "es") > 0) { const tmp = names[j]; names[j] = names[j + 1]; names[j + 1] = tmp; } }}console.log(names.join(", "));Alonso, Blanco, Iglesias, Torresnames.join(", ") pega los elementos del array con un separador entre ellos.
Más precisamente, para curiosos
localeComparecon las reglas de"es"pone la minúscula antes que la mayúscula:"iglesias".localeCompare("Iglesias", "es")es -1. Con{ sensitivity: "base" }no distingue mayúsculas y minúsculas y da 0.- Con
{ numeric: true }compara los números del texto como números: entonces"INV-10"va después de"INV-9". names.sort()ordena el array sin el ordenamiento de burbuja, pero por los códigos de los caracteres, como<. Para estos nombres el resultado es el mismo.
console.log("iglesias".localeCompare("Iglesias", "es"));console.log("iglesias".localeCompare("Iglesias", "es", { sensitivity: "base" }));console.log("INV-10".localeCompare("INV-9", "es", { numeric: true }));const names = ["Torres", "Alonso", "Iglesias", "Blanco"];names.sort();console.log(names.join(", "));-101Alonso, Blanco, Iglesias, TorresPruébalo
¿Factura 10 o 9?
Los números de las facturas son texto. ¿Cuál va antes?
¿Qué imprimirá el programa?
const a = "INV-10";const b = "INV-9";console.log(a < b);console.log(Number(a.slice(4)) < Number(b.slice(4)));El texto se compara carácter por carácter. Los cuatro primeros caracteres son iguales, después vienen «1» y «9», y «1» va antes que «9». Por eso "INV-10" va antes que "INV-9", igual que en el diccionario «ba» va antes que «c». Los números 10 y 9 se comparan como números solo después de Number(). Si ordenas números como texto, rellénalos con ceros: INV-009, INV-010.
La idea
Buscar, cortar, juntar
Aquí tienes una línea de un archivo de contactos: nombre, correo y ciudad, separados con « ; », con espacios de sobra en los extremos:
const line = " Lucía Gómez ; lucia.gomez@empresa.es ; Sevilla ";const parts = line.trim().split(" ; ");const email = parts[1];const at = email.indexOf("@");console.log(email.slice(0, at));console.log(email.slice(at + 1));console.log(email.indexOf(".", at));console.log(parts.join(" | "));console.log("=".repeat(20));lucia.gomezempresa.es19Lucía Gómez | lucia.gomez@empresa.es | Sevilla====================trim()quita los espacios de delante y de detrás, y también los espacios «anchos» de otros idiomas.indexOf("@")da el índice de la primera aparición, yindexOf(".", at)empieza a buscar solo desde el índiceaten adelante.lastIndexOfbusca desde atrás.email.slice(0, at)es desde el principio hastaat, sin él.email.slice(at + 1)es desdeat + 1hasta el final.joines lo contrario desplit."=".repeat(20)repite el texto 20 veces.
Vacío y «en blanco» no son lo mismo:
const empty = "";const spaces = " ";console.log(empty.length === 0, spaces.length === 0);console.log(empty.trim() === "", spaces.trim() === "");true falsetrue truelength === 0 es verdadero solo para un texto vacío. trim() === "" también es verdadero cuando solo hay espacios. Para revisar un campo que alguien ha rellenado, normalmente quieres lo segundo.
La idea
Números y texto
Los números entran en el texto y salen del texto:
const code = Number("0042");const price = Number("2.40");const label = String(code) + "-" + String(price);console.log(label);42-2.4Number() ya lo conoces. String() es lo contrario: crea texto a partir de un número o de un boolean.
Para una tabla con columnas necesitas un ancho para cada columna. Aquí tienes un recibo de una cafetería:
const names = ["Café", "Cruasán", "Agua mineral"];const prices = [2.4, 3.1, 1.5];const qty = [2, 1, 3];let total = 0;for (let i = 0; i < names.length; i++) { const amount = prices[i] * qty[i]; total += amount; console.log(`${names[i].padEnd(16)}${String(qty[i]).padStart(2)} x ${prices[i].toFixed(2).padStart(5)} = ${amount.toFixed(2).padStart(6)}`);}console.log("-".repeat(35));console.log(`${"Total".padEnd(26)}${total.toFixed(2).padStart(9)} €`);Café 2 x 2.40 = 4.80Cruasán 1 x 3.10 = 3.10Agua mineral 3 x 1.50 = 4.50-----------------------------------Total 12.40 €padEnd(16)rellena el texto con espacios por la derecha hasta 16 caracteres: queda alineado a la izquierda.padStart(5)rellena por la izquierda: queda alineado a la derecha.toFixed(2)crea texto a partir del número, con 2 cifras después del punto.String(qty[i])hace falta porquepadStartes un método de las cadenas, no de los números.
Más precisamente, para curiosos
a.concat(b)es lo mismo quea + b. Lo verás en código más antiguo.padEndypadStartno cortan un texto más largo: empuja la columna hacia la derecha.Number("2,40")no detiene el programa. DaNaN(«no es un número»), y el programa sigue con él.toFixedsiempre escribe un punto, mientras quetoLocaleString("es")escribe según las reglas del español, con coma.
console.log(Number("2,40"));console.log((2.4).toFixed(2));console.log((2.4).toLocaleString("es", { minimumFractionDigits: 2 }));console.log("Café".concat(" y ", "cruasán"));NaN2.402,40Café y cruasánPruébalo
Encuentra el error
«Reconocer» se lee igual al revés, pero el programa dice «reconocer no es un palíndromo». ¿En qué línea está el error?
Elige la línea con el error.
[...word].reverse() es un array de caracteres, y word es una cadena. === entre una cadena y un array es false, tenga lo que tenga dentro. La corrección es [...word].reverse().join(""): así el array se convierte en una cadena.Ejercicio
Texto que tú juntas
Resuelve las tareas en tu editor. No imprimas preguntas.
1. Palíndromo. Lee una línea y di si es un palíndromo, sin tener en cuenta las mayúsculas, los espacios, las comas y los guiones. Esta vez da la vuelta al texto con [...text].reverse().join("") en lugar de con un bucle.
Anita lava la tinaPalíndromoAnita lava la ropaNo es un palíndromo2. Un recibo con columnas. La primera línea tiene el número de artículos. Cada línea siguiente tiene un nombre, un precio y una cantidad, separados con ;. Imprime un recibo con las columnas alineadas y el total al final.
3Pan;1.20;2Queso;9.80;1Huevos;0.35;10Pan 2 x 1.20 = 2.40Queso 1 x 9.80 = 9.80Huevos 10 x 0.35 = 3.50------------------------------------Total 15.703. Palabras al revés. Lee una frase e imprime sus palabras en orden inverso. Entre las palabras puede haber más de un espacio, y en el resultado tiene que haber exactamente uno. Júntalo con un array y join.
Mañana a las 9 hay reuniónreunión hay 9 las a MañanaMás tareas
- Iniciales. Lee un nombre completo («María López Sánchez») e imprime las iniciales con puntos: «M. L. S.» Júntalas con un array y
join. - Correo oculto. A partir de
lucia.gomez@empresa.escreal**********@empresa.es: la primera letra y después tantos asteriscos como caracteres quedan antes de la@. UsaindexOfyrepeat. - Un recorrido por las cadenas. Crea una cadena con comillas (
"Texto de ejemplo") y otra connew String("Texto de ejemplo"). Compáralas con===y con==, tomaslice(0, 5), divide la primera por el espacio e imprime las partes, convierte"100"en un número e imprímelo con una plantilla literal`Valor: ${...}`, y al final añade" y más"a un array con la primera cadena y pégalas conjoin. Predice cada línea antes de ejecutar el programa. (new Stringcrea un objeto en lugar de una cadena. El código real no lo usa.) - Ordenar sin distinguir mayúsculas. Cambia el ordenamiento de burbuja de la tarjeta de
localeComparepara que pase{ sensitivity: "base" }, y pruébalo con «lucas», «Bruno» y «ana».
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Cadenas: trabajar con texto · Clases y objetos · Métodos de instancia · Herencia
- Continúa en: Expresiones regulares · Expresiones regulares: grupos y reemplazo · Archivos · Datos en archivos
- Ejercicios sobre el tema: Texto y archivos