Ir al contenido
wannacode

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);
Salida
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);
Salida
Pedido 1: 12.50 €
Pedido 2: 12.50 €
Pedido 3: 12.50 €
  • const parts = [] es un array vacío. push añ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, escribes parts.join("").
  • El salto de línea entre los trozos viene del separador, por eso aquí los trozos no llevan \n al 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
  1. report apunta a una cadena vacía.
  2. El primer += crea una cadena nueva con una línea: 18 caracteres.
  3. 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.
  4. 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.
  5. parts es un array vacío: en él irán los trozos.
  6. push mete cada trozo en el array en su sitio. Los trozos viejos no se copian, y el array sigue siendo el mismo.
  7. 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 que join es 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(""));
Salida
4532 0151 1283 0366
**** **** **** 0366
0888123456
amor

card.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);
Respuesta

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");
Salida
-1
0
true
false
  • 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(", "));
Salida
Alonso, Blanco, Iglesias, Torres

names.join(", ") pega los elementos del array con un separador entre ellos.

Más precisamente, para curiosos
  • localeCompare con 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(", "));
Salida
-1
0
1
Alonso, Blanco, Iglesias, Torres

Prué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)));
Respuesta

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));
Salida
lucia.gomez
empresa.es
19
Lucí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, y indexOf(".", at) empieza a buscar solo desde el índice at en adelante. lastIndexOf busca desde atrás.
  • email.slice(0, at) es desde el principio hasta at, sin él. email.slice(at + 1) es desde at + 1 hasta el final. join es lo contrario de split.
  • "=".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() === "");
Salida
true false
true true

length === 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);
Salida
42-2.4

Number() 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)} €`);
Salida
Café 2 x 2.40 = 4.80
Cruasán 1 x 3.10 = 3.10
Agua 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 porque padStart es un método de las cadenas, no de los números.
Más precisamente, para curiosos
  • a.concat(b) es lo mismo que a + b. Lo verás en código más antiguo.
  • padEnd y padStart no cortan un texto más largo: empuja la columna hacia la derecha.
  • Number("2,40") no detiene el programa. Da NaN («no es un número»), y el programa sigue con él. toFixed siempre escribe un punto, mientras que toLocaleString("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"));
Salida
NaN
2.40
2,40
Café y cruasán

Prué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.

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.

Entrada
Anita lava la tina
Salida
Palíndromo
Entrada
Anita lava la ropa
Salida
No es un palíndromo

2. 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.

Entrada
3
Pan;1.20;2
Queso;9.80;1
Huevos;0.35;10
Salida
Pan 2 x 1.20 = 2.40
Queso 1 x 9.80 = 9.80
Huevos 10 x 0.35 = 3.50
------------------------------------
Total 15.70

3. 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.

Entrada
Mañana a las 9 hay reunión
Salida
reunión hay 9 las a Mañana

Má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.es crea l**********@empresa.es: la primera letra y después tantos asteriscos como caracteres quedan antes de la @. Usa indexOf y repeat.
  • Un recorrido por las cadenas. Crea una cadena con comillas ("Texto de ejemplo") y otra con new String("Texto de ejemplo"). Compáralas con === y con ==, toma slice(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 con join. Predice cada línea antes de ejecutar el programa. (new String crea 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 localeCompare para que pase { sensitivity: "base" }, y pruébalo con «lucas», «Bruno» y «ana».

Test

Ponte a prueba

1. ¿Por qué en Node report += line no es lento en un bucle con 50 000 vueltas?
2. ¿Qué devuelve "Blanco".localeCompare("Alonso", "es")?
3. const a = ["12"]; const b = a; b.push("3"); ¿Qué da a.join("")?
4. parts es un array de cadenas. ¿Cómo haces con él una sola cadena, sin separadores?
5. ¿Qué da (2.4).toFixed(2).padStart(6)?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·